Skip to content

Instantly share code, notes, and snippets.

@nicmart
Last active August 29, 2015 14:21
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nicmart/f15bb3be54a38a31375a to your computer and use it in GitHub Desktop.
Save nicmart/f15bb3be54a38a31375a to your computer and use it in GitHub Desktop.
StatementBuilder

StatementBuilder

This library was born to improve the abstraction in the conversion of an Expression to a SQL statement, but it is completely Expression and SQL agnostic, so it can be used for simlar use-cases.

The problem

Let's say you have a type FieldConverter that converts a FieldExpression object to a prepared statement. That is basically a function type

FieldConverter: FieldExpression => PreparedStatement

For the sake of the argument, let's assume that PreparedStatement is just a string with placeholders, without the PDO values bindings. Let's assume you use it in the following code

$fieldConverter = new FieldConverter;
$field1 = new FieldExpression("field1", "=", "value1");
$field2 = new FieldExpression("field2", "=", "value2");

// This gives the statement "field1 = :param1"
$statement1 = $fieldConverter->convert($field1);
// And this gives "field2 = :param1"
$statement2 = $fieldConverter->convert($field1);

As you can see, since the converter is completely side-effects free, as it should be, it is returning the same placeholder for the two statements. And that's fine, since they are completely different statements. The problem arises when we try to define a CompositeExpressionConverter, i.e. something that convert a CompositeExpression (like an AND) to a prepared statement.

A CompositeExpression is defined like this:

$and = new CompositeExpression("and", [$field1, $field2]);

Now when we try to convert it using a FieldConverter for children, we are in trouble. If we do like this

class CompositeExpressionConverter
{
	public function __constructor(FieldConverter $fieldConverter) { ... }

