dadajuice/zephyrus

View on GitHub
src/Zephyrus/Database/Core/Database.php

Summary

Maintainability
A
30 mins
Test Coverage
A
100%
<?php namespace Zephyrus\Database\Core;

use PDO;
use PDOException;
use Zephyrus\Exceptions\DatabaseException;
use Zephyrus\Exceptions\FatalDatabaseException;

class Database
{
    private DatabaseConfiguration $configuration;
    private DatabaseHandle $handle;
    private SchemaInterrogator $schemaInterrogator;
    private array $typeConversions = [];

    /**
     * Instantiates the database facade instance which will permit queries to be sent to the database.
     *
     * @param array $configurations
     * @throws FatalDatabaseException
     */
    public function __construct(array $configurations)
    {
        $this->configuration = new DatabaseConfiguration($configurations);
        $this->handle = $this->connect();
        $this->schemaInterrogator = new SchemaInterrogator($this);
    }

    /**
     * Executes a parametrized SQL query. Parameters must be included as an array compatible with the PDO query
     * preparation.
     *
     * @param string $query
     * @param array $parameters
     * @throws DatabaseException
     * @return DatabaseStatement
     */
    public function query(string $query, array $parameters = []): DatabaseStatement
    {
        try {
            $statement = $this->handle->prepare($query, [PDO::ATTR_CURSOR => PDO::CURSOR_FWDONLY]);
            foreach ($parameters as $name => &$variable) {
                $statement->bindParam(
                    is_string($name) ? ":$name" : intval($name) + 1,
                    $variable,
                    $this->evaluatePdoType($variable)
                );
            }
            $statement->execute();
            return new DatabaseStatement($statement, $this->typeConversions);
        } catch (PDOException $e) {
            throw new DatabaseException('Error while preparing query « ' . $query . ' » (' .
                $e->getMessage() . ')', $query);
        }
    }

    /**
     * Registers a custom type conversion for a project. Useful for automated composite type processing.
     *
     * @param string $pdoType
     * @param callable $callback
     */
    public function registerTypeConversion(string $pdoType, callable $callback): void
    {
        $this->typeConversions[strtoupper($pdoType)] = $callback;
    }

    /**
     * Retrieves the configured database source currently used by the instance.
     *
     * @return DatabaseConfiguration
     */
    final public function getConfiguration(): DatabaseConfiguration
    {
        return $this->configuration;
    }

    /**
     * Proceeds to include the given variable into the database environnement so that the executed queries, triggers or
     * stored procedures could have access to the variable. Useful for example to pass a user id to register for
     * automated log triggers.
     *
     * @param string $name
     * @param string $value
     */
    public function addSessionVariable(string $name, string $value)
    {
        $this->query("set session \"$name\" = '$value';");
    }

    /**
     * Retrieves the interrogator instance to perform various meta database (schema) queries.
     *
     * @return SchemaInterrogator
     */
    public function getSchemaInterrogator(): SchemaInterrogator
    {
        return $this->schemaInterrogator;
    }

    /**
     * Retrieves the wrapped native PDO instance used for database interaction.
     *
     * @return DatabaseHandle
     */
    public function getHandle(): DatabaseHandle
    {
        return $this->handle;
    }

    /**
     * Disable auto-commit mode and begin an SQL transaction. When this method is called, database will only be updated
     * when calling the <commit> method. Calling the <rollback> method will undo any SQL commands done within the
     * started transaction.
     *
     * @see self::commit()
     * @see self::rollback()
     */
    public function beginTransaction()
    {
        $this->handle->beginTransaction();
    }

    /**
     * Manually commit a started SQL transaction. After a successful commit, the connection handler will return
     * auto-commit mode.
     *
     * @throws FatalDatabaseException
     */
    public function commit()
    {
        try {
            $this->handle->commit();
        } catch (PDOException $e) {
            throw FatalDatabaseException::transactionCommitFailed($e->getMessage());
        }
    }

    /**
     * Cancels any SQL commands done within a started SQL transaction. After a successful rollback, the connection
     * handler will return auto-commit mode.
     *
     * @throws FatalDatabaseException
     */
    public function rollback()
    {
        try {
            $this->handle->rollBack();
        } catch (PDOException $e) {
            throw FatalDatabaseException::transactionRollbackFailed($e->getMessage());
        }
    }

    /**
     * Finds the last inserted id for the specified sequence name.
     *
     * @param string $sequenceName
     * @return string
     */
    public function getLastInsertedId(string $sequenceName): string
    {
        return $this->handle->lastInsertId($sequenceName);
    }

    public function getVersion(): string
    {
        $statement = $this->query("SHOW server_version");
        $row = $statement->next();
        return $row->server_version;
    }

    public function getSize(): int
    {
        $statement = $this->query("SELECT pg_database_size('" . $this->configuration->getDatabaseName() . "') as size");
        $row = $statement->next();
        return $row->size;
    }

    /**
     * Creates the PDO handle to allow for query to be executed to the configured database source. Will throw
     * a FatalDatabaseException when connection fails.
     *
     * @throws FatalDatabaseException
     * @return DatabaseHandle
     */
    private function connect(): DatabaseHandle
    {
        try {
            return new DatabaseHandle(
                $this->configuration->getDatabaseSourceName(),
                $this->configuration->getUsername(),
                $this->configuration->getPassword()
            );
        } catch (PDOException $e) {
            throw FatalDatabaseException::connectionFailed($e->getMessage());
        }
    }

    /**
     * Guesses the best PDO::PARAM_x type constant for a given variable. Ignored from coverage because test Database
     * sqlite doesn't have proper BOOL or NULL.
     *
     * @codeCoverageIgnore
     * @param mixed $variable
     * @return int
     */
    private function evaluatePdoType(mixed $variable): int
    {
        if (is_float($variable)) {
            // PDO doesn't have PARAM_FLOAT, so it must be evaluated as STR
            return PDO::PARAM_STR;
        } elseif (is_int($variable)) {
            return PDO::PARAM_INT;
        } elseif (is_bool($variable)) {
            return PDO::PARAM_BOOL;
        } elseif (is_null($variable)) {
            return PDO::PARAM_NULL;
        }
        return PDO::PARAM_STR;
    }

    /**
     * Basic filtering to eliminate any tags and empty leading / trailing
     * characters.
     *
     * @param string $data
     * @return string
     */
    public function purify(string $data): string
    {
        return htmlspecialchars(trim($data), ENT_QUOTES | ENT_HTML401);
    }
}