Partage d'export enregistré et génération asynchrone des exports

This commit is contained in:
2025-07-08 13:53:25 +00:00
parent c4cc0baa8e
commit 8bc16dadb0
447 changed files with 14134 additions and 3854 deletions

View File

@@ -0,0 +1,359 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\Utils\Rector\Rector;
use Chill\MainBundle\Export\AggregatorInterface;
use Chill\MainBundle\Export\DirectExportInterface;
use Chill\MainBundle\Export\ExportDataNormalizerTrait;
use Chill\MainBundle\Export\ExportInterface;
use Chill\MainBundle\Export\FilterInterface;
use Chill\MainBundle\Export\FormatterInterface;
use Chill\MainBundle\Export\ListInterface;
use Chill\MainBundle\Form\Type\ChillDateTimeType;
use Chill\MainBundle\Form\Type\ChillDateType;
use Chill\MainBundle\Form\Type\PickRollingDateType;
use Chill\MainBundle\Form\Type\PickUserDynamicType;
use Chill\MainBundle\Form\Type\PickUserLocationType;
use Chill\MainBundle\Form\Type\Select2CountryType;
use Chill\MainBundle\Service\RollingDate\RollingDate;
use Chill\ThirdPartyBundle\Form\Type\PickThirdpartyDynamicType;
use PhpParser\Node;
use PhpParser\Node\Expr\PropertyFetch;
use Rector\Rector\AbstractRector;
use Rector\Symfony\NodeAnalyzer\ClassAnalyzer;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\Extension\Core\Type\DateTimeType;
use Symfony\Component\Form\Extension\Core\Type\DateType;
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
class ChillBundleAddNormalizationMethodsOnExportRector extends AbstractRector
{
public function __construct(
private ClassAnalyzer $classAnalyzer,
) {}
public function getRuleDefinition(): RuleDefinition
{
return new RuleDefinition(
'Add methods `normalizeFormData` and `denormalizeFormData` on Export, Filter, Aggregator, and Formatter',
[]
);
}
public function getNodeTypes(): array
{
return [Node\Stmt\Class_::class];
}
public function refactor(Node $node)
{
if (!$node instanceof Node\Stmt\Class_) {
return null;
}
if (
!$this->classAnalyzer->hasImplements($node, FilterInterface::class)
&& !$this->classAnalyzer->hasImplements($node, AggregatorInterface::class)
&& !$this->classAnalyzer->hasImplements($node, ExportInterface::class)
&& !$this->classAnalyzer->hasImplements($node, DirectExportInterface::class)
&& !$this->classAnalyzer->hasImplements($node, ListInterface::class)
&& !$this->classAnalyzer->hasImplements($node, FormatterInterface::class)
) {
return null;
}
$hasDenormalizeMethod = false;
$hasNormalizedMethod = false;
$hasGetVersionMethod = false;
$buildFormStmtIndex = null;
$buildForm = null;
$hasTraitHelper = false;
foreach ($node->stmts as $k => $stmt) {
if ($stmt instanceof Node\Stmt\TraitUse) {
$trait = $stmt->traits[0];
if (str_contains($trait->toString(), ExportDataNormalizerTrait::class)) {
$hasTraitHelper = true;
}
continue;
}
if (!$stmt instanceof Node\Stmt\ClassMethod) {
continue;
}
if ('buildForm' === $stmt->name->name) {
$buildForm = $stmt;
$buildFormStmtIndex = $k;
} elseif ('normalizeFormData' === $stmt->name->name) {
$hasNormalizedMethod = true;
} elseif ('denormalizeFormData' === $stmt->name->name) {
$hasDenormalizeMethod = true;
} elseif ('getNormalizationVersion' === $stmt->name->name) {
$hasGetVersionMethod = true;
}
}
if ($hasDenormalizeMethod && $hasNormalizedMethod && $hasGetVersionMethod) {
return null;
}
$toAddBefore = [];
$toAdd = [];
$stmtBefore = array_slice($node->stmts, 0, $buildFormStmtIndex, false);
$stmtAfter = array_slice($node->stmts, $buildFormStmtIndex + 1);
$propertiesOnForm = $this->getPropertiesOnForm($buildForm);
// if the trait is not present, we add it into the statements
if (
!$hasTraitHelper
&& (!$hasNormalizedMethod || !$hasDenormalizeMethod)
&& array_reduce(
$propertiesOnForm,
function (bool $carry, string $item): bool {
if ('entity' === $item || 'date' === $item) {
return true;
}
return $carry;
},
false,
)) {
$toAddBefore[] = new Node\Stmt\TraitUse([
new Node\Name('\\'.ExportDataNormalizerTrait::class),
]);
}
// if we do not have the `getNormalizerVersion` method
if (!$hasGetVersionMethod) {
$toAdd[] = $this->buildGetNormalizationVersionMethod();
}
// if we do not have the `normalizeFormData` method
if (!$hasNormalizedMethod) {
$toAdd[] = $this->buildNormalizeFormDataMethod($propertiesOnForm);
}
if (!$hasDenormalizeMethod) {
$toAdd[] = $this->buildDenormalizeFormDataMethod($propertiesOnForm);
}
$node->stmts = [
...array_values($toAddBefore),
...array_values($stmtBefore),
$buildForm,
...array_values($toAdd),
...array_values($stmtAfter),
];
return $node;
}
private function buildDenormalizeFormDataMethod(array $propertiesOnForm): Node\Stmt\ClassMethod
{
$array = new Node\Expr\Array_([]);
foreach ($propertiesOnForm as $name => $kind) {
$arrayDimFetch = new Node\Expr\ArrayDimFetch(
new Node\Expr\Variable('formData'),
new Node\Scalar\String_($name),
);
$array->items[] = new Node\Expr\ArrayItem(
match ($kind) {
'entity' => new Node\Expr\MethodCall(
new Node\Expr\Variable('this'),
new Node\Identifier('denormalizeDoctrineEntity'),
[
new Node\Arg($arrayDimFetch),
new Node\Arg(
new PropertyFetch(
new Node\Expr\Variable('this'),
'someRepository',
),
),
],
),
'rolling_date' => new Node\Expr\StaticCall(
new Node\Name\FullyQualified(RollingDate::class),
'fromNormalized',
[new Node\Arg($arrayDimFetch)],
),
'date' => new Node\Expr\MethodCall(
new Node\Expr\Variable('this'),
new Node\Identifier('denormalizeDate'),
[
new Node\Arg($arrayDimFetch),
],
),
'scalar' => $arrayDimFetch,
default => $arrayDimFetch,
},
new Node\Scalar\String_($name),
);
}
return new Node\Stmt\ClassMethod(
'denormalizeFormData',
[
'flags' => Node\Stmt\Class_::MODIFIER_PUBLIC,
'returnType' => new Node\Identifier('array'),
'params' => [
new Node\Param(new Node\Expr\Variable('formData'), type: new Node\Identifier('array')),
new Node\Param(new Node\Expr\Variable('fromVersion'), type: new Node\Identifier('int')),
],
'stmts' => [
new Node\Stmt\Return_($array),
],
],
);
}
private function buildNormalizeFormDataMethod(array $propertiesOnForm): Node\Stmt\ClassMethod
{
$array = new Node\Expr\Array_([]);
foreach ($propertiesOnForm as $name => $kind) {
$arrayDimFetch = new Node\Expr\ArrayDimFetch(
new Node\Expr\Variable('formData'),
new Node\Scalar\String_($name),
);
$array->items[] = new Node\Expr\ArrayItem(
match ($kind) {
'entity' => new Node\Expr\MethodCall(
new Node\Expr\Variable('this'),
new Node\Identifier('normalizeDoctrineEntity'),
[
new Node\Arg($arrayDimFetch),
],
),
'rolling_date' => new Node\Expr\MethodCall(
$arrayDimFetch,
new Node\Identifier('normalize'),
),
'date' => new Node\Expr\MethodCall(
new Node\Expr\Variable('this'),
new Node\Identifier('normalizeDate'),
[
new Node\Arg($arrayDimFetch),
],
),
'scalar' => $arrayDimFetch,
default => $arrayDimFetch,
},
new Node\Scalar\String_($name),
);
}
return new Node\Stmt\ClassMethod(
'normalizeFormData',
[
'flags' => Node\Stmt\Class_::MODIFIER_PUBLIC,
'returnType' => new Node\Identifier('array'),
'params' => [
new Node\Param(new Node\Expr\Variable('formData'), type: new Node\Identifier('array')),
],
'stmts' => [
new Node\Stmt\Return_($array),
],
],
);
}
private function buildGetNormalizationVersionMethod(): Node\Stmt\ClassMethod
{
return new Node\Stmt\ClassMethod(
'getNormalizationVersion',
[
'flags' => Node\Stmt\Class_::MODIFIER_PUBLIC,
'returnType' => new Node\Identifier('int'),
'stmts' => [
new Node\Stmt\Return_(
new Node\Scalar\LNumber(1)
),
],
],
);
}
/**
* @return array<string, 'entity'|'scalar'|'rolling_date'|'date'>
*/
private function getPropertiesOnForm(Node\Stmt\ClassMethod $buildFormMethod): array
{
$builderName = $buildFormMethod->params[0]->var->name;
$values = [];
foreach ($buildFormMethod->stmts as $stmt) {
if ($stmt instanceof Node\Stmt\Expression
// it must be a method call
&& $stmt->expr instanceof Node\Expr\MethodCall
&& 'add' === $stmt->expr->name->toString()
) {
$newValues = $this->handleMethodCallAdd($stmt->expr, $builderName);
if (false === $newValues) {
continue;
}
$values = [...$values, ...$newValues];
}
}
return $values;
}
private function handleMethodCallAdd(Node\Expr\MethodCall $call, string $builderName): array|false
{
if ($call->var instanceof Node\Expr\Variable) {
// in this case, the call is done on the form builder: $formBuilder->add
// or the last method call on the form builder, for instance $formBuilder->add( )->add( )
if ($builderName !== $call->var->name) {
return false;
}
} elseif ($call->var instanceof Node\Expr\MethodCall && 'add' === $call->var->name->toString()) {
// in this case, we have a chained method call: something like $formbuilder->add()->add().
// we have to go deeper into the call to get the information from them
$previous = $this->handleMethodCallAdd($call->var, $builderName);
if (false === $previous) {
return false;
}
}
$arg0 = $call->args[0] ?? null;
if (null === $arg0) {
throw new \UnexpectedValueException("The first argument of an 'add' call method is empty");
}
if (!$arg0->value instanceof Node\Scalar\String_) {
throw new \UnexpectedValueException("The first argument of an 'add' call is not a string");
}
$key = $arg0->value->value;
/** @var Node\Expr\ClassConstFetch $argType */
$argType = $call->args[1]->value;
return [
...$previous ?? [],
$key => match($argType->class->toString()) {
EntityType::class, PickUserDynamicType::class, PickUserLocationType::class, PickThirdpartyDynamicType::class, Select2CountryType::class => 'entity',
PickRollingDateType::class => 'rolling_date',
ChillDateType::class, ChillDateTimeType::class, DateTimeType::class, DateType::class => 'date',
default => 'scalar',
},
];
}
}