For the examples below, the following users
table structure is used:
id | name | gender |
---|---|---|
1 | Sam | m |
2 | Alex | f |
3 | Daniel | m |
The class works with Composer's autoloading, so if you install through it you just need to require the vendor/autoload.php
file somewhere in your project. It's defined in the SesinopSys
namespace, so if you're using it from within a namespace, refer to it as \SeinopSys\PostgresDb
or add use \SeinopSys\PostgresDb;
at the top of the file so you can drop the namespace afterwards.
If you decided to go the simple include route then you can include the src/PostgresDb.php
file and use it pretty much the same way.
To start using the class you need to create an instance with the following parameters.
use \SeinopSys\PostgresDb;
$db = new PostgresDb($database_name, $host, $username, $password, $port);
Due to the way these parameters are mapped to the internal connection call within PHP these values must not contain spaces. If you do not specify the $port
parameter the PostgreSQL default (5432) will be used.
Initializing the class does not immediately create a connection. To force a connection attempt, call:
$db->getConnection();
This will return the internal PDO object (and create it if it doesn't exist yet) used by the class and simultaneously attempts to connect to the database. Initial connection errors can be caught by using try…catch
but by default the script uses PDO::ERRMODE_WARNING
for further errors.
If you'd prefer exceptions to be thrown instead, or if you like to live dangerously and want to silence all errors, you can use the chainable setPDOErrmode()
method by passing any of the PDO::ERRMODE_*
constants both before and after the connection has been made.
// Before connection
$db->setPDOErrmode(PDO::ERRMODE_EXCEPTION)->getConnection();
// After connection
$db->getConnection();
$db->setPDOErrmode(PDO::ERRMODE_SILENT)->get(…)->setPDOErrmode(PDO::ERRMODE_WARNING);
If you need the value later for whatever reason you can read it out using getPDOErrmode()
.
If you happen to have an existing PDO connection lying around you can also use it with this class, like so:
require "PostgresDb.php";
$db = new PostgresDb();
$db->setConnection($existing_connection);
Any future method calls on the $db
object will act on the existing connection instead of creating a new instance. Only use a different connection with this class if you know what you're doing. Manipulating the same PDO object with different libraries can result in strange behavior, so be careful.
Returns an array containing each result as an array of its own. Returns false
if no row is found.
// SELECT * FROM users
$db->get('users');
// SELECT * FROM users LIMIT 1
$db->get('users', 1);
// SELECT id, name FROM users LIMIT 1
$db->get('users', 1, 'id, name');
// SELECT id, name FROM users LIMIT 2 OFFSET 1
$db->get('users', [1,2], 'id, name');
Return format:
[
[
'id' => 2,
'name' => 'Alex'
],
[
'id' => 3,
'name' => 'Daniel'
]
]
Alternatively, you can use getOne
to only select a single row. Returns false
if no row is found.
// SELECT * FROM users LIMIT 1
$db->getOne('users');
// SELECT id, name FROM users LIMIT 1
$db->getOne('users', 'id, name');
Return format:
[
'id' => 1,
'name' => 'Sam'
]
// SELECT * FROM users WHERE "id" = 1 LIMIT 1
$db->where('id', 1)->get('users');
// SELECT * FROM users LIMIT 1 WHERE "id" != 1
$db->where('"id" != 1')->getOne('users');
// SELECT * FROM users LIMIT 1 WHERE "id" != 1
$db->where('id', 1, '!=')->getOne('users');
// SELECT * FROM users LIMIT 1 WHERE "id" IN (1, 3)
$db->where('id', [1, 3])->getOne('users');
// SELECT * FROM users LIMIT 1 WHERE "id" NOT IN (1, 3)
$db->where('id', [1, 3], '!=')->getOne('users');
// SELECT * FROM users u LEFT JOIN "messages" m ON m.user = u.id
$db->join('messages m', 'm.user = u.id', 'LEFT')->get('users u');
// SELECT * FROM users u INNER JOIN "messages" m ON u.id = m.user
$db->join('messages m', 'u.id = m.user', 'INNER')->get('users u');
Complex ORDER BY
statements can be passed to orderByLiteral
which will just use the value you provide as it is. you can use the built-in constant to order the results randomly.
// SELECT * FROM users ORDER BY "name" DESC
$db->orderBy('name')->get('users');
// SELECT * FROM users ORDER BY "name" ASC
$db->orderBy('name','ASC')->get('users');
// SELECT * FROM users ORDER BY rand()
$db->orderBy(PostgresDb::ORDERBY_RAND)->get('users');
// SELECT * FROM users ORDER BY CASE WHEN "name" IS NULL THEN 1 ELSE 0 END DESC, "name" ASC
$db
->orderByLiteral('CASE WHEN "name" IS NULL THEN 1 ELSE 0 END')
->orderBy('name','ASC')
->get('users');
// SELECT * FROM "users" GROUP BY "gender"
$db->groupBy('gender')->get('users');
// SELECT * FROM "users" GROUP BY "gender", "othercol"
$db->groupBy('gender')->groupBy('othercol')->get('users');
Returns false
on failure and true
if successful.
// INSERT INTO users ("name", gender) VALUES ('Joe', 'm')
$db->insert('users', ['name' => 'Joe', 'gender' => 'm']);
id | name | gender |
---|---|---|
1 | Sam | m |
2 | Alex | f |
3 | Daniel | m |
4 | Joe | m |
You can also ask for the values of inserted columns, in which case it'll return those instead of true
if the insert is successful. This is especially useful when you need the value of an index that's generated on the fly by Postgres.
When returning a single column its value is returned by the method, and when multiple columns are specified the return value is an associative array with the keys being the columns and the values being, well, the values.
// INSERT INTO users ("name") VALUES ('Joe') RETURNING id
$id = $db->insert('users', ['name' => 'Joe'], 'id');
// $id === 4
// INSERT INTO users ("name") VALUES ('Joe') RETURNING id, "name"
$return = $db->insert('users', ['name' => 'Joe'], ['id', 'name']);
// or $db->insert('users', ['name' => 'Joe'], 'id, name');
// $return === ['id' => 4, 'name' => 'Joe']
Returns false
on failure and true
if successful.
// UPDATE users SET "name" = 'Dave' WHERE "id" = 1
$db->where('id', 1)->update('users', ['name' => 'Dave']);
id | name | gender |
---|---|---|
1 | Dave | m |
2 | Alex | f |
3 | Daniel | m |
4 | Joe | m |
Returns false
on failure and true
if successful. Cannot be used with groupBy()
, join()
and orderBy()
.
// DELETE FROM users WHERE "id" = 3
$success = $db->where('id', 3)->delete('users');
// $success === true
A second argument can optionally be passed to return columns from the deleted row, similar to insert()
.
// DELETE FROM users WHERE "id" = 3 RETURNING gender
$return = $db->where('id', 3)->delete('users', 'gender');
// $return === 'm'
// DELETE FROM users WHERE "id" = 3 RETURNING "name", gender
$return = $db->where('id', 3)->delete('users', ['name', 'gender']);
// or $db->where('id', 3)->delete('users', 'name, gender');
// $return === ['name' => 'Daniel', 'gender' => 'm']
id | name | gender |
---|---|---|
1 | Dave | m |
2 | Alex | f |
4 | Joe | m |
Executes the query exactly* as passed, for when it is far too complex to use the built-in methods, or when a built-in method does not exist for what you want to achieve. Return value format matches get()
's.
// Query string only
$db->query('SELECT * FROM users WHERE id = 4');
// Using prepared statement parameters
$id = 4;
$db->query('SELECT * FROM users WHERE id = ? && name = ?', [4, 'Joe']);
$db->query('SELECT * FROM users WHERE id = :id && name = :who', [':id' => $id, ':who' => 'Joe']);
Return format:
[
[
'id' => 4,
'name' => 'Joe'
],
]
* The class currently replaces &&
with AND
if it's surrounded by whitespace on both sides. PostgreSQL would report a syntax error when using the former instead of the latter without this pre-processing step. See the _alterQuery()
method for the exact implementation.
If you want the power of a custom query, but also the convenience of getting the first result as a single array you can use querySingle()
which wortks similarly to getOne()
.
$db->querySingle("SELECT * FROM 'users' WHERE id = 4");
// Works the same way for bound parameters
$id = 4;
$db->querySingle("SELECT * FROM 'users' WHERE id = ?", [$id]);
Return format:
[
'id' => 4,
'name' => 'Joe'
]
Note: These will reset the object, so the following will NOT work as expected:
$withWhere = $db->where('"id" = 2');
if ($withWhere->has('users')) {
$db->getOne('users');
}
Instead of returning the user with the id
equal to 2
, it'll return whatever row it finds first since the where()
call is no longer in effect.
$db->tableExists('users'); // true
$db->count('users'); // 3
$db->where('"id" = 2')->count('users'); // 1
Basically count() >= 1
, but reads nicer.
$db->has('users'); // true
$db->where('id', 2)->has('users'); // true
$db->where('id', 0)->has('users'); // false
This will return the last query executed through the class where the placeholders have been replaced with actual values. While the class does its best to keep the SQL valid you should not rely on the returned value being a valid query.
$db->insert('users', ['name' => 'Sarah'])
echo $db->getLastQuery(); // INSERT INTO "users" ("name") VALUES ('Sarah')
This can be useful after a failed insert()
/update()
call, for example.
// Duplicated ID
$db->insert('users', ['id' => '1', 'name' => 'Fred'])
echo $db->getLastError(); // #1062 - Duplicate entry '1' for key 'PRIMARY'
Returns the number of rows affected by the last SQL statement
$db->get('users');
echo $db->count; // 3
With a simple wrapper class it's trivial to include a query counter if need be.
class PostgresDbWrapper extends \SeinopSys\PostgresDb
{
public $query_count = 0;
/**
* @param PDOStatement $stmt Statement to execute
*
* @return bool|array|object[]
*/
protected function execStatement($stmt)
{
$this->query_count++;
return parent::execStatement($stmt);
}
}
The class contains two utility methods (tableNameToClassName
and setClass
) which allow for the creation of a wrapper that can force returned values into a class instead of an array. This allows for using both the array and class method simultaneously with minimal effort. An example of such a wrapper class is shown below.
This assumes an autoloader is configured within the project which allows classes to be loaded on the fly as needed. This method will cause the autoloader to attempt loading the class within the Models
namespace when class_exists
is called. If it fails, a key is set on a private array. This prevents future checks for the existence of the same class to avoid significantly impacting the application's performance.
class PostgresDbWrapper extends \SeinopSys\PostgresDb
{
private $nonexistantClassCache = [];
/**
* @param PDOStatement $stmt Statement to execute
*
* @return bool|array|object[]
*/
protected function execStatement($stmt)
{
$className = $this->tableNameToClassName();
if (isset($className) && empty($this->nonexistantClassCache[$className])) {
try {
if (!class_exists("\\Models\\$className")) {
throw new Exception();
}
$this->setClass("\\Models\\$className");
}
catch (Exception $e) {
$this->nonexistantClassCache[$className] = true;
}
}
return parent::execStatement($stmt);
}
}