In one of our last post we built a simple SQL wrapper with PHP. Now we are going to improve it a little bit. We area going to use a class Table instead of the table name. Why? Simple. We want to create triggers. OK we can create triggers directly in the database but sometimes our triggers need to perform operations outside the database, such as call a REST webservice, filesystem’s logs or things like that.
<?php class Storage { static $count = 0; static function init() { self::$count = 0; } static function increment() { self::$count++; } static function decrement() { self::$count--; } static function get() { return self::$count; } } class SqlTest extends PHPUnit_Framework_TestCase { public function setUp() { $this->dbh = new Conn('pgsql:dbname=db;host=localhost', 'gonzalo', 'password'); $this->dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); $this->dbh->forceRollback(); } public function testInsertWithPostInsertShowingInsertedValues() { Storage::init(); $that = $this; $this->dbh->transactional(function($dbh) use ($that) { $sql = new Sql($that->dbh); $users = new Table('users'); $users->postInsert(function($values) {Storage::increment();}); $that->assertEquals(0, Storage::get()); $actual = $sql->insert($users, array('uid' => 7, 'name' => 'Gonzalo', 'surname' => 'Ayuso')); $that->assertTrue($actual); $that->assertEquals(1, Storage::get()); }); } public function testInsertWithPostInsert() { Storage::init(); $that = $this; $this->dbh->transactional(function($dbh) use ($that) { $sql = new Sql($that->dbh); $users = new Table('users'); $users->postInsert(function() {Storage::increment();}); $that->assertEquals(0, Storage::get()); $actual = $sql->insert($users, array('uid' => 7, 'name' => 'Gonzalo', 'surname' => 'Ayuso')); $that->assertTrue($actual); $that->assertEquals(1, Storage::get()); }); } public function testInsertWithPrePostInsert() { Storage::init(); $that = $this; $this->dbh->transactional(function($dbh) use ($that) { $sql = new Sql($that->dbh); $users = new Table('users'); $users->preInsert(function() {Storage::increment();}); $users->postInsert(function() {Storage::decrement();}); $that->assertEquals(0, Storage::get()); $actual = $sql->insert($users, array('uid' => 7, 'name' => 'Gonzalo', 'surname' => 'Ayuso')); $that->assertTrue($actual); $that->assertEquals(0, Storage::get()); }); } public function testUpdateWithPrePostInsert() { Storage::init(); $that = $this; $this->dbh->transactional(function($dbh) use ($that) { $sql = new Sql($that->dbh); $users = new Table('users'); $users->preUpdate(function() {Storage::increment();}); $users->postUpdate(function() {Storage::increment();}); $that->assertEquals(0, Storage::get()); $actual = $sql->insert($users, array('uid' => 7, 'name' => 'Gonzalo', 'surname' => 'Ayuso')); $that->assertTrue($actual); $that->assertEquals(0, Storage::get()); $data = $sql->select('users', array('uid' => 7)); $that->assertEquals('Gonzalo', $data[0]['name']); $actual = $sql->update($users, array('name' => 'gonzalo',), array('uid' => 7)); $that->assertTrue($actual); $that->assertEquals(2, Storage::get()); $data = $sql->select('users', array('uid' => 7)); $that->assertEquals('gonzalo', $data[0]['name']); }); } public function testDeleteWithPrePostInsert() { Storage::init(); $that = $this; $this->dbh->transactional(function($dbh) use ($that) { $sql = new Sql($that->dbh); $users = new Table('users'); $users->preDelete(function() {Storage::increment();}); $users->postDelete(function() {Storage::increment();}); $that->assertEquals(0, Storage::get()); $actual = $sql->insert($users, array('uid' => 7, 'name' => 'Gonzalo', 'surname' => 'Ayuso')); $that->assertTrue($actual); $that->assertEquals(0, Storage::get()); $actual = $sql->delete($users, array('uid' => 7)); $that->assertTrue($actual); $that->assertEquals(2, Storage::get()); }); } }
And here the whole library:
class Conn extends PDO { private $forcedRollback = false; public function transactional(Closure $func) { $this->beginTransaction(); try { $func($this); $this->forcedRollback ? $this->rollback() : $this->commit(); } catch (Exception $e) { $this->rollback(); throw $e; } } public function forceRollback() { $this->forcedRollback = true; } } class Table { private $tableName; function __construct($tableName) { $this->tableName = $tableName; } private $cbkPostInsert; private $cbkPostUpdate; private $cbkPostDelete; private $cbkPreInsert; private $cbkPreUpdate; private $cbkPreDelete; public function getTableName() { return $this->tableName; } public function postInsert(Closure $func) { $this->cbkPostInsert = $func; } public function postUpdate(Closure $func) { $this->cbkPostUpdate = $func; } public function postDelete(Closure $func) { $this->cbkPostDelete = $func; } public function preInsert(Closure $func) { $this->cbkPreInsert = $func; } public function preUpdate(Closure $func) { $this->cbkPreUpdate = $func; } public function preDelete(Closure $func) { $this->cbkPreDelete = $func; } public function execPostInsert($values) { $func = $this->cbkPostInsert; if ($this->cbkPostInsert instanceof Closure) $func($values); } public function execPostUpdate($values, $where) { $func = $this->cbkPostUpdate; if ($func instanceof Closure) $func($values, $where); } public function execPostDelete($where) { $func = $this->cbkPostDelete; if ($func instanceof Closure) $func($where); } public function execPreInsert($values) { $func = $this->cbkPreInsert; if ($func instanceof Closure) $func($values); } public function execPreUpdate($values) { $func = $this->cbkPreUpdate; if ($func instanceof Closure) $func($values); } public function execPreDelete($where) { $func = $this->cbkPreDelete; if ($func instanceof Closure) $func($where); } } class Sql { /** @var Conn */ private $dbh; function __construct(Conn $dbh) { $this->dbh = $dbh; } public function select($table, $where) { $tableName = ($table instanceof Table) ? $table->getTableName() : $table; $sql = $this->createSelect($tableName, $where); $whereParams = $this->getWhereParameters($where); $stmp = $this->dbh->prepare($sql); $stmp->execute($whereParams); return $stmp->fetchAll(); } public function insert($table, $values) { $tableName = ($table instanceof Table) ? $table->getTableName() : $table; $sql = $this->createInsert($tableName, $values); if ($table instanceof Table) $table->execPreInsert($values); $stmp = $this->dbh->prepare($sql); $out = $stmp->execute($values); if ($table instanceof Table) $table->execPostInsert($values); return $out; } public function update($table, $values, $where) { $tableName = ($table instanceof Table) ? $table->getTableName() : $table; $sql = $this->createUpdate($tableName, $values, $where); $whereParams = $this->getWhereParameters($where); if ($table instanceof Table) $table->execPreUpdate($values, $where); $stmp = $this->dbh->prepare($sql); $out = $stmp->execute(array_merge($values, $whereParams)); if ($table instanceof Table) $table->execPostUpdate($values, $where); return $out; } public function delete($table, $where) { $tableName = ($table instanceof Table) ? $table->getTableName() : $table; $sql = $this->createDelete($tableName, $where); $whereParams = $this->getWhereParameters($where); if ($table instanceof Table) $table->execPreDelete($where); $stmp = $this->dbh->prepare($sql); $out = $stmp->execute($whereParams); if ($table instanceof Table) $table->execPostDelete($where); return $out; } protected function getWhereParameters($where) { $whereParams = array(); foreach ($where as $key => $value) { $whereParams[":W_{$key}"] = $value; } return $whereParams; } protected function createSelect($table, $where) { return "SELECT * FROM " . $table . $this->createSqlWhere($where); } protected function createUpdate($table, $values, $where) { $sqlValues = array(); foreach (array_keys($values) as $key) { $sqlValues[] = "{$key} = :{$key}"; } return "UPDATE {$table} SET " . implode(', ', $sqlValues) . $this->createSqlWhere($where); } protected function createInsert($table, $values) { $sqlValues = array(); foreach (array_keys($values) as $key) { $sqlValues[] = ":{$key}"; } return "INSERT INTO {$table} (" . implode(', ', array_keys($values)) . ") VALUES (" . implode(', ', $sqlValues) . ")"; } protected function createDelete($table, $where) { return "DELETE FROM {$table}" . $this->createSqlWhere($where); } protected function createSqlWhere($where) { if (count((array) $where) == 0) return null; $whereSql = array(); foreach ($where as $key => $value) { $whereSql[] = "{$key} = :W_{$key}"; } return ' WHERE ' . implode(' AND ', $whereSql); } }
My carpal tunnel addled wrists ache just looking at this; reinvent the wheel much?
Two things I would do with the Table/Sql classes:
– Allow multiple callbacks
– Dynamic checking of table fields when inserting/updating generating where conditions
– Allow the select function to accept the fields the program needs(like $sql->select(‘users’, array(‘id’, ‘username’), array(‘password’ => hash(‘256’, $password)) )
Thanks for the comments. I don’t understand the second one, can you show me an example?
According to the third one. I’ve got an internal fight with it. Let me explain it a little bit. This script is based on another one that I wrote time ago. In this library I use the pattern you recommend. I Understand that it’s more flexible. But what happens? When want to use ‘*’ (to select all fields) I always forget to set the second argument. I’ve working with one JavaScript library to use WebSQL and the author of this library removed this second argument. Maybe we can move it to the end as a optional parameter, but after reading the book the “Clean Code” optional arguments are a little bit ugly 🙂