<?php

declare(strict_types=1);

namespace Database;

/**
 * Classe permettant de retourner une instance unique et configurée de PDO.
 *
 * Ceci permet de ne pas multiplier les connexions au serveur de base de données.
 * L'instance peut être configurée de trois façons, utilisées dans cet ordre jusqu'à obtenir une configuration valide :
 *  - programmatique ; MyPDO::setConfiguration(DSN, username, password)
 *  - variables d'environnement ; MYPDO_DSN, MYPDO_USERNAME et MYPDO_PASSWORD
 *  - fichier ; [APP_DIR/].mypdo[.MYPDO_ENV].ini où APP_DIR et MYPDO_ENV sont des variables d'environnement
 *
 * @startuml
 *
 *  namespace Database {
 *      class MyPdo {
 *          - {static} dsn : string
 *          - {static} username : string := ''
 *          - {static} password : string := ''
 *          - {static} options : array := []
 *
 *          - __construct(\n\tdsn : string,\n\tusername : string := null,\n\tpassword : string := null,\n\toptions : array := null)
 *          - private __clone() : void
 *          + {static} getInstance() : MyPdo
 *          + {static} setConfiguration(\n\tdsn : string,\n\tusername : string := '',\n\tpassword : string := '',\n\toptions : array := []) : void
 *          - {static} hasConfiguration() : bool
 *          - {static} setConfigurationFromEnvironmentVariables() : bool
 *          - {static} setConfigurationFromIniFile() : bool
 *      }
 *  }
 *
 *  Database\\MyPdo -left-|> PDO
 *  Database\\MyPdo "1" *-- "1\n-<u>myPdoInstance</u>" Database\\MyPdo : contains
 *
 * @enduml
 */
final class MyPdo extends \PDO {
    /**
     * Instance unique de PDO.
     */
    private static self $myPdoInstance;

    /**
     *  DSN pour la connexion BD.
     */
    private static string $dsn;

    /**
     * Nom d'utilisateur pour la connexion BD.
     */
    private static string $username = '';

    /**
     * Mot de passe pour la connexion BD.
     */
    private static string $password = '';

    /**
     * Options du pilote BD.
     */
    private static array $options = [
        \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
        \PDO::ATTR_DEFAULT_FETCH_MODE => \PDO::FETCH_ASSOC,
    ];

    /**
     * Constructeur privé.
     *
     * Seule la classe MyPDO peut construire une instance de MyPDO.
     *
     * @param string      $dsn      DSN pour la connexion BD
     * @param string|null $username Utilisateur pour la connexion BD
     * @param string|null $password Mot de passe pour la connexion BD
     * @param array|null  $options  Options du pilote BD
     */
    private function __construct(string $dsn, string $username = null, string $password = null, array $options = null)
    {
        parent::__construct($dsn, $username, $password, $options);
        // La base de données est-elle de type SQLite
        if ('sqlite' === $this->getAttribute(\PDO::ATTR_DRIVER_NAME)) {
            // Activer les clés étrangères qui sont désactivées par défaut
            $this->exec('PRAGMA foreign_keys = ON');
        }
    }

    /**
     * Empêcher le clonage, le singleton doit rester unique.
     */
    private function __clone(): void
    {
    }

    /**
     * Point d'accès à l'instance unique.
     *
     * L'instance est créée au premier appel et réutilisée aux appels suivants.
     *
     * @return self Instance unique de MyPdo
     *
     * @throws \PDOException Si la configuration n'a pas été effectuée
     */
    public static function getInstance(): self
    {
        // Instance de la classe présente ?
        if (!isset(self::$myPdoInstance)) {
            // Configuration effectuée ?
            if (!self::hasConfiguration()
                && !self::setConfigurationFromEnvironmentVariables()
                && !self::setConfigurationFromIniFile()) {
                throw new \PDOException(__CLASS__.': Configuration not set');
            }
            // Construire une instance
            self::$myPdoInstance = new self(self::$dsn, self::$username, self::$password, self::$options);
        }

        return self::$myPdoInstance;
    }

