Skip to content

Instantly share code, notes, and snippets.

@janedbal
Last active November 12, 2024 14:13
Show Gist options
  • Save janedbal/15d777bd60186dcd70015825c318fbd8 to your computer and use it in GitHub Desktop.
Save janedbal/15d777bd60186dcd70015825c318fbd8 to your computer and use it in GitHub Desktop.
This rule ensures your DQLs can be undertood by phpstan/phpstan-doctrine. See https://docs.google.com/presentation/d/1DunYnntmODLv4CvDbyW_-dRUWdvntgadlqPhnxrCiac/edit#slide=id.g3095c091180_0_115
<?php declare(strict_types = 1);
namespace ShipMonk\Rules\PHPStan\Rule;
use Doctrine\ORM\Query;
use PhpParser\Node;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Identifier;
use PhpParser\Node\VariadicPlaceholder;
use PHPStan\Analyser\Scope;
use PHPStan\Rules\IdentifierRuleError;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;
/**
* @implements Rule<MethodCall>
*/
class EnsureAnalysableDoctrineGetResultCallsRule implements Rule
{
public function getNodeType(): string
{
return MethodCall::class;
}
/**
* @param MethodCall $node
* @return list<IdentifierRuleError>
*/
public function processNode(
Node $node,
Scope $scope,
): array
{
$methodName = $node->name;
if (!$methodName instanceof Identifier) {
return [];
}
$methodNameString = $methodName->toString();
$object = $scope->getType($node->var);
foreach ($object->getObjectClassReflections() as $callerReflection) {
if (!$callerReflection->is(Query::class)) {
return [];
}
}
if ($methodNameString === 'getOneOrNullResult' || $methodNameString === 'getSingleResult') {
return $this->validateCallRequiringHydratationMode($methodNameString, $node, $scope, false);
}
if ($methodNameString === 'getResult') {
return $this->validateCallRequiringHydratationMode($methodNameString, $node, $scope, true);
}
if ($methodNameString === 'getScalarResult' || $methodNameString === 'getArrayResult') {
return $this->validateUnanalysableCall($methodNameString, 'getResult');
}
if ($methodNameString === 'getSingleScalarResult') {
return $this->validateUnanalysableCall($methodNameString, 'getSingleResult(Query::HYDRATE_OBJECT)[\'column\']');
}
if ($methodNameString === 'getSingleColumnResult') {
return $this->validateUnanalysableCall($methodNameString, 'getResult() + array_column');
}
if ($methodNameString === 'iterate') {
return $this->validateUnanalysableCall($methodNameString, 'toIterable()');
}
if ($methodNameString === 'toIterable') {
return $this->validateCallRequiringHydratationMode($methodNameString, $node, $scope, true, 1);
}
return [];
}
/**
* @return list<IdentifierRuleError>
*/
private function validateCallRequiringHydratationMode(
string $methodName,
MethodCall $methodCall,
Scope $scope,
bool $noArgumentIsFine,
int $hydratationModeArgumentIndex = 0,
): array
{
$errorInfix = $noArgumentIsFine ? ' (or no argument)' : '';
$error = RuleErrorBuilder::message(
"Method {$methodName} is required to be called with Query::HYDRATE_OBJECT{$errorInfix} to ensure type-infering by PHPStan.",
)
->identifier('shipmonk.ensureAnalysableDoctrineGetResultCalls')
->build();
if (!isset($methodCall->args[$hydratationModeArgumentIndex])) {
if ($noArgumentIsFine) {
return [];
}
return [$error];
}
$arg = $methodCall->args[$hydratationModeArgumentIndex];
if ($arg instanceof VariadicPlaceholder) {
return [];
}
$argType = $scope->getType($arg->value);
$constantScalarValues = $argType->getConstantScalarValues();
foreach ($constantScalarValues as $constantScalarValue) {
if ($constantScalarValue !== Query::HYDRATE_OBJECT) {
return [$error];
}
}
return [];
}
/**
* @return list<IdentifierRuleError>
*/
private function validateUnanalysableCall(
string $methodName,
string $replacementSuggestion,
): array
{
return [
RuleErrorBuilder::message(
"Method $methodName cannot be analysed by PHPStan to provide type-infering and therefore is forbidden. Use {$replacementSuggestion} instead.",
)
->identifier('shipmonk.ensureAnalysableDoctrineGetResultCalls')
->build(),
];
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment