Merge remote-tracking branch 'origin/ticket-app-master' into migrate_to_sf72

# Conflicts:
#	.gitlab-ci.yml
#	composer.json
#	config/services.yaml
#	phpunit.xml.dist
#	src/Bundle/ChillAsideActivityBundle/src/Entity/AsideActivity.php
#	src/Bundle/ChillCalendarBundle/Entity/CancelReason.php
#	src/Bundle/ChillCalendarBundle/Messenger/Handler/CalendarRemoveHandler.php
#	src/Bundle/ChillCalendarBundle/RemoteCalendar/DependencyInjection/RemoteCalendarCompilerPass.php
#	src/Bundle/ChillDocGeneratorBundle/Service/Messenger/OnGenerationFails.php
#	src/Bundle/ChillJobBundle/src/Entity/Immersion.php
#	src/Bundle/ChillMainBundle/CRUD/Controller/CRUDController.php
#	src/Bundle/ChillMainBundle/DataFixtures/ORM/LoadLocationType.php
#	src/Bundle/ChillMainBundle/Entity/Location.php
#	src/Bundle/ChillMainBundle/Routing/MenuComposer.php
#	src/Bundle/ChillMainBundle/Routing/MenuTwig.php
#	src/Bundle/ChillMainBundle/Security/PasswordRecover/RecoverPasswordHelper.php
#	src/Bundle/ChillMainBundle/Serializer/Normalizer/DateNormalizer.php
#	src/Bundle/ChillMainBundle/Tests/Form/Type/ScopePickerTypeTest.php
#	src/Bundle/ChillMainBundle/Tests/Services/MenuComposerTest.php
#	src/Bundle/ChillPersonBundle/Controller/PersonController.php
#	src/Bundle/ChillPersonBundle/Entity/Person.php
#	src/Bundle/ChillPersonBundle/Form/DataMapper/PersonAltNameDataMapper.php
#	src/Bundle/ChillPersonBundle/Repository/PersonACLAwareRepository.php
#	src/Bundle/ChillPersonBundle/Serializer/Normalizer/PersonJsonNormalizer.php
#	src/Bundle/ChillPersonBundle/Tests/Serializer/Normalizer/PersonJsonNormalizerTest.php
#	src/Bundle/ChillTaskBundle/Form/SingleTaskType.php
This commit is contained in:
2025-12-22 16:36:57 +01:00
888 changed files with 64991 additions and 32076 deletions

View File

@@ -0,0 +1,42 @@
<?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\PersonBundle\Repository\Identifier;
use Chill\PersonBundle\Entity\Identifier\PersonIdentifier;
use Chill\PersonBundle\Entity\Identifier\PersonIdentifierDefinition;
use Chill\PersonBundle\PersonIdentifier\PersonIdentifierManagerInterface;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
class PersonIdentifierRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry, private readonly PersonIdentifierManagerInterface $personIdentifierManager)
{
parent::__construct($registry, PersonIdentifier::class);
}
public function findByDefinitionAndCanonical(PersonIdentifierDefinition $definition, array|string $valueOrCanonical): array
{
return $this->createQueryBuilder('p')
->where('p.definition = :definition')
->andWhere('p.canonical = :canonical')
->setParameter('definition', $definition)
->setParameter(
'canonical',
is_string($valueOrCanonical) ?
$valueOrCanonical :
$this->personIdentifierManager->buildWorkerByPersonIdentifierDefinition($definition)->canonicalizeValue($valueOrCanonical),
)
->getQuery()
->getResult();
}
}

View File

@@ -17,15 +17,25 @@ use Chill\MainBundle\Search\ParsingException;
use Chill\MainBundle\Search\SearchApiQuery;
use Chill\MainBundle\Security\Authorization\AuthorizationHelperInterface;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\PersonIdentifier\PersonIdentifierManagerInterface;
use Chill\PersonBundle\PersonIdentifier\PersonIdentifierWorker;
use Chill\PersonBundle\Security\Authorization\PersonVoter;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\NonUniqueResultException;
use Doctrine\ORM\Query;
use libphonenumber\PhoneNumber;
use libphonenumber\PhoneNumberFormat;
use Symfony\Bundle\SecurityBundle\Security;
final readonly class PersonACLAwareRepository implements PersonACLAwareRepositoryInterface
{
public function __construct(private Security $security, private EntityManagerInterface $em, private CountryRepository $countryRepository, private AuthorizationHelperInterface $authorizationHelper) {}
public function __construct(
private Security $security,
private EntityManagerInterface $em,
private CountryRepository $countryRepository,
private AuthorizationHelperInterface $authorizationHelper,
private PersonIdentifierManagerInterface $personIdentifierManager,
) {}
public function buildAuthorizedQuery(
?string $default = null,
@@ -105,6 +115,15 @@ final readonly class PersonACLAwareRepository implements PersonACLAwareRepositor
$query
->setFromClause('chill_person_person AS person');
$idDefinitionWorkers = array_map(
fn (PersonIdentifierWorker $worker) => $worker->getDefinition()->getId(),
array_filter(
$this->personIdentifierManager->getWorkers(),
fn (PersonIdentifierWorker $worker) => $worker->getDefinition()->isSearchable()
)
);
$idDefinitionWorkerQuestionMarks = implode(', ', array_fill(0, count($idDefinitionWorkers), '?'));
$pertinence = [];
$pertinenceArgs = [];
$andWhereSearchClause = [];
@@ -122,20 +141,53 @@ final readonly class PersonACLAwareRepository implements PersonACLAwareRepositor
'(starts_with(LOWER(UNACCENT(lastname)), UNACCENT(LOWER(?))))::int';
\array_push($pertinenceArgs, $str, $str, $str, $str);
$andWhereSearchClause[] =
'(LOWER(UNACCENT(?)) <<% person.fullnamecanonical OR '.
"person.fullnamecanonical LIKE '%' || LOWER(UNACCENT(?)) || '%' )";
\array_push($andWhereSearchClauseArgs, $str, $str);
$q = [
'LOWER(UNACCENT(?)) <<% person.fullnamecanonical',
"person.fullnamecanonical LIKE '%' || LOWER(UNACCENT(?)) || '%' ",
];
$qArguments = [$str, $str];
if (count($idDefinitionWorkers) > 0) {
$q[] = $mq = "EXISTS (
SELECT 1 FROM chill_person_identifier AS identifier
WHERE identifier.canonical LIKE LOWER(UNACCENT(?)) || '%' AND identifier.definition_id IN ({$idDefinitionWorkerQuestionMarks})
AND person.id = identifier.person_id
)";
$pertinence[] = "({$mq})::int * 1000000";
$qArguments = [...$qArguments, $str, ...$idDefinitionWorkers];
$pertinenceArgs = [...$pertinenceArgs, $str, ...$idDefinitionWorkers];
}
$andWhereSearchClause[] = '('.implode(' OR ', $q).')';
$andWhereSearchClauseArgs = [...$andWhereSearchClauseArgs, ...$qArguments];
}
$query->andWhereClause(
\implode(' AND ', $andWhereSearchClause),
$andWhereSearchClauseArgs
);
} else {
$pertinence = ['1'];
$pertinenceArgs = [];
}
if (null !== $phonenumber) {
$personPhoneClause = "person.phonenumber LIKE '%' || ? || '%' OR person.mobilenumber LIKE '%' || ? || '%' OR EXISTS (SELECT 1 FROM chill_person_phone where person_id = person.id AND phonenumber LIKE '%' || ? || '%')";
if (count($andWhereSearchClauseArgs) > 0) {
$initialSearchClause = '(('.\implode(' AND ', $andWhereSearchClause).') OR '.$personPhoneClause.')';
}
$andWhereSearchClauseArgs = [...$andWhereSearchClauseArgs, $phonenumber, $phonenumber, $phonenumber];
// drastically increase pertinence
$pertinence[] = "(person.phonenumber LIKE '%' || ? || '%' OR person.mobilenumber LIKE '%' || ? || '%' OR EXISTS (SELECT 1 FROM chill_person_phone where person_id = person.id AND phonenumber LIKE '%' || ? || '%'))::int * 1000000";
$pertinenceArgs = [...$pertinenceArgs, $phonenumber, $phonenumber, $phonenumber];
} else {
$initialSearchClause = \implode(' AND ', $andWhereSearchClause);
}
if (isset($initialSearchClause)) {
$query->andWhereClause(
$initialSearchClause,
$andWhereSearchClauseArgs
);
}
$query
->setSelectPertinence(\implode(' + ', $pertinence), $pertinenceArgs);
@@ -174,14 +226,6 @@ final readonly class PersonACLAwareRepository implements PersonACLAwareRepositor
);
}
if (null !== $phonenumber) {
$query->andWhereClause(
"person.phonenumber LIKE '%' || ? || '%' OR person.mobilenumber LIKE '%' || ? || '%' OR pp.phonenumber LIKE '%' || ? || '%'",
[$phonenumber, $phonenumber, $phonenumber]
);
$query->setFromClause($query->getFromClause().' LEFT JOIN chill_person_phone pp ON pp.person_id = person.id');
}
if (null !== $city) {
$query->setFromClause($query->getFromClause().' '.
'JOIN view_chill_person_current_address vcpca ON vcpca.person_id = person.id '.
@@ -298,4 +342,27 @@ final readonly class PersonACLAwareRepository implements PersonACLAwareRepositor
\array_map(static fn (Center $c) => $c->getId(), $authorizedCenters)
);
}
public function findByPhone(PhoneNumber $phoneNumber, int $start = 0, int $limit = 20): array
{
$authorizedCenters = $this->authorizationHelper
->getReachableCenters($this->security->getUser(), PersonVoter::SEE);
if ([] === $authorizedCenters) {
return [];
}
$util = \libphonenumber\PhoneNumberUtil::getInstance();
return $this->em->createQuery(
'SELECT p FROM '.Person::class.' p LEFT JOIN p.otherPhoneNumbers opn JOIN p.centerCurrent pcc '.
'WHERE (p.phonenumber LIKE :phone OR p.mobilenumber LIKE :phone OR opn.phonenumber LIKE :phone) '.
'AND pcc.center IN (:centers)'
)
->setMaxResults($limit)
->setFirstResult($start)
->setParameter('phone', $util->format($phoneNumber, PhoneNumberFormat::E164))
->setParameter('centers', $authorizedCenters)
->getResult();
}
}

View File

@@ -13,6 +13,7 @@ namespace Chill\PersonBundle\Repository;
use Chill\MainBundle\Search\SearchApiQuery;
use Chill\PersonBundle\Entity\Person;
use libphonenumber\PhoneNumber;
interface PersonACLAwareRepositoryInterface
{
@@ -60,4 +61,13 @@ interface PersonACLAwareRepositoryInterface
?string $phonenumber = null,
?string $city = null,
): array;
/**
* @return list<Person>
*/
public function findByPhone(
PhoneNumber $phoneNumber,
int $start = 0,
int $limit = 20,
): array;
}

View File

@@ -12,10 +12,12 @@ declare(strict_types=1);
namespace Chill\PersonBundle\Repository;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Entity\PersonPhone;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\QueryBuilder;
use Doctrine\Persistence\ObjectRepository;
use libphonenumber\PhoneNumber;
class PersonRepository implements ObjectRepository
{
@@ -29,6 +31,8 @@ class PersonRepository implements ObjectRepository
/**
* @throws \Doctrine\ORM\NoResultException
* @throws \Doctrine\ORM\NonUniqueResultException
*
* @deprecated
*/
public function countByPhone(
string $phonenumber,
@@ -71,6 +75,8 @@ class PersonRepository implements ObjectRepository
/**
* @throws \Exception
*
* @deprecated Use @see{self::findByPhoneNumber} or use a dedicated method in PersonACLAwareRepository
*/
public function findByPhone(
string $phonenumber,
@@ -91,6 +97,25 @@ class PersonRepository implements ObjectRepository
return $qb->getQuery()->getResult();
}
/**
* Find a person which is associated to the given phonenumber, without restrictions
* on any.
*
* @return list<Person>
*/
public function findByPhoneNumber(PhoneNumber $phoneNumber, int $firstResult = 0, int $maxResults = 50): array
{
$qb = $this->repository->createQueryBuilder('p');
$qb->select('p');
$this->searchByPhoneNumbers($qb, $phoneNumber);
$qb->setFirstResult($firstResult)
->setMaxResults($maxResults);
return $qb->getQuery()->getResult();
}
public function findOneBy(array $criteria)
{
return $this->repository->findOneBy($criteria);
@@ -109,6 +134,20 @@ class PersonRepository implements ObjectRepository
}
}
private function searchByPhoneNumbers(QueryBuilder $qb, PhoneNumber $phoneNumber): void
{
$qb->setParameter('number', $phoneNumber, 'phone_number');
$orX = $qb->expr()->orX();
$orX->add($qb->expr()->eq('p.mobilenumber', ':number'));
$orX->add($qb->expr()->eq('p.phonenumber', ':number'));
$orX->add(
$qb->expr()->exists('SELECT 1 FROM '.PersonPhone::class.' k WHERE k.phonenumber = :number AND k.person = p')
);
$qb->andWhere($orX);
}
/**
* @throws \Exception
*/