Skip to content

Commit

Permalink
Refactor for new squirrel connection dependency
Browse files Browse the repository at this point in the history
  • Loading branch information
iquito committed Sep 28, 2024
1 parent b3e433c commit 53e9641
Show file tree
Hide file tree
Showing 63 changed files with 1,369 additions and 2,637 deletions.
7 changes: 1 addition & 6 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,4 @@ insert_final_newline = false

[*.php]

end_of_line = lf
charset = utf-8
indent_style = space
indent_size = 4
insert_final_newline = true
trim_trailing_whitespace = true
insert_final_newline = true
12 changes: 4 additions & 8 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -1,15 +1,11 @@
/bin export-ignore
/build export-ignore
/tests export-ignore
/tools export-ignore
/examples export-ignore
/docker export-ignore
/vendor-bin export-ignore
/.editorconfig export-ignore
/.gitattributes export-ignore
/.gitignore export-ignore
/.travis.yml export-ignore
/captainhook.json export-ignore
/phpstan.neon export-ignore
/phpstan-baseline.neon export-ignore
/phpunit.xml.dist export-ignore
/psalm.xml export-ignore
/psalm-baseline.xml export-ignore
/ruleset.xml export-ignore
/.travis.yml export-ignore
6 changes: 3 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
/vendor
/vendor-bin/**/vendor
/vendor-bin/**/composer.lock
/.phpunit.result.cache
/.phpcs-cache
/tests/_output
/tests/_reports
/build
/build
/tools/cache/*
!/tools/cache/.gitkeep
131 changes: 66 additions & 65 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ Squirrel Queries

Provides a slimmed down concise interface for low level database queries and transactions (DBInterface) as well as a query builder to make it easier and more expressive to create queries (DBBuilderInterface). The interfaces are limited to avoid confusion/misuse and encourage fail-safe usage.

Doctrine DBAL is used for the underlying connection (and abstraction) handling, what we add are an insertOrUpdate functionality (known as UPSERT), structured queries which are easier to write and read (and for which the query builder can be used), and the possibility to layer database concerns (like actual implementation, connections retries, performance measurements, logging, etc.). This library also smoothes over some differences between MySQL, Postgres and SQLite. While DBAL is a dependency for now, when using this library you only need to configure/create the necessary DBAL connections in your code, no other parts of DBAL are relevant.
[squirrelphp/connection](https://github.com/squirrelphp/connection) is used for the underlying connection (and abstraction) handling starting with v2.0 (v1.3 and below had Doctrine DBAL as a dependency), what we add are an insertOrUpdate functionality (known as UPSERT), structured queries which are easier to write and read (and for which the query builder can be used), and the possibility to layer database concerns (like actual implementation, connections retries, performance measurements, logging, etc.). This library also smoothes over some differences between MySQL, Postgres and SQLite.

By default this library provides two layers, one dealing with Doctrine DBAL (passing the queries, processing and returning the results) and one dealing with errors (DBErrorHandler). DBErrorHandler catches deadlocks and connection problems and tries to repeat the query or transaction, and it unifies the exceptions coming from DBAL so the originating call to DBInterface is provided and the error can easily be found.
By default this library provides two layers, one dealing with the actual database connection (passing the queries, processing and returning the results) and one dealing with errors (DBErrorHandler). DBErrorHandler catches deadlocks and connection problems and tries to repeat the query or transaction, and it unifies the exceptions coming from the connection so the originating call to DBInterface is provided and the error can easily be found.

Installation
------------
Expand All @@ -27,79 +27,80 @@ Table of contents
Setting up
----------

Use Squirrel\Queries\DBInterface as a type hint in your services for the low-level interface, and/or Squirrel\Queries\DBBuilderInterface for the query builder. The low-level interface options are based upon Doctrine and PDO with some tweaks, and the builder interface is an expressive way to write structured (and not too complex) queries.
Use Squirrel\Queries\DBInterface as a type hint in your services for the low-level interface, and/or Squirrel\Queries\DBBuilderInterface for the query builder - the query builder is an expressive way to write structured (and not too complex) queries.

If you know Doctrine or PDO you should be able to use this library easily. You should especially have an extra look at structured queries and UPSERT, as these are additions to the low-level interface, helping you to make readable queries and taking care of your column field names and parameters automatically, making it easier to write secure queries.
If you know Doctrine DBAL or PDO you should be able to use this library easily, while avoiding some of their complexities. You should especially have an extra look at structured queries and UPSERT, as these are additions to the low-level interface, helping you to make readable queries and taking care of your column field names and parameters automatically, making it easier to write secure queries.

For a solution which integrates easily with the Symfony framework, check out [squirrelphp/queries-bundle](https://github.com/squirrelphp/queries-bundle), and for entity and repository support check out [squirrelphp/entities](https://github.com/squirrelphp/entities) and [squirrelphp/entities-bundle](https://github.com/squirrelphp/entities-bundle).

If you want to assemble a DBInterface object yourself, something like the following code can be a start:

use Doctrine\DBAL\DriverManager;
use Squirrel\Queries\DBBuilderInterface;
use Squirrel\Queries\DBInterface;
use Squirrel\Queries\Doctrine\DBErrorHandler;
use Squirrel\Queries\Doctrine\DBMySQLImplementation;

// Create a doctrine connection
$dbalConnection = DriverManager::getConnection([
'url' => 'mysql://user:secret@localhost/mydb',
'driverOptions' => [
\PDO::ATTR_EMULATE_PREPARES => false, // Separates query and values
\PDO::MYSQL_ATTR_FOUND_ROWS => true, // important so MySQL behaves like Postgres/SQLite
\PDO::MYSQL_ATTR_MULTI_STATEMENTS => false,
],
]);
If you want to assemble a DBInterface and DBBuilder yourself (even though you will likely want to use integration bundles instead), something like the following code can be a start:

// Create a MySQL implementation layer
$implementationLayer = new DBMySQLImplementation($dbalConnection);
```php
use Squirrel\Connection\Config\Mysql;
use Squirrel\Connection\PDO\ConnectionPDO;
use Squirrel\Queries\DBBuilder;
use Squirrel\Queries\DBInterface;
use Squirrel\Queries\DB\ErrorHandler;
use Squirrel\Queries\DB\MySQLImplementation;

// Create an error handler layer
$errorLayer = new DBErrorHandler();
// Create a squirrel connection
$connection = new ConnectionPDO(
new Mysql(
host: 'localhost',
user: 'user',
password: 'password',
dbname: 'mydb',
),
);

// Set implementation layer beneath the error layer
$errorLayer->setLowerLayer($implementationLayer);
// Create a MySQL implementation layer
$implementationLayer = new MySQLImplementation($connection);

// Rename our layered service - this is now our database object
$db = $errorLayer;
// Create an error handler layer
$errorLayer = new ErrorHandler();

// $db is now useable and can be injected
// anywhere you need it. Typehint it with
// \Squirrel\Queries\DBInterface
// Set implementation layer beneath the error layer
$errorLayer->setLowerLayer($implementationLayer);

$fetchEntry = function(DBInterface $db): array {
return $db->fetchOne('SELECT * FROM table');
};
// Rename our layered service - this is now our database object
$db = $errorLayer;

$fetchEntry($db);
// $db is now useable and can be injected
// anywhere you need it. Typehint it with
// \Squirrel\Queries\DBInterface
$fetchEntry = function(DBInterface $db): array {
return $db->fetchOne('SELECT * FROM table');
};

// A builder just needs a DBInterface to be created:
$fetchEntry($db);

$queryBuilder = new DBBuilderInterface($db);
// A builder just needs a DBInterface to be created:
$queryBuilder = new DBBuilder($db);

// The query builder generates more readable queries, and
// helps your IDE in terms of type hints / possible options
// depending on the query you are doing
$entries = $queryBuilder
->select()
->fields([
'id',
'name',
])
->where([
'name' => 'Robert',
])
->getAllEntries();
// The query builder generates more readable queries, and
// helps your IDE in terms of type hints / possible options
// depending on the query you are doing
$entries = $queryBuilder
->select()
->fields([
'id',
'name',
])
->where([
'name' => 'Robert',
])
->getAllEntries();

// If you want to add more layers, you can create a
// class which implements DBRawInterface and includes
// the DBPassToLowerLayer trait and then just overwrite
// the functions you want to change, and then connect
// it to the other layers through setLowerLayer
// If you want to add more layers, you can create a
// class which implements DBRawInterface and includes
// the DBPassToLowerLayer trait and then just overwrite
// the functions you want to change, and then connect
// it to the other layers through setLowerLayer

// It is also a good idea to catch \Squirrel\Queries\DBException
// in your application in case of a DB error so it
// can be handled gracefully
// It is also a good idea to catch \Squirrel\Queries\DBException
// in your application in case of a DB error so it
// can be handled gracefully
```

Database support
----------------
Expand Down Expand Up @@ -294,13 +295,13 @@ with the values `5`, `Henry` and `Liam`.

UPSERT (update-or-insert) queries are an addition to SQL, known under different queries in different database systems:

- MySQL implemented them as "INSERT ... ON DUPLICATE KEY UPDATE"
- MySQL and MariaDB implemented them as "INSERT ... ON DUPLICATE KEY UPDATE"
- PostgreSQL and SQLite as "INSERT ... ON CONFLICT (index) DO UPDATE"
- The ANSI standard knows them as MERGE queries, although those can be a bit different

In this library we call this type of query `insertOrUpdate`. Such a query tries to insert a row, but if the row already exists it does an update instead, and all of this is done as one atomic operation in the database. If implemented without an UPSERT query you would need at least an UPDATE and then possibly an INSERT query within a transaction to do the same. UPSERT exists to be a faster and easier solution.

PostgreSQL and SQLite need the specific column names which form a unique index in the table which is used to determine if an entry already exists or if a new entry is inserted. MySQL does this automatically, but for all database systems it is important to have a unique index involved in an UPSERT query.
PostgreSQL and SQLite need the specific column names which form a unique index in the table which is used to determine if an entry already exists or if a new entry is inserted. MySQL/MariaDB do this automatically, but for all database systems it is important to have a unique index involved in an UPSERT query.

#### Usage and examples

Expand All @@ -319,7 +320,7 @@ $db->insertOrUpdate('users_visits', [
]);
```

For MySQL, this query would be converted to:
For MySQL/MariaDB, this query would be converted to:

```php
$db->change('INSERT INTO `users_visits` (`userId`,`visit`) VALUES (?,?) ON DUPLICATE KEY UPDATE `visit` = `visit` + 1', [5, 1]);
Expand All @@ -344,7 +345,7 @@ $db->insertOrUpdate('users_names', [
]);
```

This would INSERT with userId and firstName, but if the row already exists, it would just update firstName to Jane, so for MySQL it would be converted to:
This would INSERT with userId and firstName, but if the row already exists, it would just update firstName to Jane, so for MySQL/MariaDB it would be converted to:

```php
$db->change('INSERT INTO `users_names` (`userId`,`firstName`) VALUES (?,?) ON DUPLICATE KEY UPDATE `firstName`=?, [5, 'Jane', 'Jane']);
Expand Down Expand Up @@ -816,12 +817,12 @@ BLOB handling for Postgres
For MySQL and SQLite retrieving or inserting/updating BLOBs (Binary Large Objects) works just the same as with shorter/non-binary string fields. Postgres needs some adjustments, but these are streamlined by this library:

- For SELECT queries, streams returned by Postgres are automatically converted into strings, mimicking how MySQL and SQLite are doing it
- For INSERT/UPDATE queries, you need to wrap BLOB values with an instance of LargeObject provided by this library.
- For INSERT/UPDATE queries, you need to wrap BLOB values with an instance of LargeObject provided by `squirrelphp/connection`.

So the following works if `file_data` is a BYTEA field in Postgres:

```php
use Squirrel\Queries\LargeObject;
use Squirrel\Connection\LargeObject;

$rowsAffected = $dbBuilder
->update()
Expand Down
48 changes: 48 additions & 0 deletions bin/vendorbin
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
#!/usr/bin/env php
<?php
error_reporting(E_ALL); // Report everything, even notices
set_time_limit(0); // No time limit for console commands

$projectDir = dirname(__DIR__);

$composerRunType = $_SERVER['argv'][1] ?? 'outdated';

require $projectDir.'/vendor/autoload.php';

$sourceFinder = new \Symfony\Component\Finder\Finder();
$sourceFinder->in($projectDir . '/vendor-bin')->directories()->depth(0)->sortByName();

/** @var array<string, \Symfony\Component\Process\Process> $tools */
$tools = [];

foreach ($sourceFinder as $directory) {
$toolName = $directory->getFilename();

$options = [
'--ansi',
];

if ($composerRunType === 'update') {
$options[] = '--no-progress';
}

$process = new \Symfony\Component\Process\Process(['composer', $composerRunType, ...$options]);
if (isset($_SERVER['COMPOSER_CACHE_DIR'])) {
$process->setEnv(['COMPOSER_CACHE_DIR' => $_SERVER['COMPOSER_CACHE_DIR']]);
}
$process->setWorkingDirectory($projectDir . '/vendor-bin/' . $toolName);
$process->start();
$process->wait();

echo 'Running composer ' . $composerRunType . ' for ' . $toolName . ' ...' . "\n";

$processOutput = \trim($process->getOutput());

if ($composerRunType === 'update') {
$processOutput = \trim($processOutput . "\n" . $process->getErrorOutput());
}

if (\strlen($processOutput) > 0) {
echo $processOutput . "\n";
}
}
57 changes: 31 additions & 26 deletions composer.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "squirrelphp/queries",
"type": "library",
"description": "Slimmed down concise interface and query builder for database queries and transactions which can be layered / decorated.",
"description": "Slimmed down concise interfaces and query builder for database queries and transactions which can be layered / decorated.",
"keywords": [
"php",
"mysql",
Expand All @@ -19,17 +19,18 @@
}
],
"require": {
"php": ">=8.0",
"ext-pdo": "*",
"squirrelphp/debug": "^2.0",
"doctrine/dbal": "^3.0"
"php": ">=8.2",
"squirrelphp/connection": "^0.2",
"squirrelphp/debug": "^2.0"
},
"require-dev": {
"bamarni/composer-bin-plugin": "^1.8",
"captainhook/plugin-composer": "^5.0",
"phpunit/phpunit": "^9.0",
"captainhook/captainhook-phar": "^5.0",
"captainhook/hook-installer": "^1.0",
"phpunit/phpunit": "^11.2",
"mockery/mockery": "^1.0",
"squirrelphp/types": "^1.0"
"squirrelphp/types": "^1.0",
"symfony/finder": "^7.0",
"symfony/process": "^7.0"
},
"suggest": {
"squirrelphp/queries-bundle": "Symfony integration of squirrelphp/queries - automatic assembling of decorated connections",
Expand All @@ -39,9 +40,13 @@
"config": {
"sort-packages": false,
"allow-plugins": {
"bamarni/composer-bin-plugin": true,
"captainhook/plugin-composer": true,
"composer/package-versions-deprecated": true
"captainhook/captainhook-phar": true,
"captainhook/hook-installer": true
}
},
"extra": {
"captainhook": {
"config": "tools/captainhook.json"
}
},
"autoload": {
Expand All @@ -55,19 +60,19 @@
}
},
"scripts": {
"phpstan": "vendor/bin/phpstan analyse",
"phpstan_full": "vendor/bin/phpstan clear-result-cache && vendor/bin/phpstan analyse",
"phpstan_base": "vendor/bin/phpstan analyse --generate-baseline",
"psalm": "vendor/bin/psalm --show-info=false",
"psalm_full": "vendor/bin/psalm --clear-cache && vendor/bin/psalm --show-info=false",
"psalm_base": "vendor/bin/psalm --set-baseline=psalm-baseline.xml",
"phpunit": "vendor/bin/phpunit --colors=always",
"phpunit_clover": "vendor/bin/phpunit --coverage-text --coverage-clover build/logs/clover.xml",
"coverage": "XDEBUG_MODE=coverage vendor/bin/phpunit --coverage-html tests/_reports",
"phpcs": "vendor/bin/phpcs --standard=ruleset.xml --extensions=php --cache=.phpcs-cache --colors src tests",
"phpcsd": "vendor/bin/phpcs -s --standard=ruleset.xml --extensions=php --cache=.phpcs-cache --colors src tests",
"phpcsfix": "vendor/bin/phpcbf --standard=ruleset.xml --extensions=php --cache=.phpcs-cache src tests",
"binupdate": "@composer bin all update --ansi",
"bininstall": "@composer bin all install --ansi"
"phpstan": "vendor-bin/phpstan/vendor/bin/phpstan analyse --configuration=tools/phpstan.neon",
"phpstan_full": "rm -Rf tools/cache/phpstan && vendor-bin/phpstan/vendor/bin/phpstan analyse --configuration=tools/phpstan.neon",
"phpstan_base": "vendor-bin/phpstan/vendor/bin/phpstan analyse --configuration=tools/phpstan.neon --generate-baseline=tools/phpstan-baseline.php",
"psalm": "vendor-bin/psalm/vendor/bin/psalm --config=tools/psalm.xml --show-info=false",
"psalm_full": "vendor-bin/psalm/vendor/bin/psalm --config=tools/psalm.xml --clear-cache && vendor-bin/psalm/vendor/bin/psalm --config=tools/psalm.xml --show-info=false",
"psalm_base": "vendor-bin/psalm/vendor/bin/psalm --config=tools/psalm.xml --set-baseline=tools/psalm-baseline.xml",
"phpunit": "vendor/bin/phpunit --configuration=tools/phpunit.xml.dist --cache-result-file=tools/cache/.phpunit.result.cache --colors=always --testsuite=unit",
"phpunit_clover": "vendor/bin/phpunit --configuration=tools/phpunit.xml.dist --cache-result-file=tools/cache/.phpunit.result.cache --coverage-text --coverage-clover build/logs/clover.xml",
"coverage": "XDEBUG_MODE=coverage vendor/bin/phpunit --configuration=tools/phpunit.xml.dist --coverage-html=tests/_reports",
"phpcs": "vendor-bin/phpcs/vendor/bin/phpcs --standard=tools/ruleset.xml --extensions=php --cache=tools/cache/.phpcs-cache --colors src tests",
"phpcs_diff": "vendor-bin/phpcs/vendor/bin/phpcs -s --standard=tools/ruleset.xml --extensions=php --cache=tools/cache/.phpcs-cache --colors src tests",
"phpcs_fix": "vendor-bin/phpcs/vendor/bin/phpcbf --standard=tools/ruleset.xml --extensions=php --cache=tools/cache/.phpcs-cache --colors src tests",
"binupdate": "bin/vendorbin update",
"binoutdated": "bin/vendorbin outdated"
}
}
3 changes: 1 addition & 2 deletions docker/compose-coverage.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
version: "3.7"
services:
squirrel_queries_coverage:
image: thecodingmachine/php:8.1-v4-cli
image: thecodingmachine/php:8.3-v4-cli
container_name: squirrel_queries_coverage
tty: true
working_dir: /usr/src/app
Expand Down
Loading

0 comments on commit 53e9641

Please sign in to comment.