16 Commits

Author SHA1 Message Date
96d0232d78 Added testing 2024-04-22 11:56:36 -04:00
99344fda7d Reorder and Added missing databases 2023-02-28 23:41:51 -03:00
a326904825 Use exception in ResultSet 2022-12-22 22:24:55 -03:00
2c6c0e6e55 Added exceptions with base numbering 300 2022-12-22 18:58:26 -03:00
cc7cd638e3 Added check for empty or false results. 2022-09-12 21:54:20 -03:00
758ff0e282 Added more detail in obtaining first result 2022-09-12 21:45:14 -03:00
6dda8e1515 FIX 2022-09-09 11:18:36 -04:00
6525eb2f31 FIX: missing autoload definitions 2022-09-09 10:56:05 -04:00
28e31de2a1 Readme 2022-09-08 20:02:11 -04:00
0df2e95e7f Implementations 2022-09-08 20:02:05 -04:00
7fbc52b702 Abstract 2022-09-08 20:01:52 -04:00
5322d81e1b Interfaces 2022-09-08 20:01:32 -04:00
ce460740cd Dependencies 2022-09-08 18:13:50 -04:00
f87aab17f6 Merge remote-tracking branch 'origin/master'
# Conflicts:
#	.gitignore
2022-09-08 18:12:44 -04:00
25bed5e41e Ignore 2022-09-08 18:10:15 -04:00
607172ccf3 Merge branch 'develop' 2021-10-13 22:46:59 -03:00
29 changed files with 803 additions and 90 deletions

8
.gitignore vendored
View File

@ -1,2 +1,8 @@
**/composer.lock
# Composer
**/vendor/
**/*.lock
# PHPStorm
**/.idea/
**/.cache/

22
Dockerfile Normal file
View File

