Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .env.test
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ SYMFONY_DEPRECATIONS_HELPER=999999

APP_DEBUG=true

# Here the real url is "var/test_db.sqlite", don't ask me why sqlite eats the first char
DATABASE_URL="sqlite:////var/www/html/test_db.sqlite"
# Use kernel.project_dir to make path work in both Docker and GitHub Actions
# SQLite URL format requires 4 slashes for absolute path: sqlite:////absolute/path
DATABASE_URL="sqlite:///%kernel.project_dir%/var/test_db.sqlite"
DATABASE_ENGINE="pdo_sqlite"

CIM_11_API='http://icd_11_api'
7 changes: 4 additions & 3 deletions .github/actions/create-test-db/action.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,14 @@ runs:

- name: Create test database
run: |
touch ./var/test_db.sqlite
chmod 777 ./var/test_db.sqlite
pwd
touch /home/runner/work/rpps_api/rpps_api/test_db.sqlite
chmod 777 /home/runner/work/rpps_api/rpps_api/test_db.sqlite
php bin/console doctrine:schema:update --force --env=test
php bin/console doctrine:fixtures:load --env=test
php bin/console cache:clear --env=test
shell: bash
env:
DATABASE_URL: "/var/test_db.sqlite"
DATABASE_URL: "sqlite:///%kernel.project_dir%/var/test_db.sqlite"
DATABASE_ENGINE: "pdo_sqlite"
CIM_11_API: "http://icd_11_api"
4 changes: 3 additions & 1 deletion .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: PHPUnit

env:
PHP_VERSION: 8.5
DATABASE_URL: "/var/test_db.sqlite"
DATABASE_URL: "sqlite:///%kernel.project_dir%/var/test_db.sqlite"
DATABASE_ENGINE: "pdo_sqlite"

on:
Expand Down Expand Up @@ -47,3 +47,5 @@ jobs:
env:
XDEBUG_MODE: coverage
PHP_MEMORY_LIMIT: 4096M
DATABASE_URL: ${{ env.DATABASE_URL }}
DATABASE_ENGINE: ${{ env.DATABASE_ENGINE }}
2 changes: 1 addition & 1 deletion config/packages/test/doctrine.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
doctrine:
dbal:
driver: '%env(DATABASE_ENGINE)%' # pdo_sqlite
url: '%env(DATABASE_URL)%' # ex: "sqlite:///%kernel.project_dir%/var/test_db.sqlite"
url: '%env(resolve:DATABASE_URL)%' # ex: "sqlite:///%kernel.project_dir%/var/test_db.sqlite"
charset: UTF8
default_table_options:
charset: utf8
Expand Down
33 changes: 33 additions & 0 deletions migrations/Version20250915094404.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

declare(strict_types=1);

namespace DoctrineMigrations;

use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;

/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20250915094404 extends AbstractMigration
{
public function getDescription(): string
{
return 'Add RPPS address';
}

public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE TABLE rpps_address (id CHAR(36) NOT NULL COMMENT \'(DC2Type:guid)\', rpps_id CHAR(36) NOT NULL COMMENT \'(DC2Type:guid)\', city_id CHAR(36) DEFAULT NULL COMMENT \'(DC2Type:guid)\', md5_address VARCHAR(32) NOT NULL, address VARCHAR(255) DEFAULT NULL, address_extension VARCHAR(255) DEFAULT NULL, zipcode VARCHAR(255) DEFAULT NULL, original_address LONGTEXT DEFAULT NULL, latitude DOUBLE PRECISION DEFAULT NULL, longitude DOUBLE PRECISION DEFAULT NULL, coordinates POINT NOT NULL COMMENT \'(DC2Type:point)\', created_date DATETIME NOT NULL, import_id VARCHAR(20) NOT NULL, INDEX IDX_6EC5A0EA8BAC62AF (city_id), INDEX idx_rppsaddress_rpps (rpps_id), INDEX idx_rppsaddress_md5 (md5_address), UNIQUE INDEX uniq_rppsaddress_rpps_md5 (rpps_id, md5_address), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
$this->addSql('ALTER TABLE rpps_address ADD CONSTRAINT FK_6EC5A0EAF4E1E022 FOREIGN KEY (rpps_id) REFERENCES rpps (id)');
$this->addSql('ALTER TABLE rpps_address ADD CONSTRAINT FK_6EC5A0EA8BAC62AF FOREIGN KEY (city_id) REFERENCES city (id)');
}

public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('DROP TABLE rpps_address');
}
}
2 changes: 1 addition & 1 deletion phpunit.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
</coverage>
<php>
<ini name="error_reporting" value="-1"/>
<env name="SYMFONY_DEPRECATIONS_HELPER" value="disabled" />
<server name="APP_ENV" value="test" force="true"/>
<server name="SHELL_VERBOSITY" value="-1"/>
<server name="CIM_11_API" value="http://icd_11_api"/>
Expand All @@ -20,7 +21,6 @@
<server name="DEBUG_ENCRYPTED_PASSWORD" value="$2a$12$B/4OfIMheqGSU7d8mhv94uO2EgzAeECRXqS7kdYafzNn2W47G64HK"/>
<server name="SYMFONY_PHPUNIT_VERSION" value="7.5"/>
<server name="KERNEL_CLASS" value="App\Kernel"/>
<env name="SYMFONY_DEPRECATIONS_HELPER" value="disabled" />
</php>
<testsuites>
<testsuite name="Project Test Suite">
Expand Down
113 changes: 74 additions & 39 deletions src/ApiPlatform/Filter/RPPSFilter.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
use ApiPlatform\Metadata\Operation;
use App\Entity\City;
use App\Entity\Specialty;
use Doctrine\DBAL\Platforms\MySQLPlatform;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Query\Expr\Join;
use Doctrine\ORM\QueryBuilder;
use Doctrine\Persistence\ManagerRegistry;
use Exception;
Expand Down Expand Up @@ -94,21 +94,31 @@ protected function addCityFilter(QueryBuilder $queryBuilder, ?string $value): vo
/** @var City|null $city */
$city = $this->em->getRepository(City::class)->findOneBy(['canonical' => $value]);