    /**
     * Fixer programmatiquement la configuration de la connexion à la BD.
     *
     * @param string $dsn      DSN pour la connexion BD
     * @param string $username Utilisateur pour la connexion BD
     * @param string $password Mot de passe pour la connexion BD
     * @param array  $options  Options du pilote BD
     *
     * @throws \PDOException Si la variable d'environnement APP_DIR est utilisée, mais n'est pas définie
     */
    public static function setConfiguration(
        string $dsn,
        string $username = '',
        string $password = '',
        array $options = []
    ): void {
        self::$dsn = $dsn;
        self::$username = $username;
        self::$password = $password;
        self::$options = $options + self::$options;

        // Remplacer %APP_DIR% par le chemin de l'application si SQLite est utilisé
        if (preg_match('/^(.*)(%APP_DIR%)(.*)$/', $dsn, $matches)) {
            if (!($appDir = getenv('APP_DIR'))) {
                throw new \PDOException(__CLASS__.': APP_DIR environment variable not set');
            }
            self::$dsn = $matches[1].$appDir.$matches[3];
        }
    }

    /**
     * Vérifier si la configuration de la connexion à la BD a été effectuée.
     */
    private static function hasConfiguration(): bool
    {
        return isset(self::$dsn);
    }

    /**
     * Lire la configuration depuis des variables d'environnement.
     *
     * Les variables sont :
     *  - MYPDO_DSN pour le DSN
     *  - MYPDO_USERNAME pour le nom d'utilisateur
     *  - MYPDO_PASSWORD pour le mot de passe.
     *
     * @return bool Vrai si la configuration a été trouvée
     *
     * @throws \PDOException Si self::setConfiguration() échoue
     */
    private static function setConfigurationFromEnvironmentVariables(): bool
    {
        // DSN ?
        $dsn = getenv('MYPDO_DSN', true);
        if (false !== $dsn) {
            // username et password facultatifs
            $username = getenv('MYPDO_USERNAME', true) ?: '';
            $password = getenv('MYPDO_PASSWORD', true) ?: '';
            self::setConfiguration($dsn, $username, $password);

            return true;
        }

        return false;
    }

    /**
     * Lire la configuration depuis un fichier ini.
     *
     * Le nom du fichier peut être :
     *  - ".mypdo.ini"
     *  - ".mypdo<.environment_name>.ini" (environment_name dans la variable d'environnement MYPDO_ENV)
     * Le fichier peut être placé :
     *  - dans un répertoire accessible (https://www.php.net/manual/fr/ini.core.php#ini.include-path)
     *  - dans le répertoire défini par la variable d'environnement APP_DIR
     * Le fichier contient :
     * [mypdo]
     * dsn = ...
     * username = ...
     * password = ...
     *
     * @return bool Vrai si la configuration a été trouvée
     *
     * @throws \PDOException Si le fichier des paramètres est invalide
     */
    private static function setConfigurationFromIniFile(): bool
    {
        // Environnement MyPdo défini ?
        $myPdoEnv = getenv('MYPDO_ENV', true) ?: '';
        // Chemin du fichier en fonction de APP_DIR
        $appDir = getenv('APP_DIR');
        $directory = false !== $appDir ? $appDir.DIRECTORY_SEPARATOR : '';
        $parameterFile = sprintf('%s.mypdo%s.ini', $directory, $myPdoEnv ? ".$myPdoEnv" : '');
        // Lecture du fichier de configuration
        $parameters = @parse_ini_file($parameterFile, true);
        if (false !== $parameters) {
            if (!isset($parameters['mypdo'])) {
                throw new \PDOException('`mypdo` section not found in `'.basename($parameterFile).'`');
            }
            if (!isset($parameters['mypdo']['dsn'])) {
                throw new \PDOException('`dsn` not found in `'.basename($parameterFile).'`');
            }
            $dsn = $parameters['mypdo']['dsn'];
            // username et password facultatifs
            $username = $parameters['mypdo']['username'] ?? '';
            $password = $parameters['mypdo']['password'] ?? '';
            self::setConfiguration($dsn, $username, $password);

            return true;
        }

        return false;
    }
}

/* Exemple de configuration et d'utilisation

use Database\MyPdo;

MyPDO::setConfiguration('mysql:host=mysql;dbname=cutron01_music;charset=utf8', 'web', 'web');

$stmt = MyPDO::getInstance()->prepare(
    <<<'SQL'
    SELECT id, name
    FROM artist
    ORDER BY name
SQL
);

$stmt->execute();

while (($ligne = $stmt->fetch()) !== false) {
    echo "<p>{$ligne['name']}\n";
}
*/