Commit aa1d794d authored by Antony Le Courtes's avatar Antony Le Courtes
Browse files

select() passe par la connexion Doctrine partagée quand elle est définie

Quand une connexion Doctrine est injectée, select() l'utilise désormais,
comme exec() et les transactions. Les lectures se font donc dans la même
transaction et voient les données pas encore commitées + normalisation des paramètres bool/date
parent d106c54e
Loading
Loading
Loading
Loading
Loading
+109 −34
Changes for src/Bdd.php: 109 added lines, 34 removed lines.
Original line number Diff line number Diff line
@@ -3,7 +3,7 @@
namespace Unicaen\BddAdmin;

use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Types\Types;
use Doctrine\DBAL\ParameterType;
use Unicaen\BddAdmin\Anonymisation\AnonymisationManager;
use Unicaen\BddAdmin\Data\DataManager;
use Unicaen\BddAdmin\Ddl\Ddl;
@@ -350,39 +350,8 @@ class Bdd
    public function exec(string $sql, array $params = [], array $types = []): self
    {
        if ($this->doctrineConnection) {
            foreach ($types as $ti => $tv) {
                switch ($tv) {
                    case self::TYPE_INT:
                        $types[$ti] = Types::INTEGER;
                        break;
                    case self::TYPE_BOOL:
                        $types[$ti] = Types::BOOLEAN;
                        if (isset($params[$ti])) {
                            if (false === $params[$ti]) {
                                $params[$ti] = 0;
                            } elseif (true === $params[$ti]) {
                                $params[$ti] = 1;
                            }
                        }
                        break;
                    case self::TYPE_FLOAT:
                        $types[$ti] = Types::FLOAT;
                        break;
                    case self::TYPE_STRING:
                        $types[$ti] = Types::STRING;
                        break;
                    case self::TYPE_DATE:
                        $types[$ti] = Types::DATETIME_IMMUTABLE;
                        break;
                    case self::TYPE_BLOB:
                        $types[$ti] = Types::BLOB;
                        break;
                    case self::TYPE_CLOB:
                        $types[$ti] = Types::TEXT;
                        break;
                }
            }
            $this->doctrineConnection->executeStatement($sql, $params);
            [$sql, $params, $doctrineTypes] = $this->doctrinePrepareParams($sql, $params, $types);
            $this->doctrineConnection->executeStatement($sql, $params, $doctrineTypes);
        } else {
            $this->driver->exec($sql, $params, $types);
        }
@@ -407,6 +376,11 @@ class Bdd

    public function select(string $sql, array $params = [], array $options = []): array|null|SelectParser
    {
        // Connexion Doctrine partagée (ex. transaction applicative en cours) :
        // on lit par elle pour voir les données non encore commitées, comme le fait déjà exec()
        if ($this->doctrineConnection) {
            return $this->doctrineSelect($sql, $params, $options);
        }
        $result = $this->driver->select($sql, $params, $options);
        if (false === $result) {
            return null;
@@ -414,6 +388,107 @@ class Bdd
        return $result;
    }

    private function doctrineSelect(string $sql, array $params, array $options): array|null|SelectParser
    {
        $options = array_merge([
                                   'fetch' => self::FETCH_ALL,
                                   'case'  => -1,
                                   'types' => [],
                               ], $options);

        // ici $options['types'] décrit les colonnes du résultat, pas les paramètres
        [$sql, $params, $doctrineTypes] = $this->doctrinePrepareParams($sql, $params);
        $result = $this->doctrineConnection->executeQuery($sql, $params, $doctrineTypes);

        switch ($options['fetch']) {
            case self::FETCH_ONE:
                $row = $result->fetchAssociative();
                $result->free();

                return false === $row ? null : $this->doctrineConvertRow($row, $options);

            case self::FETCH_EACH:
                return new DoctrineSelectParser($result, fn(array $row) => $this->doctrineConvertRow($row, $options));

            default: // FETCH_ALL
                $rows = $result->fetchAllAssociative();
                foreach ($rows as $i => $row) {
                    $rows[$i] = $this->doctrineConvertRow($row, $options);
                }

                return $rows;
        }
    }

    /**
     * Normalise les paramètres avant envoi par la connexion Doctrine, comme le font les drivers natifs :
     * - booléens => 1/0
     * - dates => 'Y-m-d H:i:s' (encadré par to_date() sous Oracle)
     * et traduit les types BddAdmin en ParameterType Doctrine (seuls INT et BLOB changent le binding).

     * @return array{0: string, 1: array, 2: array}
     */
    private function doctrinePrepareParams(string $sql, array $params, array $types = []): array
    {
        $isOracle      = $this->driver instanceof Driver\Oracle\Driver;
        $doctrineTypes = [];

        foreach ($params as $name => $val) {
            if (is_bool($val)) {
                $params[$name] = $val ? 1 : 0;
            } elseif ($val instanceof \DateTimeInterface) {
                $params[$name] = $val->format('Y-m-d H:i:s');
                if ($isOracle && is_string($name)) {
                    $sql = preg_replace(
                        '/:' . preg_quote($name, '/') . '\b/',
                        "to_date(:$name, 'YYYY-MM-DD HH24:MI:SS')",
                        $sql
                    );
                }
            }

            switch ($types[$name] ?? null) {
                case self::TYPE_INT:
                    $doctrineTypes[$name] = ParameterType::INTEGER;
                    break;
                case self::TYPE_BLOB:
                    $doctrineTypes[$name] = ParameterType::LARGE_OBJECT;
                    break;
            }
        }

        return [$sql, $params, $doctrineTypes];
    }



    /**
     * Même post-traitement que les drivers natifs : casse des colonnes + conversion selon 'types'
     */
    private function doctrineConvertRow(array $row, array $options): array
    {
        if (CASE_LOWER === $options['case'] || CASE_UPPER === $options['case']) {
            $row = array_change_key_case($row, $options['case']);
        }

        foreach ($row as $col => $value) {
            $type = $options['types'][$col] ?? null;
            if (null === $value || null === $type) {
                continue;
            }
            $row[$col] = match ($type) {
                self::TYPE_INT   => (int)$value,
                self::TYPE_BOOL  => (bool)$value,
                self::TYPE_FLOAT => (float)$value,
                self::TYPE_STRING, self::TYPE_CLOB, self::TYPE_BLOB => (string)$value,
                self::TYPE_DATE  => \DateTime::createFromFormat('Y-m-d H:i:s', $value) ?: $value,
                default          => $value,
            };
        }

        return $row;
    }



    public function selectSingle(string $sql, array $params = [], array $options = []): mixed
+36 −0
Changes for src/DoctrineSelectParser.php: 36 added lines, 0 removed lines.
Original line number Diff line number Diff line
<?php

namespace Unicaen\BddAdmin;

use Doctrine\DBAL\Result;

/**
 * Parcours ligne à ligne d'un résultat lu via la connexion Doctrine partagée
 */
class DoctrineSelectParser extends SelectParser
{
    /** @var \Closure(array): array */
    private \Closure $convert;



    public function __construct(private readonly Result $result, \Closure $convert)
    {
        // pas d'appel au constructeur parent : ni driver ni statement natif ici
        $this->convert = $convert;
    }



    public function next(): ?array
    {
        $row = $this->result->fetchAssociative();
        if (false === $row) {
            $this->result->free();

            return null;
        }

        return ($this->convert)($row);
    }
}
 No newline at end of file