// if city not found, force an empty result set
if (!$city) {
$queryBuilder->andWhere('1 = 2');

return;
}

$rootAlias = $queryBuilder->getRootAliases()[0];

// Use RPPSAddress -> City relation instead of legacy RPPS.cityEntity
// Ensure we don't duplicate RPPS rows if multiple addresses match
$queryBuilder->distinct();
$queryBuilder
->innerJoin("$rootAlias.addresses", 'addr')
->innerJoin('addr.city', 'city');

if ($city->getSubCities()->toArray()) {
$queryBuilder->innerJoin("$rootAlias.cityEntity", 'city', Join::WITH, 'city.canonical IN (:cityId)');
$queryBuilder->setParameter('cityId', [
$queryBuilder->andWhere('city.canonical IN (:cityCanonicalList)');
$queryBuilder->setParameter('cityCanonicalList', [
$value,
...array_map(fn (City $city) => $city->getCanonical(), $city->getSubCities()->toArray()),
...array_map(static fn (City $c) => $c->getCanonical(), $city->getSubCities()->toArray()),
]);
} else {
$queryBuilder->innerJoin("$rootAlias.cityEntity", 'city', Join::WITH, 'city.canonical = :cityId');
$queryBuilder->setParameter('cityId', $value);
$queryBuilder->andWhere('city.canonical = :cityCanonical');
$queryBuilder->setParameter('cityCanonical', $value);
}
}

Expand Down Expand Up @@ -161,14 +171,17 @@ protected function addSearchFilter(QueryBuilder $queryBuilder, ?string $value):
$value = $this->cleanValue($value);

if (str_contains($value, '%')) {
$result = $this->em->getConnection()->fetchFirstColumn('(SELECT id FROM rpps WHERE full_name LIKE :search
$result = $this->em->getConnection()->fetchFirstColumn(
'(SELECT id FROM rpps WHERE full_name LIKE :search
LIMIT 500)
UNION
(SELECT id FROM rpps WHERE full_name_inversed LIKE :search
LIMIT 500)
LIMIT 500;', [
'search' => "$value%",
]);
LIMIT 500;',
[
'search' => "$value%",
]
);

$queryBuilder->andWhere("$alias.id IN (:result)");
$queryBuilder->setParameter('result', $result);
Expand Down Expand Up @@ -200,16 +213,19 @@ protected function addExcludedRppsFilter(QueryBuilder $queryBuilder, mixed $excl
return $queryBuilder;
}

