First Commit

This commit is contained in:
marco
2026-09-04 14:54:13 -03:00
commit 9c54bd3ac7
4914 changed files with 789004 additions and 0 deletions
+13
View File
@@ -0,0 +1,13 @@
# editorconfig.org
root = true
[*]
charset = utf-8
indent_style = tab
trim_trailing_whitespace = true
end_of_line = lf
insert_final_newline = true
[*.md]
indent_style = space
indent_size = 4
+6
View File
@@ -0,0 +1,6 @@
# IntelliJ
.idea/
# Composer
vendor/
composer.phar
Vendored Executable
+21
View File
@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) delight.im (https://www.delight.im/)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+316
View File
@@ -0,0 +1,316 @@
# PHP-DB
Safe and convenient SQL database access in a driver-agnostic way
## Features
* Convenient interface
* Write less code for the same tasks
* Safe against SQL injections through the use of prepared statements
* Data returned by the server has the correct native types (i.e. no stringified fields)
* Driver-agnostic support for multiple database systems
## Requirements
* PHP 5.6.0+
* PDO extension
## Installation
1. Include the library via Composer [[?]](https://github.com/delight-im/Knowledge/blob/master/Composer%20(PHP).md):
```
$ composer require delight-im/db
```
1. Include the Composer autoloader:
```php
require __DIR__ . '/vendor/autoload.php';
```
## Usage
### Connecting to a database
* From a dynamic database configuration:
```php
$dataSource = new \Delight\Db\PdoDataSource('mysql'); // see "Available drivers for database systems" below
$dataSource->setHostname('localhost');
$dataSource->setPort(3306);
$dataSource->setDatabaseName('my-database');
$dataSource->setCharset('utf8mb4');
$dataSource->setUsername('my-username');
$dataSource->setPassword('my-password');
$db = \Delight\Db\PdoDatabase::fromDataSource($dataSource);
```
This is the most convenient way to establish the database connection since the correct DSN will be created for you automatically. You can use the various setters with a fluent interface in order to set up your configuration.
You can leave out any options that you don't need and check the `PdoDataSource` class for other available setters.
Connections will be created lazily, i.e. they won't consume any resources before they're actually used. All that happens automatically.
* From a DSN (data source name):
```php
$db = \Delight\Db\PdoDatabase::fromDsn(
new \Delight\Db\PdoDsn(
'mysql:dbname=my-database;host=localhost',
'my-username',
'my-password'
)
);
```
The parameters of the `PdoDsn` constructor are exactly the same as the parameters to the `PDO` constructor. But if you don't need another `PDO` instance, it's more efficient to go without `PDO` and create this library's instance from a DSN directly.
Connections will be created lazily, i.e. they won't consume any resources before they're actually used.
* Using an existing `PDO` instance:
```php
// $pdo = new PDO('mysql:dbname=my-database;host=localhost;charset=utf8mb4', 'my-username', 'my-password');
$db = \Delight\Db\PdoDatabase::fromPdo($pdo);
```
This will not cause another database connection to be created but instead re-use the existing connection from the `PDO` instance that you provided.
Furthermore, you can benefit from the strong guarantees about this library's behavior and from its convenience while still being able to use your existing `PDO` instance without any side effects.
Just pass `true` as the second argument to the `fromPdo` method to completely preserve the original state of your `PDO` instance:
```php
$db = \Delight\Db\PdoDatabase::fromPdo($pdo, true);
```
#### Available drivers for database systems
```php
\Delight\Db\PdoDataSource::DRIVER_NAME_MYSQL;
\Delight\Db\PdoDataSource::DRIVER_NAME_POSTGRESQL;
\Delight\Db\PdoDataSource::DRIVER_NAME_SQLITE;
```
### Selecting data
When selecting data, you get the desired results with just one method call. And, of course, you can easily choose whether you want to get multiple rows, a single row, a single value only, or a single column from all rows.
```php
$rows = $db->select('SELECT id, name FROM books');
// or
$rows = $db->select(
'SELECT name, year FROM books WHERE author = ? ORDER BY copies DESC LIMIT 0, 10',
[ 'Charles Dickens' ]
);
// or
$row = $db->selectRow(
'SELECT author, year FROM books WHERE author <> ? ORDER BY year ASC LIMIT 0, 1',
[ 'Miguel de Cervantes' ]
);
// or
$value = $db->selectValue(
'SELECT year FROM books WHERE name <> ? AND name <> ? ORDER BY year DESC LIMIT 0, 1',
[
'Tale of Two Cities',
'Alice in Wonderland'
]
);
// or
$column = $db->selectColumn(
'SELECT author FROM books ORDER BY copies DESC LIMIT ?, ?',
[
0,
3
]
);
```
### Inserting data
For simple insertions, you can use a convenient shorthand:
```php
$db->insert(
'books',
[
// set
'name' => 'Don Quixote',
'author' => 'Miguel de Cervantes',
'year' => 1612
]
);
```
Does your table have automatically generated primary IDs? Access to these inserted IDs is available with another short method call. The sequence name is only required for *some* database drivers, e.g. PostgreSQL.
```php
$newId = $db->getLastInsertId();
// or
$newId = $db->getLastInsertId('my-sequence-name');
```
If you need to execute more advanced statements, please refer to section "Executing statements" below.
### Updating data
For simple updates, you can use a convenient shorthand as well:
```php
$db->update(
'books',
[
// set
'author' => 'J. K. Rowling',
'copies' => 2
],
[
// where
'name' => "Harry Potter and the Philosopher's Stone"
]
);
```
Do you want to know how many rows have been updated by this operation? Just grab the return value from the `update` method call.
If you need to execute more advanced statements, please refer to section "Executing statements" below.
### Deleting data
Again, for simple deletions, you can use a convenient shorthand:
```php
$db->delete(
'books',
[
// where
'author' => 'C. S. Lewis',
'year' => 1949
]
);
```
The return value from the `delete` method call will tell you how many rows have been deleted by this operation.
If you need to execute more advanced statements, please refer to section "Executing statements" below.
### Executing statements
You can execute any arbitrary SQL statements as shown in the following examples:
```php
$db->exec(
'INSERT INTO books (name, author, year) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE copies = copies + 1',
[
"Harry Potter and the Philosopher's Stone",
'J. K. Rowling',
1997
]
);
// or
$db->exec(
"UPDATE books SET name = CONCAT(LEFT(name, 5), ' in ', RIGHT(name, 10)) WHERE year >= ? AND year < ?",
[
1860,
1890
]
);
```
For every statement that you execute, the return value will be the number of rows affected.
If the statement that you're executing is an `INSERT`, you can get the inserted IDs via the `getLastInsertId` method, again.
### Transactions
Transaction controls are as easy as they should be:
```php
$db->beginTransaction();
// and later
$db->commit();
// or
$db->rollBack();
// and optionally
$active = $db->isTransactionActive();
```
### Error handling
The methods from this library do not return any error codes. You will be informed about any problem via an exception.
### Input escaping
There's no need for you to escape any input manually. Just use the methods as shown above by using placeholders and passing the data separately. All input will be escaped for you automatically.
### Performance profiling
In order to monitor query performance during development, you can enable performance profiling:
```php
$db->setProfiler(new \Delight\Db\SimpleProfiler());
```
Whenever you want to see the analyzed queries or store them in a log file, you can get all measurements recorded by the profiler as an array:
```php
$db->getProfiler()->getMeasurements();
```
Typically, you'll want to sort the measurements before retrieving them, so that the longest-running queries are listed first:
```php
$db->getProfiler()->sort();
```
### Server and client information
In order to retrieve some information about the database server that you're connected to or about the database client used by PHP, you can use one of the following methods:
```php
$db->getDriverName();
// e.g. 'MySQL'
$db->getServerInfo();
// e.g. 'Uptime: 82196 Threads: 1 Questions: 2840 Slow queries: 0 Opens: 23 Flush tables: 1 Open tables: 33 Queries per second avg: 0.736'
$db->getServerVersion();
// e.g. '5.5.5-10.1.13-MariaDB'
$db->getClientVersion();
// e.g. 'mysqlnd 5.0.1-dev'
```
### Listeners
#### Connection established
```php
$db->addOnConnectListener(function (\Delight\Db\PdoDatabase $db) {
// do something
});
```
## Contributing
All contributions are welcome! If you wish to contribute, please create an issue first so that your feature, problem or question can be discussed.
## License
This project is licensed under the terms of the [MIT License](https://opensource.org/licenses/MIT).
+17
View File
@@ -0,0 +1,17 @@
{
"name": "delight-im/db",
"description": "Safe and convenient SQL database access in a driver-agnostic way",
"require": {
"php": ">=5.6.0",
"ext-pdo": "*"
},
"type": "library",
"keywords": [ "database", "sql", "pdo", "mysql", "postgresql", "pgsql", "sqlite" ],
"homepage": "https://github.com/delight-im/PHP-DB",
"license": "MIT",
"autoload": {
"psr-4": {
"Delight\\Db\\": "src/"
}
}
}
Generated Vendored Executable
+21
View File
@@ -0,0 +1,21 @@
{
"_readme": [
"This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
"This file is @generated automatically"
],
"hash": "f40a675f76ed738a86242507aefec136",
"content-hash": "25f1b8ee1b78ffbaba4f8cef1a5f0dfa",
"packages": [],
"packages-dev": [],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": [],
"prefer-stable": false,
"prefer-lowest": false,
"platform": {
"php": ">=5.6.0",
"ext-pdo": "*"
},
"platform-dev": []
}
+21
View File
@@ -0,0 +1,21 @@
<?php
/*
* PHP-DB (https://github.com/delight-im/PHP-DB)
* Copyright (c) delight.im (https://www.delight.im/)
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
*/
namespace Delight\Db;
/** Description of a data source */
interface DataSource {
/**
* Converts this instance to a DSN
*
* @return Dsn
*/
public function toDsn();
}
+244
View File
@@ -0,0 +1,244 @@
<?php
/*
* PHP-DB (https://github.com/delight-im/PHP-DB)
* Copyright (c) delight.im (https://www.delight.im/)
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
*/
namespace Delight\Db;
use Delight\Db\Throwable\BeginTransactionFailureException;
use Delight\Db\Throwable\CommitTransactionFailureException;
use Delight\Db\Throwable\IntegrityConstraintViolationException;
use Delight\Db\Throwable\RollBackTransactionFailureException;
use Delight\Db\Throwable\TransactionFailureException;
/** Safe and convenient SQL database access in a driver-agnostic way */
interface Database {
/**
* Selects from the database using the specified query and returns all rows and columns
*
* You should not include any dynamic input in the query
*
* Instead, pass `?` characters (without any quotes) as placeholders and pass the actual values in the second argument
*
* @param string $query the SQL query to select with
* @param array|null $bindValues (optional) the values to bind as replacements for the `?` characters in the query
* @return array|null the rows and columns returned by the server or `null` if no results have been found
*/
public function select($query, array $bindValues = null);
/**
* Selects from the database using the specified query and returns the value of the first column in the first row
*
* You should not include any dynamic input in the query
*
* Instead, pass `?` characters (without any quotes) as placeholders and pass the actual values in the second argument
*
* @param string $query the SQL query to select with
* @param array|null $bindValues (optional) the values to bind as replacements for the `?` characters in the query
* @return mixed|null the value of the first column in the first row returned by the server or `null` if no results have been found
*/
public function selectValue($query, array $bindValues = null);
/**
* Selects from the database using the specified query and returns the first row
*
* You should not include any dynamic input in the query
*
* Instead, pass `?` characters (without any quotes) as placeholders and pass the actual values in the second argument
*
* @param string $query the SQL query to select with
* @param array|null $bindValues (optional) the values to bind as replacements for the `?` characters in the query
* @return array|null the first row returned by the server or `null` if no results have been found
*/
public function selectRow($query, array $bindValues = null);
/**
* Selects from the database using the specified query and returns the first column
*
* You should not include any dynamic input in the query
*
* Instead, pass `?` characters (without any quotes) as placeholders and pass the actual values in the second argument
*
* @param string $query the SQL query to select with
* @param array|null $bindValues (optional) the values to bind as replacements for the `?` characters in the query
* @return array|null the first column returned by the server or `null` if no results have been found
*/
public function selectColumn($query, array $bindValues = null);
/**
* Inserts the given mapping between columns and values into the specified table
*
* @param string|string[] $tableName the name of the table to insert into (or an array of components of the qualified name)
* @param array $insertMappings the mappings between columns and values to insert
* @return int the number of inserted rows
* @throws IntegrityConstraintViolationException
*/
public function insert($tableName, array $insertMappings);
/**
* Updates the specified table with the given mappings between columns and values
*
* @param string|string[] $tableName the name of the table to update (or an array of components of the qualified name)
* @param array $updateMappings the mappings between columns and values to update
* @param array $whereMappings the mappings between columns and values to filter by
* @return int the number of updated rows
* @throws IntegrityConstraintViolationException
*/
public function update($tableName, array $updateMappings, array $whereMappings);
/**
* Deletes from the specified table where the given mappings between columns and values are found
*
* @param string|string[] $tableName the name of the table to delete from (or an array of components of the qualified name)
* @param array $whereMappings the mappings between columns and values to filter by
* @return int the number of deleted rows
*/
public function delete($tableName, array $whereMappings);
/**
* Executes an arbitrary statement and returns the number of affected rows
*
* This is especially useful for custom `INSERT`, `UPDATE` or `DELETE` statements
*
* You should not include any dynamic input in the statement
*
* Instead, pass `?` characters (without any quotes) as placeholders and pass the actual values in the second argument
*
* @param string $statement the SQL statement to execute
* @param array|null $bindValues (optional) the values to bind as replacements for the `?` characters in the statement
* @return int the number of affected rows
* @throws IntegrityConstraintViolationException
* @throws TransactionFailureException
*/
public function exec($statement, array $bindValues = null);
/**
* Returns the ID of the last row that has been inserted or returns the last value from the specified sequence
*
* @param string|null $sequenceName (optional) the name of the sequence that the ID should be returned from
* @return string|int the ID or the number from the sequence
*/
public function getLastInsertId($sequenceName = null);
/**
* Starts a new transaction and turns off auto-commit mode
*
* Changes won't take effect until the transaction is either finished via `commit` or cancelled via `rollBack`
*
* @throws BeginTransactionFailureException
*/
public function beginTransaction();
/**
* Alias of `beginTransaction`
*
* @throws BeginTransactionFailureException
*/
public function startTransaction();
/**
* Returns whether a transaction is currently active
*
* @return bool
*/
public function isTransactionActive();
/**
* Finishes an existing transaction and turns on auto-commit mode again
*
* This makes all changes since the last commit or roll-back permanent
*
* @throws CommitTransactionFailureException
*/
public function commit();
/**
* Cancels an existing transaction and turns on auto-commit mode again
*
* This discards all changes since the last commit or roll-back
*
* @throws RollBackTransactionFailureException
*/
public function rollBack();
/**
* Returns the performance profiler currently used by this instance (if any)
*
* @return Profiler|null
*/
public function getProfiler();
/**
* Sets the performance profiler used by this instance
*
* This should only be used during development and not in production
*
* @param Profiler|null $profiler the profiler instance or `null` to disable profiling again
* @return static this instance for chaining
*/
public function setProfiler(Profiler $profiler = null);
/**
* Returns the name of the driver that is used for the current connection
*
* @return string
*/
public function getDriverName();
/**
* Quotes an identifier (e.g. a table name or column reference)
*
* This allows for special characters and reserved keywords to be used in identifiers
*
* There is usually no need to call this method
*
* Identifiers should not be set from untrusted user input and in most cases not even from dynamic expressions
*
* @param string $identifier the identifier to quote
* @return string the quoted identifier
*/
public function quoteIdentifier($identifier);
/**
* Quotes a table name
*
* This allows for special characters and reserved keywords to be used in table names
*
* There is usually no need to call this method
*
* Table names should not be set from untrusted user input and in most cases not even from dynamic expressions
*
* @param string|string[] $tableName the table name to quote (or an array of components of the qualified name)
* @return string the quoted table name
*/
public function quoteTableName($tableName);
/**
* Quotes a literal value (e.g. a string to insert or to use in a comparison) or an array thereof
*
* This allows for special characters to be used in literal values
*
* There is usually no need to call this method
*
* You should always use placeholders for literal values and pass the actual values to bind separately
*
* @param string $literal the literal value to quote
* @return string the quoted literal value
*/
public function quoteLiteral($literal);
/**
* Adds a listener that will execute as soon as the database connection has been established
*
* If the database connection has already been active before, the listener will execute immediately
*
* @param callable $onConnectListener the callback to execute
* @return static this instance for chaining
*/
public function addOnConnectListener(callable $onConnectListener);
}
+12
View File
@@ -0,0 +1,12 @@
<?php
/*
* PHP-DB (https://github.com/delight-im/PHP-DB)
* Copyright (c) delight.im (https://www.delight.im/)
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
*/
namespace Delight\Db;
/** Data source name or connection string describing how to connect to a data source */
interface Dsn {}
+92
View File
@@ -0,0 +1,92 @@
<?php
/*
* PHP-DB (https://github.com/delight-im/PHP-DB)
* Copyright (c) delight.im (https://www.delight.im/)
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
*/
namespace Delight\Db;
use PDOException;
use Delight\Db\Throwable\DatabaseNotFoundError;
use Delight\Db\Throwable\Error;
use Delight\Db\Throwable\Exception;
use Delight\Db\Throwable\IntegrityConstraintViolationException;
use Delight\Db\Throwable\NoDatabaseSelectedError;
use Delight\Db\Throwable\SyntaxError;
use Delight\Db\Throwable\TableNotFoundError;
use Delight\Db\Throwable\UnknownColumnError;
use Delight\Db\Throwable\WrongCredentialsError;
/**
* Handles, processes and re-throws exceptions and errors
*
* For more information about possible exceptions and errors, see:
*
* https://en.wikibooks.org/wiki/Structured_Query_Language/SQLSTATE
*
* https://dev.mysql.com/doc/refman/5.5/en/error-messages-server.html
*/
final class ErrorHandler {
/**
* Handles the specified exception thrown by PDO and tries to throw a more specific exception or error instead
*
* @param PDOException $e the exception thrown by PDO
* @throws Exception
* @throws Error
*/
public static function rethrow(PDOException $e) {
// the 2-character class of the error (if any) has the highest priority
$errorClass = null;
// the 3-character subclass of the error (if any) has a medium priority
$errorSubClass = null;
// the full error code itself has the lowest priority
$error = null;
// if an error code is available
if (!empty($e->getCode())) {
// remember the error code
$error = $e->getCode();
// if the error code is an "SQLSTATE" error
if (strlen($e->getCode()) === 5) {
// remember the class as well
$errorClass = substr($e->getCode(), 0, 2);
// and remember the subclass
$errorSubClass = substr($e->getCode(), 2);
}
}
if ($errorClass === '3D') {
throw new NoDatabaseSelectedError($e->getMessage());
}
elseif ($errorClass === '23') {
throw new IntegrityConstraintViolationException($e->getMessage());
}
elseif ($errorClass === '42') {
if ($errorSubClass === 'S02') {
throw new TableNotFoundError($e->getMessage());
}
elseif ($errorSubClass === 'S22') {
throw new UnknownColumnError($e->getMessage());
}
else {
throw new SyntaxError($e->getMessage());
}
}
else {
if ($error === 1044) {
throw new WrongCredentialsError($e->getMessage());
}
elseif ($error === 1049) {
throw new DatabaseNotFoundError($e->getMessage());
}
else {
throw new Error($e->getMessage());
}
}
}
}
+42
View File
@@ -0,0 +1,42 @@
<?php
/*
* PHP-DB (https://github.com/delight-im/PHP-DB)
* Copyright (c) delight.im (https://www.delight.im/)
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
*/
namespace Delight\Db;
/** Individual measurement of a profiler that monitors performance */
interface Measurement {
/**
* Returns the duration of the operation that was monitored
*
* @return float the duration in milliseconds
*/
public function getDuration();
/**
* Returns the (parameterized) SQL query or statement that was used during the operation
*
* @return string
*/
public function getSql();
/**
* Returns any values that might have been bound to the statement
*
* @return array|null
*/
public function getBoundValues();
/**
* Returns the trace that shows the path taken through the application until the operation was executed
*
* @return array
*/
public function getTrace();
}
+289
View File
@@ -0,0 +1,289 @@
<?php
/*
* PHP-DB (https://github.com/delight-im/PHP-DB)
* Copyright (c) delight.im (https://www.delight.im/)
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
*/
namespace Delight\Db;
/** Description of a data source for use with PHP's built-in PDO */
final class PdoDataSource implements DataSource {
/** The driver name for MySQL */
const DRIVER_NAME_MYSQL = 'mysql';
/** The driver name for PostgreSQL */
const DRIVER_NAME_POSTGRESQL = 'pgsql';
/** The driver name for SQLite */
const DRIVER_NAME_SQLITE = 'sqlite';
/** The driver name for Oracle */
const DRIVER_NAME_ORACLE = 'oci';
/** Hostname for the virtual loopback interface */
const HOST_LOOPBACK_NAME = 'localhost';
/** IPv4 address for the virtual loopback interface */
const HOST_LOOPBACK_IP = '127.0.0.1';
/** The default hostname */
const HOST_DEFAULT = self::HOST_LOOPBACK_NAME;
/** @var string the name of the driver, e.g. `mysql` or `pgsql` */
private $driverName;
/** @var string|null the hostname where the database can be accessed, e.g. `db.example.com` */
private $hostname;
/** @var int|null the port number to use at the given host, e.g. `3306` or `5432` */
private $port;
/** @var string|null the UNIX socket to use, e.g. `/tmp/db.sock` */
private $unixSocket;
/** @var bool|null whether to keep the database in memory only */
private $memory;
/** @var string|null the path to the file where the database can be accessed on disk, e.g. `/opt/databases/mydb.ext` */
private $filePath;
/** @var string|null the name of the database, e.g. `my_application` */
private $databaseName;
/** @var string|null the character encoding of the database, e.g. `utf8` */
private $charset;
/** @var string|null the name of a user that can access the database */
private $username;
/** @var string|null the password corresponding to the username */
private $password;
/**
* Constructor
*
* @param string $driverName the name of the driver, e.g. `mysql` or `pgsql`
*/
public function __construct($driverName) {
$this->driverName = (string) $driverName;
$this->hostname = self::HOST_DEFAULT;
$this->port = self::suggestPortFromDriverName($driverName);
$this->unixSocket = null;
$this->memory = null;
$this->filePath = null;
$this->databaseName = null;
$this->charset = self::suggestCharsetFromDriverName($driverName);
$this->username = null;
$this->password = null;
}
/**
* Sets the hostname
*
* @param string|null $hostname the hostname where the database can be accessed, e.g. `db.example.com`
* @return static this instance for chaining
*/
public function setHostname($hostname = null) {
$this->hostname = $hostname !== null ? (string) $hostname : null;
return $this;
}
/**
* Sets the port number
*
* @param int|null $port the port number to use at the given host, e.g. `3306` or `5432`
* @return static this instance for chaining
*/
public function setPort($port = null) {
$this->port = $port !== null ? (int) $port : null;
return $this;
}
/**
* Sets the unix socket
*
* @param string|null $unixSocket the UNIX socket to use, e.g. `/tmp/db.sock`
* @return static this instance for chaining
*/
public function setUnixSocket($unixSocket = null) {
$this->unixSocket = $unixSocket !== null ? (string) $unixSocket : null;
return $this;
}
/**
* Sets whether to keep the database in memory only
*
* @param bool|null $memory whether to keep the database in memory only
* @return static this instance for chaining
*/
public function setMemory($memory = null) {
$this->memory = $memory !== null ? (bool) $memory : null;
return $this;
}
/**
* Sets the file path
*
* @param string|null $filePath the path to the file where the database can be accessed on disk, e.g. `/opt/databases/mydb.ext`
* @return static this instance for chaining
*/
public function setFilePath($filePath = null) {
$this->filePath = $filePath !== null ? (string) $filePath : null;
return $this;
}
/**
* Sets the database name
*
* @param string|null $databaseName the name of the database, e.g. `my_application`
* @return static this instance for chaining
*/
public function setDatabaseName($databaseName = null) {
$this->databaseName = $databaseName !== null ? (string) $databaseName : null;
return $this;
}
/**
* Sets the charset
*
* @param string|null $charset the character encoding, e.g. `utf8`
* @return static this instance for chaining
*/
public function setCharset($charset = null) {
$this->charset = $charset !== null ? (string) $charset : null;
return $this;
}
/**
* Sets the username
*
* @param string|null $username the name of a user that can access the database
* @return static this instance for chaining
*/
public function setUsername($username = null) {
$this->username = $username;
return $this;
}
/**
* Sets the password
*
* @param string|null $password the password corresponding to the username
* @return static this instance for chaining
*/
public function setPassword($password = null) {
$this->password = $password;
return $this;
}
public function toDsn() {
$components = [];
if (isset($this->hostname)) {
if ($this->driverName !== self::DRIVER_NAME_ORACLE) {
$hostname = $this->hostname;
// if we're trying to connect to a local database
if ($this->hostname === self::HOST_LOOPBACK_NAME) {
// if we're using a non-standard port
if (isset($this->port) && $this->port !== self::suggestPortFromDriverName($this->driverName)) {
// force usage of TCP over UNIX sockets for the port change to take effect
$hostname = self::HOST_LOOPBACK_IP;
}
}
$components[] = 'host='.$hostname;
}
}
if (isset($this->port)) {
if ($this->driverName !== self::DRIVER_NAME_ORACLE) {
$components[] = 'port='.$this->port;
}
}
if (isset($this->unixSocket)) {
$components[] = 'unix_socket='.$this->unixSocket;
}
if (isset($this->memory)) {
if ($this->memory === true) {
$components[] = ':memory:';
}
}
if (isset($this->filePath)) {
$components[] = $this->filePath;
}
if (isset($this->databaseName)) {
if ($this->driverName === self::DRIVER_NAME_ORACLE) {
$oracleLocation = [];
if (isset($this->hostname)) {
$oracleLocation[] = $this->hostname;
}
if (isset($this->port)) {
$oracleLocation[] = $this->port;
}
if (count($oracleLocation) > 0) {
$components[] = 'dbname=//'.implode(':', $oracleLocation).'/'.$this->databaseName;
}
else {
$components[] = 'dbname='.$this->databaseName;
}
}
else {
$components[] = 'dbname='.$this->databaseName;
}
}
if (isset($this->charset)) {
if ($this->driverName === self::DRIVER_NAME_POSTGRESQL) {
$components[] = 'client_encoding='.$this->charset;
}
else {
$components[] = 'charset='.$this->charset;
}
}
if (isset($this->username)) {
$components[] = 'user='.$this->username;
}
if (isset($this->password)) {
$components[] = 'password='.$this->password;
}
$dsnStr = $this->driverName . ':' . implode(';', $components);
return new PdoDsn($dsnStr, $this->username, $this->password);
}
/**
* Suggests a default charset for a given driver
*
* @param string $driverName the name of the driver
* @return string|null the suggested charset
*/
private static function suggestCharsetFromDriverName($driverName) {
switch ($driverName) {
case self::DRIVER_NAME_MYSQL: return 'utf8mb4';
case self::DRIVER_NAME_POSTGRESQL: return 'UTF8';
default: return null;
}
}
/**
* Suggests a default port number for a given driver
*
* @param string $driverName the name of the driver
* @return int|null the suggested port number
*/
private static function suggestPortFromDriverName($driverName) {
switch ($driverName) {
case self::DRIVER_NAME_MYSQL: return 3306;
case self::DRIVER_NAME_POSTGRESQL: return 5432;
default: return null;
}
}
}
+615
View File
@@ -0,0 +1,615 @@
<?php
/*
* PHP-DB (https://github.com/delight-im/PHP-DB)
* Copyright (c) delight.im (https://www.delight.im/)
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
*/
namespace Delight\Db;
use PDO;
use PDOException;
use PDOStatement;
use Delight\Db\Throwable\BeginTransactionFailureException;
use Delight\Db\Throwable\CommitTransactionFailureException;
use Delight\Db\Throwable\EmptyValueListError;
use Delight\Db\Throwable\EmptyWhereClauseError;
use Delight\Db\Throwable\RollBackTransactionFailureException;
/** Database access using PHP's built-in PDO */
final class PdoDatabase implements Database {
/** @var array|null the old connection attributes to restore during denormalization */
private $previousAttributes;
/** @var array|null the new connection attributes to apply during normalization */
private $attributes;
/** @var PDO|null the connection that this class operates on (may be lazily loaded) */
private $pdo;
/** @var PdoDsn|null the PDO-specific DSN that may be used to establish the connection */
private $dsn;
/** @var string|null the name of the driver that is used for the current connection (may be lazily loaded) */
private $driverName;
/** @var Profiler|null the profiler that is used to analyze query performance during development */
private $profiler;
/** @var callable[] the list of pending callbacks to execute when the connection has been established */
private $onConnectListeners;
/**
* Constructor
*
* This is private to prevent direct usage
*
* Call one of the static factory methods instead
*
* @param PDO|null $pdoInstance (optional) the connection that this class operates on
* @param PdoDsn|null $pdoDsn (optional) the PDO-specific DSN that may be used to establish the connection
* @param bool|null (optional) $preserveOldState whether the old state of the connection should be preserved
*/
private function __construct(PDO $pdoInstance = null, PdoDsn $pdoDsn = null, $preserveOldState = null) {
// if the old state of the connection must be stored somewhere
if ($preserveOldState) {
// prepare an array for that task
$this->previousAttributes = [];
}
// if the old state of the connection doesn't need to be tracked
else {
$this->previousAttributes = null;
}
// track the new attributes that should be applied during normalization
$this->attributes = [
// set the error mode for this connection to throw exceptions
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
// set the default fetch mode for this connection to use associative arrays
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
// prefer native prepared statements over emulated ones
PDO::ATTR_EMULATE_PREPARES => false,
// use lowercase and uppercase as returned by the server
PDO::ATTR_CASE => PDO::CASE_NATURAL,
// don't convert numeric values to strings when fetching data
PDO::ATTR_STRINGIFY_FETCHES => false,
// keep `null` values and empty strings as returned by the server
PDO::ATTR_ORACLE_NULLS => PDO::NULL_NATURAL
];
$this->pdo = $pdoInstance;
$this->dsn = $pdoDsn;
$this->profiler = null;
$this->onConnectListeners = [];
}
/**
* Creates and returns a new instance from an existing PDO instance
*
* @param PDO $pdoInstance the PDO instance to use
* @param bool|null (optional) $preserveOldState whether the old state of the PDO instance should be preserved
* @return static the new instance
*/
public static function fromPdo(PDO $pdoInstance, $preserveOldState = null) {
return new static($pdoInstance, null, $preserveOldState);
}
/**
* Creates and returns a new instance from a PDO-specific DSN
*
* The connection will be lazily loaded, i.e. it won't be established before it's actually needed
*
* @param PdoDsn $pdoDsn the PDO-specific DSN to use
* @return static the new instance
*/
public static function fromDsn(PdoDsn $pdoDsn) {
return new static(null, $pdoDsn);
}
/**
* Creates and returns a new instance from a data source described for use with PDO
*
* The connection will be lazily loaded, i.e. it won't be established before it's actually needed
*
* @param PdoDataSource $pdoDataSource the data source to use
* @return static the new instance
*/
public static function fromDataSource(PdoDataSource $pdoDataSource) {
return new static(null, $pdoDataSource->toDsn());
}
public function select($query, array $bindValues = null) {
return $this->selectInternal(function ($stmt) {
/** @var PDOStatement $stmt */
return $stmt->fetchAll();
}, $query, $bindValues);
}
public function selectValue($query, array $bindValues = null) {
return $this->selectInternal(function ($stmt) {
/** @var PDOStatement $stmt */
return $stmt->fetchColumn(0);
}, $query, $bindValues);
}
public function selectRow($query, array $bindValues = null) {
return $this->selectInternal(function ($stmt) {
/** @var PDOStatement $stmt */
return $stmt->fetch();
}, $query, $bindValues);
}
public function selectColumn($query, array $bindValues = null) {
return $this->selectInternal(function ($stmt) {
/** @var PDOStatement $stmt */
return $stmt->fetchAll(PDO::FETCH_COLUMN, 0);
}, $query, $bindValues);
}
public function insert($tableName, array $insertMappings) {
// if no values have been provided that could be inserted
if (empty($insertMappings)) {
// we cannot perform an insert here
throw new EmptyValueListError();
}
// escape the table name
$tableName = $this->quoteTableName($tableName);
// get the column names
$columnNames = array_keys($insertMappings);
// escape the column names
$columnNames = array_map([ $this, 'quoteIdentifier' ], $columnNames);
// build the column list
$columnList = implode(', ', $columnNames);
// prepare the values (which are placeholders only)
$values = array_fill(0, count($insertMappings), '?');
// build the value list
$placeholderList = implode(', ', $values);
// and finally build the full statement (still using placeholders)
$statement = 'INSERT INTO '.$tableName.' ('.$columnList.') VALUES ('.$placeholderList.');';
// execute the (parameterized) statement and supply the values to be bound to it
return $this->exec($statement, array_values($insertMappings));
}
public function update($tableName, array $updateMappings, array $whereMappings) {
// if no values have been provided that we could update to
if (empty($updateMappings)) {
// we cannot perform an update here
throw new EmptyValueListError();
}
// if no values have been provided that we could filter by (which is possible but dangerous)
if (empty($whereMappings)) {
// we should not perform an update here
throw new EmptyWhereClauseError();
}
// escape the table name
$tableName = $this->quoteTableName($tableName);
// prepare a list for the values to be bound (both by the list of new values and by the conditions)
$bindValues = [];
// prepare a list for the individual directives of the `SET` clause
$setDirectives = [];
// for each mapping of a column name to its respective new value
foreach ($updateMappings as $updateColumn => $updateValue) {
// create an individual directive with the column name and a placeholder for the value
$setDirectives[] = $this->quoteIdentifier($updateColumn) . ' = ?';
// and remember which value to bind here
$bindValues[] = $updateValue;
}
// prepare a list for the individual predicates of the `WHERE` clause
$wherePredicates = [];
// for each mapping of a column name to its respective value to filter by
foreach ($whereMappings as $whereColumn => $whereValue) {
// create an individual predicate with the column name and a placeholder for the value
$wherePredicates[] = $this->quoteIdentifier($whereColumn) . ' = ?';
// and remember which value to bind here
$bindValues[] = $whereValue;
}
// build the full statement (still using placeholders)
$statement = 'UPDATE '.$tableName.' SET '.implode(', ', $setDirectives).' WHERE '.implode(' AND ', $wherePredicates).';';
// execute the (parameterized) statement and supply the values to be bound to it
return $this->exec($statement, $bindValues);
}
public function delete($tableName, array $whereMappings) {
// if no values have been provided that we could filter by (which is possible but dangerous)
if (empty($whereMappings)) {
// we should not perform a deletion here
throw new EmptyWhereClauseError();
}
// escape the table name
$tableName = $this->quoteTableName($tableName);
// prepare a list for the values to be bound by the conditions
$bindValues = [];
// prepare a list for the individual predicates of the `WHERE` clause
$wherePredicates = [];
// for each mapping of a column name to its respective value to filter by
foreach ($whereMappings as $whereColumn => $whereValue) {
// create an individual predicate with the column name and a placeholder for the value
$wherePredicates[] = $this->quoteIdentifier($whereColumn) . ' = ?';
// and remember which value to bind here
$bindValues[] = $whereValue;
}
// build the full statement (still using placeholders)
$statement = 'DELETE FROM '.$tableName.' WHERE '.implode(' AND ', $wherePredicates).';';
// execute the (parameterized) statement and supply the values to be bound to it
return $this->exec($statement, $bindValues);
}
public function exec($statement, array $bindValues = null) {
$this->normalizeConnection();
try {
// create a prepared statement from the supplied SQL string
$stmt = $this->pdo->prepare($statement);
}
catch (PDOException $e) {
ErrorHandler::rethrow($e);
}
// if a performance profiler has been defined
if (isset($this->profiler)) {
$this->profiler->beginMeasurement();
}
/** @var PDOStatement $stmt */
try {
// bind the supplied values to the statement and execute it
$stmt->execute($bindValues);
}
catch (PDOException $e) {
ErrorHandler::rethrow($e);
}
// if a performance profiler has been defined
if (isset($this->profiler)) {
$this->profiler->endMeasurement($statement, $bindValues);
}
// get the number of rows affected by this operation
$affectedRows = $stmt->rowCount();
$this->denormalizeConnection();
return $affectedRows;
}
public function getLastInsertId($sequenceName = null) {
$this->normalizeConnection();
$id = $this->pdo->lastInsertId($sequenceName);
$this->denormalizeConnection();
return $id;
}
public function beginTransaction() {
$this->normalizeConnection();
try {
$success = $this->pdo->beginTransaction();
}
catch (PDOException $e) {
$success = $e->getMessage();
}
$this->denormalizeConnection();
if ($success !== true) {
throw new BeginTransactionFailureException(is_string($success) ? $success : null);
}
}
public function startTransaction() {
$this->beginTransaction();
}
public function isTransactionActive() {
$this->normalizeConnection();
$state = $this->pdo->inTransaction();
$this->denormalizeConnection();
return $state;
}
public function commit() {
$this->normalizeConnection();
try {
$success = $this->pdo->commit();
}
catch (PDOException $e) {
$success = $e->getMessage();
}
$this->denormalizeConnection();
if ($success !== true) {
throw new CommitTransactionFailureException(is_string($success) ? $success : null);
}
}
public function rollBack() {
$this->normalizeConnection();
try {
$success = $this->pdo->rollBack();
}
catch (PDOException $e) {
$success = $e->getMessage();
}
$this->denormalizeConnection();
if ($success !== true) {
throw new RollBackTransactionFailureException(is_string($success) ? $success : null);
}
}
public function getProfiler() {
return $this->profiler;
}
public function setProfiler(Profiler $profiler = null) {
$this->profiler = $profiler;
return $this;
}
public function getDriverName() {
$this->ensureConnected();
switch ($this->driverName) {
case PdoDataSource::DRIVER_NAME_MYSQL: return 'MySQL';
case PdoDataSource::DRIVER_NAME_POSTGRESQL: return 'PostgreSQL';
case PdoDataSource::DRIVER_NAME_SQLITE: return 'SQLite';
case PdoDataSource::DRIVER_NAME_ORACLE: return 'Oracle';
default: return $this->driverName;
}
}
/**
* Returns some driver-specific information about the server that this instance is connected to
*
* @return string
*/
public function getServerInfo() {
$this->ensureConnected();
return $this->pdo->getAttribute(PDO::ATTR_SERVER_INFO);
}
/**
* Returns the version of the database software used on the server that this instance is connected to
*
* @return string
*/
public function getServerVersion() {
$this->ensureConnected();
return $this->pdo->getAttribute(PDO::ATTR_SERVER_VERSION);
}
/**
* Returns the version of the database client used by this instance
*
* @return string
*/
public function getClientVersion() {
$this->ensureConnected();
return $this->pdo->getAttribute(PDO::ATTR_CLIENT_VERSION);
}
public function quoteIdentifier($identifier) {
$this->ensureConnected();
if ($this->driverName === PdoDataSource::DRIVER_NAME_MYSQL) {
$char = '`';
}
else {
$char = '"';
}
return $char . str_replace($char, $char . $char, $identifier) . $char;
}
public function quoteTableName($tableName) {
if (\is_array($tableName)) {
$tableName = \array_map([ $this, 'quoteIdentifier' ], $tableName);
return \implode('.', $tableName);
}
else {
return $this->quoteIdentifier($tableName);
}
}
public function quoteLiteral($literal) {
$this->ensureConnected();
if (!empty($literal) && is_array($literal)) {
foreach ($literal as $key => $value) {
$literal[$key] = $this->pdo->quote($value);
}
return $literal;
}
else {
return $this->pdo->quote($literal);
}
}
public function addOnConnectListener(callable $onConnectListener) {
// if the database connection has not been established yet
if ($this->pdo === null) {
// schedule the callback for later execution
$this->onConnectListeners[] = $onConnectListener;
}
// if the database connection has already been established
else {
// execute the callback immediately
$onConnectListener($this);
}
return $this;
}
/** Makes sure that the connection is active and otherwise establishes it automatically */
private function ensureConnected() {
if ($this->pdo === null) {
try {
$this->pdo = new PDO($this->dsn->getDsn(), $this->dsn->getUsername(), $this->dsn->getPassword());
}
catch (PDOException $e) {
ErrorHandler::rethrow($e);
}
// iterate over all listeners waiting for the connection to be established
foreach ($this->onConnectListeners as $onConnectListener) {
// execute the callback
$onConnectListener($this);
}
// discard the listeners now that they have all been executed
$this->onConnectListeners = [];
$this->dsn = null;
}
if ($this->driverName === null) {
$this->driverName = $this->pdo->getAttribute(PDO::ATTR_DRIVER_NAME);
}
}
/** Normalizes this connection by setting attributes that provide the strong guarantees about the connection's behavior that we need */
private function normalizeConnection() {
$this->ensureConnected();
$this->configureConnection($this->attributes, $this->previousAttributes);
}
/** Restores this connection's original behavior if desired */
private function denormalizeConnection() {
$this->configureConnection($this->previousAttributes, $this->attributes);
}
/**
* Configures this connection by setting appropriate attributes
*
* @param array|null $newAttributes the new attributes to set
* @param array|null $oldAttributes where old configurations may be saved to restore them later
*/
private function configureConnection(array &$newAttributes = null, array &$oldAttributes = null) {
// if a connection is available
if (isset($this->pdo)) {
// if there are attributes that need to be applied
if (isset($newAttributes)) {
// get the keys and values of the attributes to apply
foreach ($newAttributes as $key => $newValue) {
// if the old state of the connection must be preserved
if (isset($oldAttributes)) {
// retrieve the old value for this attribute
try {
$oldValue = @$this->pdo->getAttribute($key);
}
catch (PDOException $e) {
// the specified attribute is not supported by the driver
$oldValue = null;
}
// if an old value has been found
if (isset($oldValue)) {
// if the old value differs from the new value that we're going to set
if ($oldValue !== $newValue) {
// save the old value so that we're able to restore it later
$oldAttributes[$key] = $oldValue;
}
}
}
// and then set the desired new value
$this->pdo->setAttribute($key, $newValue);
}
// if the old state of the connection doesn't need to be preserved
if (!isset($oldAttributes)) {
// we're done updating attributes for this connection once and for all
$newAttributes = null;
}
}
}
}
/**
* Selects from the database using the specified query and returns what the supplied callback extracts from the result set
*
* You should not include any dynamic input in the query
*
* Instead, pass `?` characters (without any quotes) as placeholders and pass the actual values in the third argument
*
* @param callable $callback the callback that receives the executed statement and can then extract and return the desired results
* @param string $query the query to select with
* @param array|null $bindValues (optional) the values to bind as replacements for the `?` characters in the query
* @return mixed whatever the callback has extracted and returned from the result set
*/
private function selectInternal(callable $callback, $query, array $bindValues = null) {
$this->normalizeConnection();
try {
// create a prepared statement from the supplied SQL string
$stmt = $this->pdo->prepare($query);
}
catch (PDOException $e) {
ErrorHandler::rethrow($e);
}
// if a performance profiler has been defined
if (isset($this->profiler)) {
$this->profiler->beginMeasurement();
}
/** @var PDOStatement $stmt */
// bind the supplied values to the query and execute it
try {
$stmt->execute($bindValues);
}
catch (PDOException $e) {
ErrorHandler::rethrow($e);
}
// if a performance profiler has been defined
if (isset($this->profiler)) {
$this->profiler->endMeasurement($query, $bindValues, 1);
}
// fetch the desired results from the result set via the supplied callback
$results = $callback($stmt);
$this->denormalizeConnection();
// if the result is empty
if (empty($results) && $stmt->rowCount() === 0 && ($this->driverName !== PdoDataSource::DRIVER_NAME_SQLITE || \is_bool($results) || \is_array($results))) {
// consistently return `null`
return null;
}
// if some results have been found
else {
// return these as extracted by the callback
return $results;
}
}
}
+61
View File
@@ -0,0 +1,61 @@
<?php
/*
* PHP-DB (https://github.com/delight-im/PHP-DB)
* Copyright (c) delight.im (https://www.delight.im/)
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
*/
namespace Delight\Db;
/** DSN for use with PHP's built-in PDO */
final class PdoDsn implements Dsn {
/** @var string the DSN as a string */
private $dsn;
/** @var string|null the username that complements the DSN */
private $username;
/** @var string|null the password that complements the DSN */
private $password;
/**
* Constructor
*
* @param string $dsnStr the DSN as a string
* @param string $username (optional) the username that complements the DSN
* @param string $password (optional) the password that complements the DSN
*/
public function __construct($dsnStr, $username = null, $password = null) {
$this->dsn = (string) $dsnStr;
$this->username = (string) $username;
$this->password = (string) $password;
}
/**
* Returns the DSN as a string
*
* @return string
*/
public function getDsn() {
return $this->dsn;
}
/**
* Returns the username that complements the DSN
*
* @return string|null
*/
public function getUsername() {
return $this->username;
}
/**
* Returns the password that complements the DSN
*
* @return string|null
*/
public function getPassword() {
return $this->password;
}
}
+51
View File
@@ -0,0 +1,51 @@
<?php
/*
* PHP-DB (https://github.com/delight-im/PHP-DB)
* Copyright (c) delight.im (https://www.delight.im/)
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
*/
namespace Delight\Db;
/** Profiler that monitors performance of individual database queries and statements */
interface Profiler {
/** Starts a new measurement */
public function beginMeasurement();
/**
* Ends a previously started measurement
*
* @param string $sql the SQL query or statement that was monitored
* @param array|null $boundValues (optional) the values that have been bound to the query or statement
* @param int|null $discardMostRecentTraceEntries (optional) the number of trace entries that should be discarded (starting with the most recent ones)
*/
public function endMeasurement($sql, array $boundValues = null, $discardMostRecentTraceEntries = null);
/**
* Returns the number of measurements that this profiler has recorded
*
* @return int
*/
public function getCount();
/**
* Returns the measurement at the specified index
*
* @param int $index the index of the measurement to return
* @return Measurement
*/
public function getMeasurement($index);
/**
* Returns all measurements that this profiler has recorded
*
* @return Measurement[]
*/
public function getMeasurements();
/** Sorts the measurements that this profiler has recorded so that the longest-running operations are listed first */
public function sort();
}
+54
View File
@@ -0,0 +1,54 @@
<?php
/*
* PHP-DB (https://github.com/delight-im/PHP-DB)
* Copyright (c) delight.im (https://www.delight.im/)
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
*/
namespace Delight\Db;
/** Implementation of an individual measurement of a profiler that monitors performance */
final class SimpleMeasurement implements Measurement {
/** @var float the duration in milliseconds */
private $duration;
/** @var string the SQL query or statement */
private $sql;
/** @var array|null the values that have been bound to the query or statement */
private $boundValues;
/** @var array|null the trace that shows the path taken through the program until the operation was executed */
private $trace;
/**
* Constructor
*
* @param float $duration the duration in milliseconds
* @param string $sql the SQL query or statement
* @param array|null $boundValues (optional) the values that have been bound to the query or statement
* @param array|null $trace (optional) the trace that shows the path taken through the program until the operation was executed
*/
public function __construct($duration, $sql, array $boundValues = null, $trace = null) {
$this->duration = $duration;
$this->sql = $sql;
$this->boundValues = $boundValues;
$this->trace = $trace;
}
public function getDuration() {
return $this->duration;
}
public function getSql() {
return $this->sql;
}
public function getBoundValues() {
return $this->boundValues;
}
public function getTrace() {
return $this->trace;
}
}
+89
View File
@@ -0,0 +1,89 @@
<?php
/*
* PHP-DB (https://github.com/delight-im/PHP-DB)
* Copyright (c) delight.im (https://www.delight.im/)
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
*/
namespace Delight\Db;
/** Implementation of a profiler that monitors performance of individual database queries and statements */
final class SimpleProfiler implements Profiler {
/** The maximum number of entries in traces to use as the default */
const TRACE_MAX_LENGTH_DEFAULT = 10;
/** @var Measurement[] the measurements recorded by this instance */
private $measurements;
/** @var int the maximum number of entries in traces */
private $maxTraceLength;
/** @var float|null the start time of the current measurement in milliseconds */
private $currentMeasurementStartTime;
public function __construct($maxTraceLength = null) {
$this->measurements = [];
if ($maxTraceLength === null) {
$this->maxTraceLength = self::TRACE_MAX_LENGTH_DEFAULT;
}
else {
$this->maxTraceLength = (int) $maxTraceLength;
}
$this->currentMeasurementStartTime = null;
}
public function beginMeasurement() {
$this->currentMeasurementStartTime = microtime(true) * 1000;
}
public function endMeasurement($sql, array $boundValues = null, $discardMostRecentTraceEntries = null) {
if ($discardMostRecentTraceEntries === null) {
$discardMostRecentTraceEntries = 0;
}
else {
$discardMostRecentTraceEntries = (int) $discardMostRecentTraceEntries;
}
// get the trace at this point of the program execution
$trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, $this->maxTraceLength);
// discard as many of the most recent entries as desired (but always discard at least the current method)
for ($i = 0; $i < $discardMostRecentTraceEntries + 1; $i++) {
array_shift($trace);
}
// calculate the duration in milliseconds
$duration = (microtime(true) * 1000) - $this->currentMeasurementStartTime;
// and finally record the measurement
$this->measurements[] = new SimpleMeasurement(
$duration,
$sql,
$boundValues,
$trace
);
}
public function getCount() {
return count($this->measurements);
}
public function getMeasurement($index) {
return $this->measurements[$index];
}
public function getMeasurements() {
return $this->measurements;
}
public function sort() {
usort($this->measurements, function ($a, $b) {
/** @var Measurement $a */
/** @var Measurement $b */
return ($b->getDuration() - $a->getDuration());
});
}
}
+12
View File
@@ -0,0 +1,12 @@
<?php
/*
* PHP-DB (https://github.com/delight-im/PHP-DB)
* Copyright (c) delight.im (https://www.delight.im/)
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
*/
namespace Delight\Db\Throwable;
/** Exception that is thrown when a transaction cannot be started successfully for some reason */
class BeginTransactionFailureException extends TransactionFailureException {}
@@ -0,0 +1,12 @@
<?php
/*
* PHP-DB (https://github.com/delight-im/PHP-DB)
* Copyright (c) delight.im (https://www.delight.im/)
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
*/
namespace Delight\Db\Throwable;
/** Exception that is thrown when a transaction cannot be committed successfully for some reason */
class CommitTransactionFailureException extends TransactionFailureException {}
+12
View File
@@ -0,0 +1,12 @@
<?php
/*
* PHP-DB (https://github.com/delight-im/PHP-DB)
* Copyright (c) delight.im (https://www.delight.im/)
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
*/
namespace Delight\Db\Throwable;
/** Error that is thrown when the requested database cannot be found */
class DatabaseNotFoundError extends Error {}
+12
View File
@@ -0,0 +1,12 @@
<?php
/*
* PHP-DB (https://github.com/delight-im/PHP-DB)
* Copyright (c) delight.im (https://www.delight.im/)
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
*/
namespace Delight\Db\Throwable;
/** Error that is thrown when an empty list of values is provided where values are required */
class EmptyValueListError extends Error {}
+20
View File
@@ -0,0 +1,20 @@
<?php
/*
* PHP-DB (https://github.com/delight-im/PHP-DB)
* Copyright (c) delight.im (https://www.delight.im/)
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
*/
namespace Delight\Db\Throwable;
/**
* Error that is thrown when an empty `WHERE` clause is provided
*
* Although technically perfectly valid, an empty list of criteria is often provided by mistake
*
* This is why, for some operations, it is deemed too dangerous and thus disallowed
*
* Usually, one can simply execute a manual statement instead to get rid of this restriction
*/
class EmptyWhereClauseError extends Error {}
+12
View File
@@ -0,0 +1,12 @@
<?php
/*
* PHP-DB (https://github.com/delight-im/PHP-DB)
* Copyright (c) delight.im (https://www.delight.im/)
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
*/
namespace Delight\Db\Throwable;
/** Base class for all conditions that the application might not recover from and thus should not catch */
class Error extends \Exception {}
+12
View File
@@ -0,0 +1,12 @@
<?php
/*
* PHP-DB (https://github.com/delight-im/PHP-DB)
* Copyright (c) delight.im (https://www.delight.im/)
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
*/
namespace Delight\Db\Throwable;
/** Base class for all conditions that the application might recover from and thus should catch and handle */
class Exception extends \Exception {}
@@ -0,0 +1,18 @@
<?php
/*
* PHP-DB (https://github.com/delight-im/PHP-DB)
* Copyright (c) delight.im (https://www.delight.im/)
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
*/
namespace Delight\Db\Throwable;
/**
* Exception that is thrown when an integrity constraint is being violated
*
* Common constraints include 'UNIQUE', 'NOT NULL' and 'FOREIGN KEY'
*
* Ambiguous column references constitute violations as well
*/
class IntegrityConstraintViolationException extends Exception {}
+12
View File
@@ -0,0 +1,12 @@
<?php
/*
* PHP-DB (https://github.com/delight-im/PHP-DB)
* Copyright (c) delight.im (https://www.delight.im/)
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
*/
namespace Delight\Db\Throwable;
/** Error that is thrown when no database has been selected */
class NoDatabaseSelectedError extends Error {}
@@ -0,0 +1,12 @@
<?php
/*
* PHP-DB (https://github.com/delight-im/PHP-DB)
* Copyright (c) delight.im (https://www.delight.im/)
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
*/
namespace Delight\Db\Throwable;
/** Exception that is thrown when a transaction cannot be rolled back successfully for some reason */
class RollBackTransactionFailureException extends TransactionFailureException {}
+12
View File
@@ -0,0 +1,12 @@
<?php
/*
* PHP-DB (https://github.com/delight-im/PHP-DB)
* Copyright (c) delight.im (https://www.delight.im/)
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
*/
namespace Delight\Db\Throwable;
/** Error that is thrown when there is invalid syntax or when an access rule is being violated */
class SyntaxError extends Error {}
+12
View File
@@ -0,0 +1,12 @@
<?php
/*
* PHP-DB (https://github.com/delight-im/PHP-DB)
* Copyright (c) delight.im (https://www.delight.im/)
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
*/
namespace Delight\Db\Throwable;
/** Error that is thrown when a table cannot be found */
class TableNotFoundError extends Error {}
+12
View File
@@ -0,0 +1,12 @@
<?php
/*
* PHP-DB (https://github.com/delight-im/PHP-DB)
* Copyright (c) delight.im (https://www.delight.im/)
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
*/
namespace Delight\Db\Throwable;
/** Base class for all exceptions that occur with transaction controls */
abstract class TransactionFailureException extends Exception {}
+12
View File
@@ -0,0 +1,12 @@
<?php
/*
* PHP-DB (https://github.com/delight-im/PHP-DB)
* Copyright (c) delight.im (https://www.delight.im/)
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
*/
namespace Delight\Db\Throwable;
/** Error that is thrown when a column cannot be found */
class UnknownColumnError extends Error {}
+12
View File
@@ -0,0 +1,12 @@
<?php
/*
* PHP-DB (https://github.com/delight-im/PHP-DB)
* Copyright (c) delight.im (https://www.delight.im/)
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
*/
namespace Delight\Db\Throwable;
/** Error that is thrown when the supplied credentials cannot be used to access the database */
class WrongCredentialsError extends Error {}
+28
View File
@@ -0,0 +1,28 @@
<?php
/*
* PHP-DB (https://github.com/delight-im/PHP-DB)
* Copyright (c) delight.im (https://www.delight.im/)
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
*/
// enable error reporting
\error_reporting(\E_ALL);
\ini_set('display_errors', 'stdout');
// enable assertions
\ini_set('assert.active', 1);
@\ini_set('zend.assertions', 1);
\ini_set('assert.exception', 1);
\header('Content-type: text/plain; charset=utf-8');
require __DIR__.'/../vendor/autoload.php';
$db = \Delight\Db\PdoDatabase::fromDsn(
new \Delight\Db\PdoDsn(
'mysql:dbname=my-database;host=localhost;charset=utf8mb4',
'my-username',
'my-password'
)
);