-
Notifications
You must be signed in to change notification settings - Fork 28
Custom Cast/convert in Eloquent #992
Description
I am trying to figure out a way to save binary/varbinary MSSQL types with Eloquent (sqlsrv driver + doctrine/dbal v2.5.5).
So far I figured out a way to prepare statement with conversion and a placeholder, but it involves manually calling DB::statement, instead of $model->save(), which is much more desirable.
DB::statement(
"INSERT INTO files (file_name, content, ...) VALUES (?, CONVERT(VARBINARY(MAX) ?), ...));", [
$file->file_name,
base64_encode(file_get_contents($file->file_path))
]);
Notice this is a prepared statement, I call CONVERT(VARBINARY(MAX), ?) and then I feed my values afterwards.
If I do it using something like setContentAttribute on a model and have it defined as
function setContentAttribute ($value) {
$this->attributes['content'] = DB::raw("CONVERT(VARBINARY(MAX), '" . base64_encode($value) . "')");
}
it has two drawbacks:
- will not be a prepared statement value anymore,
- for some reason it is being executed extremely slow, (like a second per 100kB slow).
Looking at the code I found this:
Query\Builder :: insert
// Finally, we will run this query against the database connection and return
// the results. We will need to also flatten these bindings before running
// the query so they are all in one huge, flattened array for execution.
return $this->connection->insert(
$this->grammar->compileInsert($this, $values),
$this->cleanBindings(Arr::flatten($values, 1))
);
Grammars\Grammar :: compileInsert
// We need to build a list of parameter place-holders of values that are bound
// to the query. Each insert should have the exact same amount of parameter
// bindings so we will loop through the record and parameterize them all.
$parameters = collect($values)->map(function ($record) {
return '('.$this->parameterize($record).')';
})->implode(', ');
return "insert into $table ($columns) values $parameters";
Grammars\Grammar :: parameterize and parameter
/**
* Create query parameter place-holders for an array.
*
* @param array $values
* @return string
*/
public function parameterize(array $values)
{
return implode(', ', array_map([$this, 'parameter'], $values));
}
/**
* Get the appropriate query parameter place-holder for a value.
*
* @param mixed $value
* @return string
*/
public function parameter($value)
{
return $this->isExpression($value) ? $this->getValue($value) : '?';
}
So parameters can be easily parametarised, I can actually have my setContentAttribute, which should set $this->attributes['content'] = DB::raw("CONVERT(VARBINARY(MAX), ?)");,
but then I would need to get the value from the Expression as well. So far the
Query\Builder :: cleanBindings just ignores Expression instance.
/**
* Remove all of the expressions from a list of bindings.
*
* @param array $bindings
* @return array
*/
protected function cleanBindings(array $bindings)
{
return array_values(array_filter($bindings, function ($binding) {
return ! $binding instanceof Expression;
}));
}
From what I sort of understand we can modify Expression to not only have getValue, but also have something like getParameter, switch the logic around and make Expression->getParamter return CONVERT(VARBINARY(MAX), ?) and a value as Expression->getValue return actual string value.
What do you think?