public function addLatitudeFilter(QueryBuilder $queryBuilder, ?string $latitude, ?Operation &$operation): QueryBuilder
{
public function addLatitudeFilter(
QueryBuilder $queryBuilder,
?string $latitude,
?Operation &$operation,
): QueryBuilder {
$request = $this->requestStack->getCurrentRequest();
$longitude = $request?->query->get('longitude');

if (!$latitude || !$longitude) {
return $queryBuilder;
}
$operation = $operation->withPaginationClientEnabled(false);
$operation = $operation->withPaginationClientPartial(true);
$operation = $operation?->withPaginationClientEnabled(false);
$operation = $operation?->withPaginationClientPartial(true);

$request->attributes->set('_api_operation', $operation);

Expand All @@ -229,32 +245,51 @@ public function addLatitudeFilter(QueryBuilder $queryBuilder, ?string $latitude,
$minLng = (float) $longitude - (float) $lngOffset;
$maxLng = (float) $longitude + (float) $lngOffset;

// Add bounding box condition using the POINT function
$queryBuilder->andWhere(
'MBRContains(ST_MakeEnvelope(POINT(:minLng, :minLat), POINT(:maxLng, :maxLat)), ' . $rootAlias . '.coordinates) = true'
);

// Apply the more accurate distance filter
$queryBuilder->andWhere(
"ST_Distance_Sphere(POINT(:longitude, :latitude), $rootAlias.coordinates) < :distance"
)
->addSelect(
"ST_Distance_Sphere(POINT(:longitude, :latitude), $rootAlias.coordinates) AS HIDDEN distance"
$queryBuilder->distinct();

// Join RPPS -> RPPSAddress for coordinates
$queryBuilder->innerJoin($rootAlias . '.addresses', 'addr');

$platform = $this->em->getConnection()->getDatabasePlatform();

if ($platform instanceof MySQLPlatform) {
// TODO NOT TESTED !
// MySQL path: use POINT/MBRContains/ST_Distance_Sphere on RPPSAddress.coordinates
$queryBuilder->andWhere(
'MBRContains(ST_MakeEnvelope(POINT(:minLng, :minLat), POINT(:maxLng, :maxLat)), addr.coordinates) = 1'
);

// Set parameters
$queryBuilder->setParameter('latitude', (float) $latitude);
$queryBuilder->setParameter('longitude', (float) $longitude);
$queryBuilder->setParameter('distance', (float) $distance);
$queryBuilder->setParameter('minLat', $minLat);
$queryBuilder->setParameter('maxLat', $maxLat);
$queryBuilder->setParameter('minLng', $minLng);
$queryBuilder->setParameter('maxLng', $maxLng);
$queryBuilder
->andWhere('ST_Distance_Sphere(POINT(:longitude, :latitude), addr.coordinates) < :distance')
->addSelect('ST_Distance_Sphere(POINT(:longitude, :latitude), addr.coordinates) AS HIDDEN distance');

$queryBuilder->setParameter('latitude', (float) $latitude);
$queryBuilder->setParameter('longitude', (float) $longitude);
$queryBuilder->setParameter('distance', (float) $distance);
$queryBuilder->setParameter('minLat', $minLat);
$queryBuilder->setParameter('maxLat', $maxLat);
$queryBuilder->setParameter('minLng', $minLng);
$queryBuilder->setParameter('maxLng', $maxLng);
} else {
$queryBuilder
->andWhere('addr.latitude IS NOT NULL')
->andWhere('addr.longitude IS NOT NULL')
->andWhere(
'(addr.latitude BETWEEN :minLat AND :maxLat AND addr.longitude BETWEEN :minLng AND :maxLng)
OR (ABS(addr.latitude - :latExact) < 1e-5 AND ABS(addr.longitude - :lngExact) < 1e-5)'
);

// Set only the parameters used by this branch
$queryBuilder->setParameter('minLat', $minLat);
$queryBuilder->setParameter('maxLat', $maxLat);
$queryBuilder->setParameter('minLng', $minLng);
$queryBuilder->setParameter('maxLng', $maxLng);
$queryBuilder->setParameter('latExact', (float) $latitude);
$queryBuilder->setParameter('lngExact', (float) $longitude);
}

// $queryBuilder->orderBy(
// 'distance',
// 'ASC'
// );
// Keep ordering optional
// $queryBuilder->addOrderBy('distance', 'ASC');

return $queryBuilder;
}
Expand Down Expand Up @@ -284,7 +319,7 @@ public static function parseBooleanValue(string $string): ?bool

// If true or 1, returns true
// if false or 0 returns false
// Else, incorrect value : returns null
// Else, incorrect value: returns null
return in_array($string, ['1', 'true']) ? true : (in_array($string, ['0', 'false']) ? false : null);
}

Expand Down Expand Up @@ -326,7 +361,7 @@ public function getDescription(string $resourceClass): array
'type' => 'array',
'required' => false,
'swagger' => [
'description' => 'Exclude specific RPPS numbers from the result set. Provide one or more RPPS numbers.',
'description' => 'Exclude given RPPS numbers from the result. Provide one or more RPPS numbers',
'type' => 'array',
'items' => [
'type' => 'string',
Expand Down
Loading