vijoni/database-connector

Handle database read/write connections. Use native database engine query syntax

2.0.0 2022-11-13 20:06 UTC

This package is auto-updated.

Last update: 2024-05-13 23:52:35 UTC


README

Handle database read/write connections. Use native database engine query syntax.
( currently only postresql is supported )

Usage

Check integration tests for more details

Connect to the database

Connection parameters are provided in the form of the url, according to the mentioned schema:

// $dbUrl = 'pgsql://user:password@host:port/database_name
$masterDbUrl = 'pgsql://postgres:postgres@127.0.0.1:5432/postgres';
$slaveDbUrl = 'pgsql://postgres:postgres@127.0.0.2:5432/postgres';
$dbClient = new PgDatabaseClient($masterDbUrl, $slaveDbUrl);
$dbClient->init();

Connection is made inside the init() call.
Slave parameter can be skipped. In such case only one connection will be established with the database.
Using $dbClient->useWriteConnection() will keep the same connection. If both master and slave urls are provided, init() will connect to slave database only. Connection to master will be established on $dbClient->useWriteConnection() call, and will be kept as the active connection for all future queries.
If master and slave urls are the same it will be handled as two separate connections.

Running queries

Query comment is optional, it helps to identify the query in the database logs.
Example of postgresql log entry:

2022-03-30 17:44:26.321 UTC [2006] postgres@database_connector_test_20220330174425_889153_791607487 LOG:  statement: -- query native single row test
        SELECT 'some random string';

Simple query

Execute a query that does not require any parameter replacement.
Results are returned as \Generator objects.

$rowGenerator = $dbClient->queryNative("SELECT 'some random text';", 'read random text');
$row = $rowGenerator->current();

To get the result in the form of an associative array:

$allRows = $dbClient->queryNativeAll('SELECT name, age FROM customers;', 'read all customer names with age');
/**
 * $allRows = [
 *   ['name' => 'Konrad', 'age' => 21],
 *   ['name' => 'Thomas', 'age' => 32],
 * ];
 */

Query with parameter escaping

Parameters are escaped according to the used placeholder.

$queryBuilder = $dbClient->newQueryBuilder('SELECT name, age FROM customers WHERE name = %s;', ['Konrad'], [], 'read details about Konrad');
$rowGenerator = $dbClient->query($queryBuilder);
PlaceholderDescriptionSample ValueSample QUERYResult
%%% character%SELECT %%SELECT %
%sstring value'text', nullSELECT %sSELECT 'text'<br/>SELECT NULL
%iinteger value21, nullSELECT %iSELECT 21<br/>SELECT NULL
%ddecimal value43.23, nullSELECT %dSELECT 43.23<br/>SELECT NULL
%bboolean valuetrue, false, 'yes', 'no', 't', 'f', 1, 0, nullSELECT %bSELECT TRUE<br/>SELECT FALSE<br/>SELECT NULL
%ccolumn name'name'SELECT FROM %cSELECT FROM "name"
%t?detect value type'text', 21, 43.23, null, trueSELECT %t?SELECT 'text'<br/>SELECT 21<br/>SELECT NULL
%lslist of strings['konrad', 'thomas', null]SELECT IN (%s)SELECT IN ('konrad', 'thomas', NULL)
%lilist of integers[21, 33, null]SELECT IN (%s)SELECT IN (21, 33, NULL)
%ldlist of decimals[3.5, 1.65, null]SELECT IN (%s)SELECT IN (3.5, 1.65, NULL)
%lblist of booleans[true, false, null]SELECT IN (%s)SELECT IN (TRUE, FALSE, NULL)
%l?list of different types[true, false, 21, 'konrad', null]SELECT IN (%s)SELECT IN (TRUE, FALSE, 21, 'konrad', NULL)
%lclist of identifiers['name', 'age']SELECT %lc FROMSELECT "name", "age" FROM
%lulist of column=value['name' => 'konrad', 'age' => 21]UPDATE SET %luUPDATE SET "name" = 'konrad', "age" = 21

For longer queries you could prefer using named parameter replacement:

$queryBuilder = $dbClient->newQueryBuilder('SELECT name, age FROM customers WHERE country_code = %s_country_code AND age > %i_age;', ['country_code' => 'GB', 'age' => 18], [], 'find Great Britain grownups');
$rowGenerator = $dbClient->query($queryBuilder);

Positioned and named parameters cannot be mixed in a single query

Query with constant replacements

You can do string replacement

$queryBuilder = $dbClient->newQueryBuilder('SELECT count(*) as __total FROM customers', [], ['__total' => 'total_customers'], 'count customers');
// executed query: 'SELECT count(*) as total_customers FROM customers'
$rowGenerator = $dbClient->query($queryBuilder);

Query methods

queryNative(string $query, string $comment = ''): \Generator;
queryNativeAll(string $string, string $comment = ''): array

query(QueryBuilder $queryBuilder): \Generator
queryAll(QueryBuilder $queryBuilder): array

querySingleRow(QueryBuilder $queryBuilder): QueryResultValue
QueryResultValue:
  getValue(): mixed
  notFound(): bool

// below methods throw DatabaseValueNotFoundException if value does not exist
querySingleValue(QueryBuilder $queryBuilder): mixed
queryStringValue(QueryBuilder $queryBuilder): string|null
queryIntValue(QueryBuilder $queryBuilder): int|null
queryFloatValue(QueryBuilder $queryBuilder): float|null
queryBoolValue(QueryBuilder $queryBuilder): bool|null

Transactions

$dbClient->startTransaction();
try {
  $dbClient->queryNative("INSERT INTO catalog VALUES('12-43-54','teddy-bear')");
  $dbClient->queryNative("INSERT INTO category VALUES('toys','12-43-54')");
  $dbClient->commit();
} catch (DatabaseException $e) {
  $dbClient->rollback();
}

Nested transactions are implemented with a transaction counter. Transaction start increases the counter, transaction rollback decreases the counter. If a rollback transaction happens inside a nested execution, whole execution will be marked for rollback. If you try to commit a transaction that has been marked for rollback DatabaseTransactionException will be thrown. It is important to be aware or all possible failures and handle them properly.

// this is just an example, ignore the fact that it may not make sense:)
$dbClient->startTransaction();
  $dbClient->queryNative("INSERT INTO catalog VALUES('12-43-54','teddy-bear')");
  $dbClient->queryNative("INSERT INTO category VALUES('toys','12-43-54')");
  
  $dbClient->startTransaction();
    $dbClient->queryNative("INSERT INTO message_queue VALUES('12-43-54', 'teddy_bear')");
  $dbClient->commit();
$dbClient->commit();

Development

Vagrant setup is included in the repository.
Provisioning will install PHP, Xdebug and Postgresql.
Source code is mounted to /app.

Check composer.json for useful commands. The most important ones are:

./composer codecheck // run code analysis and tests
./composer drop_test_dbs // remove all databases created during intergration tests execution

For the purpose of running queries on a database, for every integration tests suite execution, database_connector_test_XXX database is created. I wanted a fully isolated environment for every test run, without the need of using transactions.