@ -0,0 +1,22 @@
FROM composer:lts as deps
WORKDIR /app
RUN --mount=type=bind,source=./composer.json,target=composer.json \
--mount=type=bind,source=./composer.lock,target=composer.lock \
--mount=type=cache,target=/tmp/cache \
composer install --no-interaction
FROM php:8-cli as base
WORKDIR /app
RUN apt-get update && \
apt-get install -yq --no-install-recommends libsqlite3-dev && \
rm -rf /var/lib/apt/lists/* && \
docker-php-ext-install pdo pdo_sqlite
COPY ./src /app/src
FROM base as dev
COPY ./tests /app/tests
RUN mv "$PHP_INI_DIR/php.ini-development" "$PHP_INI_DIR/php.ini"
COPY --from=deps /app/vendor/ /app/vendor
FROM dev as test
ENTRYPOINT [ "./vendor/bin/phpunit" ]

128
Readme.md Normal file
View File

@ -0,0 +1,128 @@
# Database
Database Abstraction Layer
## Requirements
+ PHP 8+
+ PDO
### Recommended
+ [`php-di/php-di`](https://packagist.org/packages/php-di/php-di)
## Installation
`composer.json`
```
{
...
"repositories": [
...
{
"type": "git",
"path": "https://git.provm.cl/ProVM/database.git"
}
...
],
...
"require": {
...
"provm/database": "^2.3"
...
},
...
}
```
## Usage
For `MySQL`/`MariaDB`
Without `DI`
```
$database new ProVM\Database\MySQL();
$database->setHost(<host>);
$database->setPort(<port>); // If diferent from 3306
$database->setUsername(<username>);
$database->setPassword(<password>);
$connection = new ProVM\Database\Connection($database);
```
With `DI`
```
$container->set(ProVM\Concept\Database::class, function(Psr\Container\ContainerInterface $container) {
$database = $container->get(ProVM\Database\MySQL::class);
$database->setHost(<host>);
$database->setPort(<port>); // If diferent from 3306
$database->setUsername(<username>);
$database->setPassword(<password>);
return $database;
});
$container->set(ProVM\Concept\Database\Connection::class, function(Psr\Container\ContainerInterface $container) {
return new ProVM\Database\Connection($container->get(ProVM\Concept\Database::class);
});
```
Run query
```
$rs = $connection->query(<query>);
```
Run prepared statement
```
$rs = $connection->prepare(<query>)->execute(<value>);
or
$rs = $connection->execute(<query>, <values>);
```
Get data from ResultSet
```
$data = $rs->fetchFirst();
$data_object = $rs->fetchFirstAsObject();
$data_array = $rs->fetchAll();
$data_array_of_objects = $rs->fetchAllAsObjects();
```
Use transactions
```
$connection->transaction()->begin();
try {
$connection->execute($query, $values);
$connection->transaction()->commit();
} catch (PDOException $exception) {
$connection->transaction()->rollBack();
}
```
## Definitions
### Database
Database configuration
+ `Database::host` Connection host name [`string`]
+ `Database::port` Connection port number [`int`]
+ `Database::name` Database name to connect [`string`]
+ `Database::username` Optional. The username to connect [`string`]
+ `Database::password` Optional. The password for the username [`string`]
+ `Database->getDsn()` get dsn string for PDO connection. [`string`]
+ `Database->needsUser()` If a user is needed for the connection.
### Connection
Connection handling
+ `Connection::query` Query the database
+ `Connection::prepare` Prepare query statement
+ `Connection::execute` Prepare and execute a query statement
+ `Connection::transaction` Return a transaction
### Transaction
Transaction
+ `Transaction::begin` Begin transaction
+ `Transaction::commit` Commit changes
+ `Transaction::rollBack` Roll back changes
### ResultSet
Result set to handle PDOStatement
+ `ResultSet::execute` Execute a prepared statement
+ `ResultSet::fetchAll` Return query results as array of associative arrays
+ `ResultSet::fetchAllAsObjects` Return query results as array of objects
+ `ResultSet::fetchFirst` Return first result as associative array
+ `ResultSet::fetchFirstAsObject` Return first result as object
## TODO

View File

@ -1,8 +0,0 @@
<?php
namespace ProVM\Common\Define;
interface Engine {
public function __construct(string $host, string $name, ?int $port = null);
public function dsn(): string;
public function hasLogin(): bool;
}

View File

@ -1,36 +0,0 @@
<?php
namespace ProVM\Common\Service;
use \Model;
class Database {
protected $settings;
public function __construct($settings) {
$this->settings = $settings;
}
public function load() {
foreach ($this->settings->databases as $name => $settings) {
$engine = $this->getEngine($settings);
$configs = ['connection_string' => $engine->dsn()];
if ($engine->hasLogin()) {
$configs['username'] = $settings->user->name;
$configs['password'] = $settings->user->password;
}
Model::configure($configs, null, $name);
}
if (isset($this->settings->short_names)) {
Model::$short_table_names = $this->settings->short_names;
}
}
protected function getEngine($settings): \ProVM\Common\Define\Engine {
$name = match($settings->engine) {
'mysql' => 'MySQL'
};
$class = implode("\\", [
'ProVM',
'Database',
$name
]);
return new $class($settings->host->name, $settings->name, $settings->host->port ?? null);
}
}

5
compose.yml Normal file
View File

@ -0,0 +1,5 @@
services:
database:
build: .
volumes:
- ./:/app

View File

@ -1,21 +1,23 @@
{
"name": "provm/database",
"description": "Database loader for j4mie/paris",
"type": "library",
"require": {
"j4mie/paris": "^1.5"
},
"license": "MIT",
"autoload": {
"psr-4": {
"ProVM\\Database\\": "src/",
"ProVM\\Common\\": "common/"
}
},
"version": "1.2.0",
"authors": [
{
"name": "Aldarien",
"email": "aldarien85@gmail.com"
}
]
],
"require": {
"php": ">=8",
"ext-pdo": "*"
},
"require-dev": {
"phpunit/phpunit": "^10.0"
},
"autoload": {
"psr-4": {
"ProVM\\": "src/"
}
}
}

28
phpunit.xml Normal file
View File

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.5/phpunit.xsd"
bootstrap="vendor/autoload.php"
cacheResultFile=".cache/test-results"
executionOrder="depends,defects"
forceCoversAnnotation="false"
beStrictAboutCoversAnnotation="true"
beStrictAboutOutputDuringTests="true"
beStrictAboutTodoAnnotatedTests="true"
convertDeprecationsToExceptions="true"
colors="true"
failOnRisky="true"
failOnWarning="true"
verbose="true">
<testsuites>
<testsuite name="default">
<directory>tests</directory>
</testsuite>
</testsuites>
<coverage cacheDirectory=".cache/code-coverage"
processUncoveredFiles="true">
<include>
<directory suffix=".php">src</directory>
</include>
</coverage>
</phpunit>

18
src/Concept/Database.php Normal file
View File

@ -0,0 +1,18 @@
<?php
namespace ProVM\Concept;
interface Database
{
public function getHost(): string;
public function getPort(): int|bool;
public function getName(): string;
public function getUser(): string;
public function getPassword(): string;
public function needsUser(): bool;
public function setHost(string $host): Database;
public function setPort(int $port): Database;
public function setName(string $name): Database;
public function setUser(string $username): Database;
public function setPassword(string $password): Database;
public function getDsn(): string;
}

View File

@ -0,0 +1,15 @@
<?php
namespace ProVM\Concept\Database;
use ProVM\Concept\Database;
interface Connection
{
public function connect(): \PDO;
public function transaction(): Transaction;
public function query(string $query): ResultSet;
public function prepare(string $query): ResultSet;
public function execute(string $query, ?array $data = null): ResultSet;
}

View File

@ -0,0 +1,12 @@
<?php
namespace ProVM\Concept\Database;
interface ResultSet
{
public function execute(array $data): ResultSet;
public function fetchFirst(): array;
public function fetchAll(): array;
public function fetchFirstAsObject(): object;
public function fetchAllAsObjects(): array;
}

View File

@ -0,0 +1,9 @@
<?php
namespace ProVM\Concept\Database;
interface Transaction
{
public function begin(): Transaction;
public function commit(): void;
public function rollBack(): void;
}

View File

@ -0,0 +1,86 @@
<?php
namespace ProVM\Database;
use PDO;
use ProVM\Concept\Database;
use ProVM\Exception\Database\InvalidQuery;
class Connection implements Database\Connection
{
public function __construct(Database $database)
{
$this->setDatabase($database);
}
protected Database $database;
protected function getDatabase(): Database
{
return $this->database;
}
protected function setDatabase(Database $database): Database\Connection
{
$this->database = $database;
return $this;
}
protected PDO $pdo;
public function connect(): PDO
{
if (!isset($this->pdo)) {
$dsn = $this->getDatabase()->getDsn();
if ($this->getDatabase()->needsUser()) {
$this->pdo = new PDO($dsn, $this->getDatabase()->getUser(), $this->getDatabase()->getPassword());
} else {
$this->pdo = new PDO($dsn);
}
}
return $this->pdo;
}
protected Database\Transaction $transaction;
public function transaction(): Database\Transaction
{
if (!isset($this->transaction)) {
$this->transaction = new Transaction($this);
}
return $this->transaction;
}
public function query(string $query): Database\ResultSet
{
$statement = $this->connect()->query($query);
if ($statement === false) {
throw new InvalidQuery($query);
}
return new ResultSet($statement);
}
public function prepare(string $query): Database\ResultSet
{
$statement = $this->connect()->prepare($query);
if ($statement === false) {
throw new InvalidQuery($query);
}
return new ResultSet($statement);
}
public function execute(string $query, ?array $data = null): Database\ResultSet
{
if ($data !== null) {
$rs = $this->prepare($query);
$rs->execute($data);
return $rs;
}
return $this->query($query);
}
public function fetchOne(string $query, ?array $data = null): array
{
return $this->execute($query, $data)->fetchFirst();
}
public function fetchMany(string $query, ?array $data = null): array
{
return $this->execute($query, $data)->fetchAll();
}
}

21
src/Database/MySQL.php Normal file
View File

@ -0,0 +1,21 @@
<?php
namespace ProVM\Database;
use ProVM\Implement\Database;
class MySQL extends Database
{
public function needsUser(): bool
{
return true;
}
public function getDsn(): string
{
$dsn = ["mysql:host={$this->getHost()}"];
if ($this->getPort()) {
$dsn []= "port={$this->getPort()}";
}
$dsn []= "dbname={$this->getName()}";
return implode(';', $dsn);
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace ProVM\Database;
use ProVM\Implement\Database;
class PostgreSQL extends Database
{
public function getDsn(): string
{
$dsn = ["pgsql:host={$this->getHost()}"];
if ($this->getPort()) {
$dsn []= "port={$this->getPort()}";
}
$dsn []= "dbname={$this->getName()}";
$dsn []= "user={$this->getUser()}";
$dsn []= "password={$this->getPassword()}";
return implode(';', $dsn);
}
}

View File

@ -0,0 +1,57 @@
<?php
namespace ProVM\Database;
use PDO;
use PDOStatement;
use ProVM\Concept\Database;
use ProVM\Exception\Database\BlankResult;
class ResultSet implements Database\ResultSet
{
public function __construct(PDOStatement $statement)
{
$this->setStatement($statement);
}
protected PDOStatement $statement;
protected function getStatement(): PDOStatement
{
return $this->statement;
}
protected function setStatement(PDOStatement $statement): ResultSet
{
$this->statement = $statement;
return $this;
}
public function execute(array $data): Database\ResultSet
{
$this->statement->execute($data);
return $this;
}
protected function checkResults(): PDOStatement
{
if ($this->getStatement()->rowCount() === 0) {
throw new BlankResult(query: $this->getStatement()->queryString);
}
return $this->getStatement();
}
public function fetchFirst(): array
{
return $this->checkResults()->fetch(PDO::FETCH_ASSOC);
}
public function fetchAll(): array
{
return $this->checkResults()->fetchAll(PDO::FETCH_ASSOC);
}
public function fetchFirstAsObject(): object
{
return $this->checkResults()->fetch(PDO::FETCH_OBJ);
}
public function fetchAllAsObjects(): array
{
return $this->checkResults()->fetchAll(PDO::FETCH_OBJ);
}
}

12
src/Database/SQLite.php Normal file
View File

@ -0,0 +1,12 @@
<?php
namespace ProVM\Database;
use ProVM\Implement\Database;
class SQLite extends Database
{
public function getDsn(): string
{
return "sqlite:{$this->getHost()}";
}
}

View File

@ -0,0 +1,40 @@
<?php
namespace ProVM\Database;
use PDO;
use ProVM\Concept;
class Transaction implements Concept\Database\Transaction
{
public function __construct(Concept\Database\Connection $connection)
{
$this->setConnection($connection);
}
protected Concept\Database\Connection $connection;
public function getConnection(): Concept\Database\Connection
{
return $this->connection;
}
public function setConnection(Concept\Database\Connection $connection): Concept\Database\Transaction
{
$this->connection = $connection;
return $this;
}
public function begin(): Concept\Database\Transaction
{
$this->getConnection()->connect()->beginTransaction();
return $this;
}
public function commit(): void
{
$this->getConnection()->connect()->commit();
}
public function rollBack(): void
{
$this->getConnection()->connect()->rollBack();
}
}

View File

@ -0,0 +1,16 @@
<?php
namespace ProVM\Exception;
use Exception;
use Throwable;
abstract class Database extends Exception
{
const BASE_CODE = 600;
public function __construct(string $message = "", int $code = 0, ?Throwable $previous = null)
{
$code += Database::BASE_CODE;
parent::__construct($message, $code, $previous);
}
}

View File

@ -0,0 +1,20 @@
<?php
namespace ProVM\Exception\Database;
use Throwable;
use ProVM\Exception\Database;
class BlankResult extends Database
{
public function __construct(?string $table = null, ?string $query = null, ?Throwable $previous = null)
{
$message = implode('', [
"No results found",
($query !== null) ? " in {$query}" : '',
($table !== null) ? " in {$table}" : '',
'.'
]);
$code = 1;
parent::__construct($message, $code, $previous);
}
}

View File

@ -0,0 +1,14 @@
<?php
namespace ProVM\Exception\Database;
use Throwable;
use ProVM\Exception;
class InvalidQuery extends Exception\Database
{
public function __construct(string $query, ?Throwable $previous = null)
{
$message = "Invalid query \"{$query}\"";
parent::__construct($message, 0, $previous);
}
}

View File

@ -0,0 +1,65 @@
<?php
namespace ProVM\Implement;
use ProVM\Concept;
abstract class Database implements Concept\Database
{
protected string $host;
protected int $port;
protected string $name;
protected string $user;
protected string $password;
public function getHost(): string
{
return $this->host;
}
public function getPort(): int|bool
{
return $this->port ?? false;
}
public function getName(): string
{
return $this->name;
}
public function getUser(): string
{
return $this->user;
}
public function getPassword(): string
{
return $this->password;
}
public function setHost(string $host): Concept\Database
{
$this->host = $host;
return $this;
}
public function setPort(int $port): Concept\Database
{
$this->port = $port;
return $this;
}
public function setName(string $name): Concept\Database
{
$this->name = $name;
return $this;
}
public function setUser(string $username): Concept\Database
{
$this->user = $username;
return $this;
}
public function setPassword(string $password): Concept\Database
{
$this->password = $password;
return $this;
}
public function needsUser(): bool
{
return false;
}
}

View File

@ -1,32 +0,0 @@
<?php
namespace ProVM\Database;
use ProVM\Common\Define\Engine;
class MySQL implements Engine {
protected $host;
protected $name;
public function __construct(string $host, string $name, ?int $port = null) {
$host_arr = [
'name' => $host
];
if ($port !== null) {
$host_arr['port'] = $port;
}
$this->host = (object) $host_arr;
$this->name = $name;
}
public function dsn(): string {
$dsn = [
'host=' . $this->host->name
];
if (isset($this->host->port)) {
$dsn []= 'port=' . $this->host->port;
}
$dsn []= 'dbname=' . $this->name;
return 'mysql:' . implode(';', $dsn);
}
public function hasLogin(): bool {
return true;
}
}

65
tests/ConnectionTest.php Normal file
View File

@ -0,0 +1,65 @@
<?php
use PHPUnit\Framework\TestCase;
use ProVM\Database\Connection;
use ProVM\Concept;
class ConnectionTest extends TestCase
{
protected PDO $pdo;
protected function setUp(): void
{
$this->pdo = new PDO('sqlite::memory:');
$query = "CREATE TABLE IF NOT EXISTS test_table (id INTEGER PRIMARY KEY, test TEXT)";
$this->pdo->query($query);
}
protected function tearDown(): void
{
unset($this->pdo);
}
public function testConnection()
{
$host = "memory";
$database = $this->getMockBuilder(Concept\Database::class)->getMock();
$database->method('getHost')->willReturn($host);
$database->method('getDsn')->willReturn("sqlite::{$host}");
$database->method('needsUser')->willReturn(false);
$connection = new Connection($database);
$this->assertEquals($this->pdo, $connection->connect());
}
public function testQuery()
{
$host = "memory";
$database = $this->getMockBuilder(Concept\Database::class)->getMock();
$database->method('getHost')->willReturn($host);
$database->method('getDsn')->willReturn("sqlite::{$host}");
$database->method('needsUser')->willReturn(false);
$connection = new Connection($database);
$query = "CREATE TABLE IF NOT EXISTS test_table (id INTEGER PRIMARY KEY, test TEXT)";
$connection->query($query);
$query = "SELECT * FROM test_table";
$result = $connection->query($query);
$this->assertInstanceOf(Concept\Database\ResultSet::class, $result);
}
public function testPrepare()
{
$host = "memory";
$database = $this->getMockBuilder(Concept\Database::class)->getMock();
$database->method('getHost')->willReturn($host);
$database->method('getDsn')->willReturn("sqlite::{$host}");
$database->method('needsUser')->willReturn(false);
$connection = new Connection($database);
$query = "CREATE TABLE IF NOT EXISTS test_table (id INTEGER PRIMARY KEY, test TEXT)";
$connection->query($query);
$query = "SELECT * FROM test_table";
$result = $connection->prepare($query);
$this->assertInstanceOf(Concept\Database\ResultSet::class, $result);
}
}

32
tests/MySQLTest.php Normal file
View File

@ -0,0 +1,32 @@
<?php
use PHPUnit\Framework\TestCase;
use ProVM\Database\MySQL;
class MySQLTest extends TestCase
{
public function testDatabase()
{
$host = "testhost";
$port = 1234;
$name = "testdb";
$user = "testuser";
$pass = "testpass";
$dsn = "mysql:host={$host};port={$port};dbname={$name}";
$database = new MySQL();
$database->setHost($host);
$database->setPort($port);
$database->setName($name);
$database->setUser($user);
$database->setPassword($pass);
$this->assertEquals($host, $database->getHost());
$this->assertEquals($port, $database->getPort());
$this->assertEquals($name, $database->getName());
$this->assertEquals($user, $database->getUser());
$this->assertEquals($pass, $database->getPassword());
$this->assertTrue($database->needsUser());
$this->assertEquals($dsn, $database->getDsn());
}
}

32
tests/PostgreSQLTest.php Normal file
View File

@ -0,0 +1,32 @@
<?php
use PHPUnit\Framework\TestCase;
use ProVM\Database\PostgreSQL;
class PostgreSQLTest extends TestCase
{
public function testDatabase()
{
$host = "testhost";
$port = 1234;
$name = "testdb";
$user = "testuser";
$pass = "testpass";
$dsn = "pgsql:host={$host};port={$port};dbname={$name};user={$user};password={$pass}";
$database = new PostgreSQL();
$database->setHost($host);
$database->setPort($port);
$database->setName($name);
$database->setUser($user);
$database->setPassword($pass);
$this->assertEquals($host, $database->getHost());
$this->assertEquals($port, $database->getPort());
$this->assertEquals($name, $database->getName());
$this->assertEquals($user, $database->getUser());
$this->assertEquals($pass, $database->getPassword());
$this->assertFalse($database->needsUser());
$this->assertEquals($dsn, $database->getDsn());
}
}

26
tests/ResultSetTest.php Normal file
View File

@ -0,0 +1,26 @@
<?php
use PHPUnit\Framework\TestCase;
use ProVM\Database\ResultSet;
class ResultSetTest extends TestCase
{
public function testResultSet()
{
$result1 = ['col1', 'col2', 'col3'];
$result2 = [['col1', 'col2'], ['col3', 'col4']];
$statement = $this->getMockBuilder(PDOStatement::class)->getMock();
$statement->method('execute')->willReturn(true);
$statement->method('fetch')->willReturn($result1);
$statement->method('fetchAll')->willReturn($result2);
$statement->method('rowCount')->willReturn(2);
$resultSet = new ResultSet($statement);
$resultSet->execute(['foo' => 'bar']);
$this->assertTrue(true);
$this->assertEquals($result1, $resultSet->fetchFirst());
$this->assertEquals($result2, $resultSet->fetchAll());
}
}

20
tests/SQLiteTest.php Normal file
View File

@ -0,0 +1,20 @@
<?php
use PHPUnit\Framework\TestCase;
use ProVM\Database\SQLite;
class SQLiteTest extends TestCase
{
public function testDatabase()
{
$host = ":memory:";
$dsn = "sqlite:{$host}";
$database = new SQLite();
$database->setHost($host);
$this->assertEquals($host, $database->getHost());
$this->assertFalse($database->needsUser());
$this->assertEquals($dsn, $database->getDsn());
}
}

19
tests/TransactionTest.php Normal file
View File

@ -0,0 +1,19 @@
<?php
use PHPUnit\Framework\TestCase;
use ProVM\Database\Transaction;
class TransactionTest extends TestCase
{
public function testTransaction()
{
$connection = $this->createMock(ProVM\Concept\Database\Connection::class);
$transaction = new Transaction($connection);
$transaction->begin();
$this->assertTrue(true);
$transaction->commit();
$this->assertTrue(true);
$transaction->begin();
$transaction->rollback();
$this->assertTrue(true);
}
}