Select Git revision
Dockerfile.php7.dev
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
DbService.php 8.16 KiB
<?php
namespace UnicaenDbAnonym\Service;
use Doctrine\DBAL\ConnectionException;
use Doctrine\DBAL\Exception;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\Mapping\ClassMetadata;
use Laminas\Log\Logger;
use Laminas\Log\Writer\Stream;
use RuntimeException;
use Throwable;
use UnicaenSql\Service\SQL\RunSQLProcess;
use UnicaenSql\Service\SQL\RunSQLResult;
use Webmozart\Assert\Assert;
class DbService
{
use GeneratorServiceAwareTrait;
const METADATA_KEY_ANONYMISEE = 'BDD_ANONYMISEE';
protected EntityManager $entityManager;
protected AbstractPlatform $databasePlatform;
protected string $updateSQLTemplate = 'update %s set %s where id = %s ;';
protected array $processedEntities = [];
/**
* @param \Doctrine\ORM\EntityManager $entityManager
* @throws \Doctrine\DBAL\Exception
*/
public function setEntityManager(EntityManager $entityManager): void
{
$this->entityManager = $entityManager;
$this->databasePlatform = $this->entityManager->getConnection()->getDatabasePlatform();
}
public function isBddAnonymisee(): bool
{
$sqlTemplate = "select value from unicaen_db_anonym where key = '%s'";
$sql = sprintf($sqlTemplate, self::METADATA_KEY_ANONYMISEE);
try {
/** @var string|false $result */
$result = $this->entityManager->getConnection()->executeQuery($sql)->fetchOne();
} catch (Throwable $e) {
throw new RuntimeException(
"Erreur lors de l'interrogation du témoin d'anonymisation dans la table des metadata", null, $e);
}
if ($result === false) {
throw new RuntimeException(sprintf(
"Le témoin d'anonymisation '%s' est introuvable dans la table des metadata",
self::METADATA_KEY_ANONYMISEE
));
}
return $result === '1';
}
public function setBddAnonymisee(bool $bddAnonymisee): void
{
$sqlTemplate = "update unicaen_db_anonym set value = '%d' where key = '%s'";
$sql = sprintf($sqlTemplate, $bddAnonymisee ? '1' : '0', self::METADATA_KEY_ANONYMISEE);
try {
$this->entityManager->getConnection()->executeQuery($sql);
} catch (Exception $e) {
throw new RuntimeException(
"Erreur lors de la modification du témoin d'anonymisation dans la table des metadata", null, $e);
}
}
public function fetchEntityRecords(string $entityName, array $fields, array $except): array
{
$qb = $this->entityManager->createQueryBuilder()
->select("partial t.{id," . implode(',', $fields) . "}")
->from($entityName, 't');
foreach ($except as $field => $values) {
$qb->andWhere($qb->expr()->notIn('t.' . $field, (array)$values));
}
return $qb->getQuery()->getArrayResult();
}
public function getEntityClassMetadata(string $entityName): ClassMetadata
{
return $this->entityManager->getClassMetadata($entityName);
}
public function normalizeMapping(string $entityName, array $mapping): array
{
$metadata = $this->getEntityClassMetadata($entityName);
// on s'assure d'avoir des noms d'attributs Doctrine comme clés
$fields = array_map(
fn($name) => $metadata->getFieldName($name),
array_keys($mapping)
);
return array_combine($fields, $mapping);
}
/**
* @param string $entityName
* @param array $records
* @param array $mapping Exemple :
* ```
* [
* 'nomUsuel' => 'lastName', // càd `$faker->lastName()`
* 'nomPatronymique' => 'null', // càd mise à NULL.
* 'leitmotiv' => ['words', 3, true], // càd `$faker->words(3, true)`
* 'leitmotivUnique' => [
* 'name' => 'words',
* 'unique' => true,
* 'params' => [3, true],
* ], // càd `$faker->unique()->words(3, true)`
* 'civilite' => [
* 'name' => 'randomElement',
* 'params' => [Individu::CIVILITE_MME, Individu::CIVILITE_M],
* ], // càd `$faker->randomElement([Individu::CIVILITE_MME, Individu::CIVILITE_M])`
* 'prenom1' => [
* 'name' => 'firstName',
* 'params' => [null],
* 'context' => true,
* ] // càd `$faker->firstName(null, $context)` // Cf. {@see \UnicaenDbAnonym\Provider\fr_FR\PersonExtension}
* ]
* ```
* @return array[]
*/
public function generateSqlLines(string $entityName, array $records, array $mapping): array
{
$metadata = $this->getEntityClassMetadata($entityName);
// Reset du modificateur unique() à chaque nouvelle entité rencontrée (cf. http://fakerphp.org/#modifiers),
// sachant que l'utilisation de unique() dépend de ce qui est spécifié dans la config.
if (!in_array($entityName, $this->processedEntities)) {
$this->generatorService->resetUniqueModifier();
$this->processedEntities[] = $entityName;
}
$migrateSqlLines = [];
$restoreSqlLines = [];
foreach ($records as $record) {
// on n'anonymise pas les colonnes dont la valeur est null
$preparedRecord = array_filter($record, fn($v) => $v !== null);
$preparedMapping = array_intersect_key($mapping, $preparedRecord);
if (count($preparedMapping) === 0) {
continue; // toutes les valeurs sont null, next !
}
$migrateSqlLines[] = $this->genUpdateForAnonymiser($preparedRecord, $preparedMapping, $metadata);
$restoreSqlLines[] = $this->genUpdateForRestaurer($preparedRecord, $preparedMapping, $metadata);
}
return [$migrateSqlLines, $restoreSqlLines];
}
private function genUpdateForAnonymiser(array $record, array $mapping, ClassMetadata $metadata): ?string
{
$entity = $metadata->getName();
$sets = [];
$context = [];
$this->generatorService->seed($record['id']);
foreach ($mapping as $name => $fakerConfig) {
$fakeValue = $this->generatorService->generateFakeValue((array)$fakerConfig, $context);
Assert::scalar($fakeValue, "Le mapping spécifiée pour l'attribut '$name' de l'entité '$entity' produit une valeur non supportée : %s");
$sets[] = $metadata->getColumnName($name) . ' = ' . $this->databasePlatform->quoteStringLiteral($fakeValue);
$context[$name] = $fakeValue; // contexte = valeurs générées jusqu'à présent
}
return sprintf($this->updateSQLTemplate,
$metadata->getTableName(),
implode(', ', $sets),
$record['id']
);
}
private function genUpdateForRestaurer(array $record, array $mapping, ClassMetadata $metadata): ?string
{
Assert::notEmpty($mapping, "Mapping vide !");
$sets = [];
foreach ($mapping as $fieldName => $fakerConfig) {
$value = $this->databasePlatform->quoteStringLiteral($record[$fieldName]);
$columnName = $metadata->getColumnName($fieldName);
$sets[] = "$columnName = $value";
}
return sprintf($this->updateSQLTemplate,
$metadata->getTableName(),
implode(', ', $sets),
$record['id']
);
}
/**
* @throws \Doctrine\DBAL\ConnectionException
* @throws \Doctrine\DBAL\Exception
*/
public function lancerScript(string $scriptPath): RunSQLResult
{
Assert::readable($scriptPath, "Le script '$scriptPath' n'existe pas ou n'est pas lisible");
$conn = $this->entityManager->getConnection();
$conn->beginTransaction();
$p = new RunSQLProcess();
$p->setConnection($conn);
$p->setScriptPath($scriptPath);
$p->setQueriesSplitPattern("#;\n#m"); // point virgule puis un retour
$p->setLogFilePath($scriptPath . '.log');
$p->setLogger((new Logger())->addWriter(new Stream('php://stdout')));
$result = $p->executeScript();
try {
$conn->commit();
} catch (ConnectionException $e) {
$conn->rollBack();
throw $e;
}
return $result;
}
}