Skip to content

Commit

Permalink
Merge pull request #505 from flightphp/database-class
Browse files Browse the repository at this point in the history
Database class
  • Loading branch information
n0nag0n authored Jan 8, 2024
2 parents 4b89e37 + 71a3fe3 commit ceeab06
Show file tree
Hide file tree
Showing 5 changed files with 316 additions and 0 deletions.
54 changes: 54 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,60 @@ $new = Flight::db(false);
Keep in mind that mapped methods have precedence over registered classes. If you
declare both using the same name, only the mapped method will be invoked.

## PDO Helper Class

Flight comes with a helper class for PDO. It allows you to easily query your database
with all the prepared/execute/fetchAll() wackiness. It greatly simplifies how you can
query your database.

```php
// Register the PDO helper class
Flight::register('db', \flight\database\PdoWrapper::class, ['mysql:host=localhost;dbname=cool_db_name', 'user', 'pass', [
PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES \'utf8mb4\'',
PDO::ATTR_EMULATE_PREPARES => false,
PDO::ATTR_STRINGIFY_FETCHES => false,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
]
]);

Flight::route('/users', function () {
// Get all users
$users = Flight::db()->fetchAll('SELECT * FROM users');

// Stream all users
$statement = Flight::db()->runQuery('SELECT * FROM users');
while ($user = $statement->fetch()) {
echo $user['name'];
}

// Get a single user
$user = Flight::db()->fetchRow('SELECT * FROM users WHERE id = ?', [123]);

// Get a single value
$count = Flight::db()->fetchField('SELECT COUNT(*) FROM users');

// Special IN() syntax to help out (make sure IN is in caps)
$users = Flight::db()->fetchAll('SELECT * FROM users WHERE id IN (?)', [[1,2,3,4,5]]);
// you could also do this
$users = Flight::db()->fetchAll('SELECT * FROM users WHERE id IN (?)', [ '1,2,3,4,5']);

// Insert a new user
Flight::db()->runQuery("INSERT INTO users (name, email) VALUES (?, ?)", ['Bob', '[email protected]']);
$insert_id = $Flight::db()->lastInsertId();

// Update a user
Flight::db()->runQuery("UPDATE users SET name = ? WHERE id = ?", ['Bob', 123]);

// Delete a user
Flight::db()->runQuery("DELETE FROM users WHERE id = ?", [123]);

// Get the number of affected rows
$statement = Flight::db()->runQuery("UPDATE users SET name = ? WHERE name = ?", ['Bob', 'Sally']);
$affected_rows = $statement->rowCount();

});
```

# Overriding

Flight allows you to override its default functionality to suit your own needs,
Expand Down
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
]
},
"require-dev": {
"ext-pdo_sqlite": "*",
"phpunit/phpunit": "^9.5",
"phpstan/phpstan": "^1.10",
"phpstan/extension-installer": "^1.3"
Expand Down
2 changes: 2 additions & 0 deletions flight/core/Loader.php
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,8 @@ public static function autoload(bool $enabled = true, $dirs = []): void

/**
* Autoloads classes.
*
* Classes are not allowed to have underscores in their names.
*
* @param string $class Class name
*/
Expand Down
148 changes: 148 additions & 0 deletions flight/database/PdoWrapper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
<?php

namespace flight\database;

use PDO;
use PDOStatement;

class PdoWrapper extends PDO {

/**
* How you create the connection for the database
*
* @param string $dsn - Ex: 'mysql:host=localhost;port=3306;dbname=testdb;charset=utf8mb4'
* @param string $username - Ex: 'root'
* @param string $password - Ex: 'password'
* @param array $options - PDO options you can pass in
*/
public function __construct(string $dsn, ?string $username = null, ?string $password = null, array $options = []) {
parent::__construct($dsn, $username, $password, $options);
}

/**
* Use this for INSERTS, UPDATES, or if you plan on using a SELECT in a while loop
*
* Ex: $statement = $db->runQuery("SELECT * FROM table WHERE something = ?", [ $something ]);
* while($row = $statement->fetch()) {
* // ...
* }
*
* $db->runQuery("INSERT INTO table (name) VALUES (?)", [ $name ]);
* $db->runQuery("UPDATE table SET name = ? WHERE id = ?", [ $name, $id ]);
*
* @param string $sql - Ex: "SELECT * FROM table WHERE something = ?"
* @param array $params - Ex: [ $something ]
* @return PDOStatement
*/
public function runQuery(string $sql, array $params = []): PDOStatement {
$processed_sql_data = $this->processInStatementSql($sql, $params);
$sql = $processed_sql_data['sql'];
$params = $processed_sql_data['params'];
$statement = $this->prepare($sql);
$statement->execute($params);
return $statement;
}

/**
* Pulls one field from the query
*
* Ex: $id = $db->fetchField("SELECT id FROM table WHERE something = ?", [ $something ]);
*
* @param string $sql - Ex: "SELECT id FROM table WHERE something = ?"
* @param array $params - Ex: [ $something ]
* @return mixed
*/
public function fetchField(string $sql, array $params = []) {
$data = $this->fetchRow($sql, $params);
return is_array($data) ? reset($data) : null;
}

/**
* Pulls one row from the query
*
* Ex: $row = $db->fetchRow("SELECT * FROM table WHERE something = ?", [ $something ]);
*
* @param string $sql - Ex: "SELECT * FROM table WHERE something = ?"
* @param array $params - Ex: [ $something ]
* @return array
*/
public function fetchRow(string $sql, array $params = []): array {
$sql .= stripos($sql, 'LIMIT') === false ? ' LIMIT 1' : '';
$result = $this->fetchAll($sql, $params);
return is_array($result) && count($result) ? $result[0] : [];
}

/**
* Pulls all rows from the query
*
* Ex: $rows = $db->fetchAll("SELECT * FROM table WHERE something = ?", [ $something ]);
* foreach($rows as $row) {
* // ...
* }
*
* @param string $sql - Ex: "SELECT * FROM table WHERE something = ?"
* @param array $params - Ex: [ $something ]
* @return array<int,array>
*/
public function fetchAll(string $sql, array $params = []): array {
$processed_sql_data = $this->processInStatementSql($sql, $params);
$sql = $processed_sql_data['sql'];
$params = $processed_sql_data['params'];
$statement = $this->prepare($sql);
$statement->execute($params);
$result = $statement->fetchAll();
return is_array($result) ? $result : [];
}

/**
* Don't worry about this guy. Converts stuff for IN statements
*
* Ex: $row = $db->fetchAll("SELECT * FROM table WHERE id = ? AND something IN(?), [ $id, [1,2,3] ]);
* Converts this to "SELECT * FROM table WHERE id = ? AND something IN(?,?,?)"
*
* @param string $sql the sql statement
* @param array $params the params for the sql statement
* @return array{sql:string,params:array}
*/
protected function processInStatementSql(string $sql, array $params = []): array {

// Handle "IN(?)". This is to be used with a comma delimited string, but can also be used with an array.
// Remove the spaces in variations of "IN ( ? )" where the space after IN is optional, and any number of spaces before and after the question mark is optional.
// Then loop through each "IN(?)" in the query and replace the single question mark with the correct number of question marks.
$sql = preg_replace('/IN\s*\(\s*\?\s*\)/i', 'IN(?)', $sql);
$current_index = 0;
while(($current_index = strpos($sql, 'IN(?)', $current_index)) !== false) {
$preceeding_count = substr_count($sql, '?', 0, $current_index - 1);

$param = $params[$preceeding_count];
$question_marks = '?';

// If param is a string, explode it and replace the question mark with the correct number of question marks
if(is_string($param) || is_array($param)) {

$params_to_use = $param;
if(is_string($param)) {
$params_to_use = explode(',', $param);
}

foreach($params_to_use as $key => $value) {
if(is_string($value)) {
$params_to_use[$key] = trim($value);
}
}

// Replace the single question mark with the appropriate number of question marks.
$question_marks = join(',', array_fill(0, count($params_to_use), '?'));
$sql = substr_replace($sql, $question_marks, $current_index + 3, 1);

// Insert the new params into the params array.
array_splice($params, $preceeding_count, 1, $params_to_use);
}

// Increment by the length of the question marks and accounting for the length of "IN()"
$current_index += strlen($question_marks) + 4;
}

return [ 'sql' => $sql, 'params' => $params ];
}
}
111 changes: 111 additions & 0 deletions tests/PdoWrapperTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
<?php

use flight\database\PdoWrapper;

/**
* Flight: An extensible micro-framework.
*
* @copyright Copyright (c) 2012, Mike Cao <[email protected]>
* @license MIT, http://flightphp.com/license
*/



class PdoWrapperTest extends PHPUnit\Framework\TestCase
{
/**
* @var Pdo_Wrapper
*/
private $pdo_wrapper;

protected function setUp(): void
{
$this->pdo_wrapper = new PdoWrapper('sqlite::memory:');
// create a test table and insert 3 rows of data
$this->pdo_wrapper->exec('CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)');
$this->pdo_wrapper->exec('INSERT INTO test (name) VALUES ("one")');
$this->pdo_wrapper->exec('INSERT INTO test (name) VALUES ("two")');
$this->pdo_wrapper->exec('INSERT INTO test (name) VALUES ("three")');
}

protected function tearDown(): void
{
// delete the test table
$this->pdo_wrapper->exec('DROP TABLE test');
}

public function testRunQuerySelectAllStatement() {
$statement = $this->pdo_wrapper->runQuery('SELECT * FROM test');
$this->assertInstanceOf(PDOStatement::class, $statement);
$this->assertCount(3, $statement->fetchAll());
}

public function testRunQuerySelectOneStatement() {
$statement = $this->pdo_wrapper->runQuery('SELECT * FROM test WHERE id = 1');
$this->assertInstanceOf(PDOStatement::class, $statement);
$this->assertCount(1, $statement->fetchAll());
}

public function testRunQueryInsertStatement() {
$statement = $this->pdo_wrapper->runQuery('INSERT INTO test (name) VALUES ("four")');
$this->assertInstanceOf(PDOStatement::class, $statement);
$this->assertEquals(1, $statement->rowCount());
}

public function testRunQueryUpdateStatement() {
$statement = $this->pdo_wrapper->runQuery('UPDATE test SET name = "something" WHERE name LIKE ?', ['%t%']);
$this->assertInstanceOf(PDOStatement::class, $statement);
$this->assertEquals(2, $statement->rowCount());
}

public function testRunQueryDeleteStatement() {
$statement = $this->pdo_wrapper->runQuery('DELETE FROM test WHERE name LIKE ?', ['%t%']);
$this->assertInstanceOf(PDOStatement::class, $statement);
$this->assertEquals(2, $statement->rowCount());
}

public function testFetchField() {
$id = $this->pdo_wrapper->fetchField('SELECT id FROM test WHERE name = ?', ['two']);
$this->assertEquals(2, $id);
}

public function testFetchRow() {
$row = $this->pdo_wrapper->fetchRow('SELECT * FROM test WHERE name = ?', ['two']);
$this->assertEquals(2, $row['id']);
$this->assertEquals('two', $row['name']);
}

public function testFetchAll() {
$rows = $this->pdo_wrapper->fetchAll('SELECT * FROM test');
$this->assertCount(3, $rows);
$this->assertEquals(1, $rows[0]['id']);
$this->assertEquals('one', $rows[0]['name']);
$this->assertEquals(2, $rows[1]['id']);
$this->assertEquals('two', $rows[1]['name']);
$this->assertEquals(3, $rows[2]['id']);
$this->assertEquals('three', $rows[2]['name']);
}

public function testFetchAllWithNamedParams() {
$rows = $this->pdo_wrapper->fetchAll('SELECT * FROM test WHERE name = :name', [ 'name' => 'two']);
$this->assertCount(1, $rows);
$this->assertEquals(2, $rows[0]['id']);
$this->assertEquals('two', $rows[0]['name']);
}

public function testFetchAllWithInInt() {
$rows = $this->pdo_wrapper->fetchAll('SELECT id FROM test WHERE id IN(?)', [ [1,2 ]]);
$this->assertEquals(2, count($rows));
}

public function testFetchAllWithInString() {
$rows = $this->pdo_wrapper->fetchAll('SELECT id FROM test WHERE name IN(?)', [ ['one','two' ]]);
$this->assertEquals(2, count($rows));
}

public function testFetchAllWithInStringCommas() {
$rows = $this->pdo_wrapper->fetchAll('SELECT id FROM test WHERE id > ? AND name IN(?)', [ 0, 'one,two' ]);
$this->assertEquals(2, count($rows));
}

}

0 comments on commit ceeab06

Please sign in to comment.