	public function convert(CompositeFieldExpression $compositeExpr)
	{
		list($field1, $field2) = $compositeExpr->getChildren();
		return sprintf(
			"%s AND %s",
			$this->fieldConverter->convert($field1),
			$this->fieldConverter->convert($field2)
		);
	}

	

Then the code

$compositeConverter = new CompositeExpressionConverter;
$compositeConverter->convert($and);

gives you, in the last line,

"field1 = :param1 AND field2 = :param1"

The placeholders are the same! Here you can see what the problem is: since the FieldConverter is completely immutable, he can't understand that he is converting expressions that are part of a composite one, and he is not able to give different placeholders.

If you want to preserve immutability, there is no way of solving this without passing the placeholder name from the outside. But this would just move the problem from the FieldConverter to the CompositeExpressionConverter, making the latter mutable. And we don't want that, since we do want to use the same converter in all around our application, without caring about its state.

The functional solution

Let's extract the placeholder generation from the FieldConverter in a function of type

Binding: (string, int) => string 

where the first argument is the value of the field and the second one is its PDO Type. The function will care of the placeholder generation and value bindings. Having moved the placeholder generation inside that function type, we can now free the FieldConverter from the placeholders simply changing its signature:

FieldConverter: FieldExpression => (Binding => PreparedStatement)

and the same thing for the CompositeExpressionConverter:

CompositeExpressionConverter: CompositeExpression => (Binding => PreparedStatement)

So we are not more returning a string, but a function that given a Binding function returns a PreparedStatement. In other words, instead of returning directly a PreparedStatement we are returning a way of building it.

Let's abstract the things a bit and define them both as a Statement type:

Binding: Something => String
Statement: Something => (Binding => String)
// Or, equivalently, expanding the Binding type
Statement: Something => ((Something => String) => String)

Where Something in our case is the type of the couples ($value, $pdoType), and in general is the input type of the binding function.

Assuming that we have defined a PDOBindingFunction somewhere, and adapted the converters to the new signature, the code that generates PDO statements will be

$statement1 = $fieldConverter->convert($field1);
$statement2 = $fieldConverter->convert($field2);
$andStatement = $compositeConverter->convert($and);

// Prints "field1 = :param1", as before
echo $statement1(new PDOBindingFunction);

// Prints "field2 = :param1", as before
echo $statement2(new PDOBindingFunction);

// Prints "field1 = :param1 AND field2 = :param2", Correct!
echo $andStatement(new PDOBindingFunction);

The and statement is now converted correctly, and the converters are still side-effects free! The side-effects, like maintaining the counter for place-holders suffixes or putting bindings somewhere, have been moved to the binding function. So the dirty stuff has been completed extracted from the statements conversion.

What we got for free

Another benefit we got from this type transition is that we are now able to choose the SQL conversion strategy we want, without affecting the statement generation. For example, maybe we don't want prepared statements with placeholders, but we want a plain old SQL statement, with all the values escaped safely accordingly to their types and the db connection. Now that is just a matter of using a different Binding function:

$binding = new PlainSQLBindingFunction($connection);

// Prints "field1 = 'value1'"
echo $statement1($binding);

// Prints "field2 = 'value2'"
echo $statement2($binding);

// Prints "field1 = 'value1' AND field2 = 'value2'"
echo $andStatement($binding);

How to use the library

The statement

The statement is defined as an interface with one method:

namespace Dxi\Core\StatementBuilder\Statement;

interface Statement
{
    /**
     * @param callable $bindingFunction
     * @return string
     */
    public function __invoke($bindingFunction);
}

There is only one implementation of the interface in the library, the CallbackStatement class, that take a callback and set it as the implementation of the __invoke method.

For example, to get a statement that returns you the current time every time it is binded, you would do

use Dxi\Core\StatementBuilder\Statement\Statement;

$time = new CallbackStatement(function ($binding) {
	return "The time is " . $binding();
});

$binding = 'time'; //The time PHP built-in function

// Prints 1432213140
echo $time($binding);
sleep(5);
// Prints 1432213145
echo $time($binding);

The Builder

The Builder gives you a set of higher-order functions to combine statements in other statements.

use Dxi\Core\StatementBuilder\Builder\Builder;

$binding = 'rand';
$builder = new Builder;

// Value statement on binding will call the binding function with the given args:
$numberFrom1To100 = $builder->valueStatement(0, 100);

$numberFrom1To100($binding); // Returns 45
$numberFrom1To100($binding); // Returns 87

// Simple statement combine an existing statement into a string using sprintf
$stringWithNumber = $builder->simpleStatement("The next number is %s", $numberFrom1To100);
// Returns "The next number is 12";
$stringWithNumber($binding); 

// Composite Statement: combine different statements using a sprintf-like template
$composite = $builder->compositeStatement(
	"The numbers are %s and %s",
	$numberFrom1To100,
	$numberFrom1To100
);
// Returns "The numbers are 1 and 56"
$composite($binding);

$implode = $builder->implode(", ", [$numberFrom1To100, $numberFrom1To100, numberFrom1To100]);
// Returns "89, 43, 67"
$implode($binding);

Some predefined binding functions

Array binding function

The array binding function returns placeholders with an inrementing numeric suffix and put the bindings in an array as a side-effect:

use Dxi\Core\StatementBuilder\Binding\ArrayBindingFunction;

$bindings = [];
$arrayBinding = new ArrayBindingFunction($bindings);

$simpleCondition = $builder->simpleStatement("table.column = %s", 1234, \PDO::PARAM_INT);

// $prepared will be "table.column = :param1"
$prepared = $simpleCondition($arrayBinding);

// bindings will be
// [ "param1" => new ValueWithType(1234, \PDO::PARAM_INT)]

DoctrineQueryBuilderBindingFunction

This builder uses the createNamedParameter method of the given DBAL DoctrineQueryBuilder

use Dxi\Core\StatementBuilder\Binding\DoctrineQueryBuilderBindingFunction;

$doctrineBinding = new DoctrineQueryBuilderBindingFunction($doctrineQueryBuilder);
$simpleCondition($doctrineBinding);

SimpleBindingFunction

This is a simple builder used for testing. It escapes values using addslashes, so it is not SQL injection safe.

use Dxi\Core\StatementBuilder\Binding\SimpleBindingFunction;

$simpleBinding = new SimpleBindingFunction;
// Returns "table.column = 1234
$simpleCondition($simpleBinding);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment