dead-code-detector
dead-code-detector copied to clipboard
How to deal with form dataclasses that have methods called by symfony property accessor
Symfony's property accessor component automatically calls getProperty/setProperty in forms, how do we ensure these methods are not marked as dead.
Do you mean that something like this:
<?php
namespace App\Form;
use App\Dto\Product;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\NumberType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class ProductType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('name', TextType::class, [
'label' => 'Product Name',
])
->add('price', NumberType::class, [
'label' => 'Price',
]);
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => Product::class,
]);
}
}
should mark Product's getters and setters (for name and price) as used?
should mark Product's getters and setters (for name and price) as used?
Yes exactly, this is a common pattern in lot of symfony apps.
I managed to extract my custom form type's data classes via symfony's debug cmd and then marked usage for their getter/setter methods. In my project it ended up being sufficient.
<?php
declare(strict_types=1);
namespace MyApp\Tools\PHPStan;
use PHPStan\DependencyInjection\Container;
use ShipMonk\PHPStan\DeadCode\Provider\ReflectionBasedMemberUsageProvider;
use ShipMonk\PHPStan\DeadCode\Provider\VirtualUsageData;
use Symfony\Component\Process\Process;
class FormDataclassUsageProvider extends ReflectionBasedMemberUsageProvider
{
/** @var list<class-string> */
private array $formDataClasses = [];
public function __construct(Container $phpstanContainer)
{
$this->formDataClasses = $this->extractFormDataClasses();
}
public function shouldMarkMethodAsUsed(\ReflectionMethod $method): ?VirtualUsageData
{
$declaringClass = $method->getDeclaringClass();
if (
\in_array($declaringClass->getName(), $this->formDataClasses, true) &&
(
1 === \preg_match(
'/^(get|set|has|is|add|remove)[A-Z].*$/',
$method->getName()
)
)
) {
return VirtualUsageData::withNote('Method maybe used in Symfony form data_class via property accessor');
}
return null;
}
/**
* @return list<class-string>
*/
private function extractFormDataClasses(): array
{
$formDataClasses = [];
$consolePath = 'bin/console';
$process = new Process(
command: ['php', $consolePath, 'debug:form', '--format=json'],
);
$process->setTimeout(2);
$process->mustRun();
$output = $process->getOutput();
\assert(\json_validate($output), $output);
$formTypesData = json_decode($output, true);
\assert(is_array($formTypesData));
\assert(array_key_exists('service_form_types', $formTypesData));
$formTypeClasses = $formTypesData['service_form_types'];
\assert(is_array($formTypeClasses));
foreach ($formTypeClasses as $formType) {
\assert(is_string($formType));
$process = new Process(
command: [
'php',
$consolePath,
'debug:form',
$formType,
'data_class',
'--format=json'
],
env: $_ENV
);
$process->setTimeout(2);
$process->run();
// if the form type class cannot be instantiated, we can't fetch it's options
// so we skip it
if(!$process->isSuccessful()) {
continue;
}
$output = $process->getOutput();
\assert(\json_validate($output), $output);
$formTypeData = json_decode($output, true);
\assert(is_array($formTypeData));
\assert(array_key_exists('default', $formTypeData));
$dataClass = $formTypeData['default'];
if(null === $dataClass) {
continue;
}
\assert(\is_string($dataClass));
\assert(class_exists($dataClass));
$formDataClasses[] = $dataClass;
}
return $formDataClasses;
}
}