Skip to content

Commit

Permalink
expand array parameters to list of placeholders
Browse files Browse the repository at this point in the history
  • Loading branch information
jakubkulhan committed May 29, 2024
1 parent f06e276 commit 8ecaa94
Show file tree
Hide file tree
Showing 23 changed files with 346 additions and 47 deletions.
3 changes: 2 additions & 1 deletion data-access-kit/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
],
"require": {
"php": ">=8.3",
"doctrine/dbal": "^3.0|^4.0"
"doctrine/dbal": "^3.0|^4.0",
"doctrine/annotations": "^2.0"
},
"require-dev": {
"phpunit/phpunit": "^11.1",
Expand Down
27 changes: 17 additions & 10 deletions data-access-kit/src/DefaultValueConverter.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@

namespace DataAccessKit;

use DateTimeImmutable;
use DateTimeZone;
use DataAccessKit\Attribute\Column;
use DataAccessKit\Attribute\Table;
use DateTime;
use DateTimeImmutable;
use DateTimeZone;
use ReflectionNamedType;
use function in_array;
use function json_decode;
Expand All @@ -21,10 +22,12 @@ public function objectToDatabase(Table $table, Column $column, mixed $value): mi
}

$valueType = $column->reflection->getType();
if ($valueType instanceof ReflectionNamedType && in_array($valueType->getName(), ["array", "object"], true)) {
$value = json_encode($value);
} else if ($valueType instanceof ReflectionNamedType && in_array($valueType->getName(), ["DateTime", "DateTimeImmutable"], true)) {
$value = $value->format("Y-m-d H:i:s");
if ($valueType instanceof ReflectionNamedType) {
if (in_array($valueType->getName(), ["array", "object"], true)) {
$value = json_encode($value);
} else if (in_array($valueType->getName(), [DateTime::class, DateTimeImmutable::class], true)) {
$value = $value->format("Y-m-d H:i:s");
}
}

return $value;
Expand All @@ -37,10 +40,14 @@ public function databaseToObject(Table $table, Column $column, mixed $value): mi
}

$valueType = $column->reflection->getType();
if ($valueType instanceof ReflectionNamedType && in_array($valueType->getName(), ["array", "object"])) {
$value = json_decode($value, true);
} else if ($valueType instanceof ReflectionNamedType && in_array($valueType->getName(), ["DateTime", "DateTimeImmutable"], true)) {
$value = new DateTimeImmutable($value, new DateTimeZone("UTC"));
if ($valueType instanceof ReflectionNamedType) {
if (in_array($valueType->getName(), ["array", "object"])) {
$value = json_decode($value, true);
} else if ($valueType->getName() === DateTime::class) {
$value = DateTime::createFromFormat("Y-m-d H:i:s", $value, new DateTimeZone("UTC"));
} else if ($valueType->getName() === DateTimeImmutable::class) {
$value = DateTimeImmutable::createFromFormat("Y-m-d H:i:s", $value, new DateTimeZone("UTC"));
}
}

return $value;
Expand Down
133 changes: 109 additions & 24 deletions data-access-kit/src/Repository/Method/SQLMethodCompiler.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
namespace DataAccessKit\Repository\Method;

use DataAccessKit\Attribute\Column;
use DataAccessKit\PersistenceInterface;
use DataAccessKit\Registry;
use DataAccessKit\Repository\Attribute\SQL;
use DataAccessKit\Repository\Compiler;
Expand All @@ -13,17 +12,25 @@
use DataAccessKit\Repository\MethodCompilerInterface;
use DataAccessKit\Repository\Result;
use DataAccessKit\Repository\ResultMethod;
use Doctrine\Common\Annotations\PhpParser;
use LogicException;
use ReflectionNamedType;
use ReflectionParameter;
use function array_keys;
use function array_map;
use function array_search;
use function chr;
use function count;
use function ctype_lower;
use function explode;
use function implode;
use function in_array;
use function preg_match;
use function preg_quote;
use function preg_replace_callback;
use function preg_split;
use function sprintf;
use function ucfirst;

/**
* @implements MethodCompilerInterface<SQL>
Expand All @@ -32,10 +39,15 @@ class SQLMethodCompiler implements MethodCompilerInterface
{
use CreateConstructorTrait;

private const DELIMITER = "\0";

private PhpParser $phpParser;

public function __construct(
private readonly Registry $registry,
)
{
$this->phpParser = new PhpParser();
}

public function compile(Result $result, ResultMethod $method, $attribute): void
Expand All @@ -51,34 +63,81 @@ public function compile(Result $result, ResultMethod $method, $attribute): void

$constructor = $this->createConstructorWithPersistenceProperty($result);

if ($method->reflection->getNumberOfParameters() > 0) {
$argumentsProperty = $method->name . "Arguments";
$result->property($argumentsProperty)
$nonArrayArgumentsPropertyName = $method->reflection->getName() . "Arguments";
$argumentsProperties = [];
foreach ($method->reflection->getParameters() as $rp) {
if ($rp->getType() instanceof ReflectionNamedType && $rp->getType()->getName() === "array") {
if (!preg_match(
'/@param\s+(?:
(?:array|list)<\s*(?:[^,>]+,\s*)?(?P<arrayValueType>[^,>]+)>
|
(?P<itemType>[^[]+)\[]
)\s+\$' . preg_quote($rp->getName(), '/') . '/xi',
$method->reflection->getDocComment() ?: "",
$m,
)) {
throw new CompilerException(sprintf(
"Method [%s::%s] has parameter \$%s of type array, but does not have valid @param annotation for it. Please provide type for the array items.",
$result->reflection->getName(),
$method->reflection->getName(),
$rp->getName(),
));
}
if (!empty($m["arrayValueType"])) {
$phpType = $m["arrayValueType"];
} else if (!empty($m["itemType"])) {
$phpType = $m["itemType"];
} else {
throw new LogicException("Unreachable statement.");
}

$useStatements = $this->phpParser->parseUseStatements($result->reflection);
if (isset($useStatements[$phpType])) {
$phpType = $result->use($useStatements[$phpType]);
} else if (!ctype_lower($phpType[0])) {
$phpType = $result->use($phpType);
}

$propertyName = $method->reflection->getName() . ucfirst($rp->getName()) . "ArgumentItem";
$name = "value";

} else {
$phpType = Compiler::phpType($result, $rp->getType());
$propertyName = $nonArrayArgumentsPropertyName;
$name = $rp->getName();
}

if (!isset($argumentsProperties[$propertyName])) {
$argumentsProperties[$propertyName] = [];
}
$argumentsProperties[$propertyName][] = "#[{$result->use(Column::class)}(name: \"{$name}\")]";
$argumentsProperties[$propertyName][] = "public {$phpType} \${$name};";
}

foreach ($argumentsProperties as $name => $anonymousClassLines) {
$result->property($name)
->setVisibility("private")
->setType("object");
$columnAlias = $result->use(Column::class);
$constructor->line("\$this->{$argumentsProperty} = new class {")->indent();
foreach ($method->reflection->getParameters() as $rp) {
$phpType = Compiler::phpType($result, $rp->getType());
$constructor->line("#[{$columnAlias}(name: \"{$rp->getName()}\")]");
$constructor->line("public {$phpType} \$" . $rp->getName() . ";");
$constructor->line("\$this->{$name} = new class {")->indent();
foreach ($anonymousClassLines as $line) {
$constructor->line($line);
}
$constructor->dedent()->line("};");
}

[$sql, $sqlParameters] = $this->expandSQLMacrosAndVariables($method, $result, $attribute);

if ($method->reflection->getNumberOfParameters() > 0) {
$method->line("\$arguments = clone \$this->{$argumentsProperty};");
if (isset($argumentsProperties[$nonArrayArgumentsPropertyName])) {
$method->line("\$arguments = clone \$this->{$nonArrayArgumentsPropertyName};");
foreach ($method->reflection->getParameters() as $rp) {
$method->line("\$arguments->{$rp->getName()} = \$" . $rp->getName() . ";");
}
$method->line("\$arguments = \$this->persistence->toRow(\$arguments);");
$method->line();
}

[$sqlPartExpressions, $sqlParameterExpression] = $this->expandSQLMacrosAndVariables($method, $result, $attribute);

if ($returnType->getName() === "void") {
$method->line("\$this->persistence->execute(" . Compiler::varExport($sql) . ", [" . implode(", ", $sqlParameters) . "]);");
$method->line("\$this->persistence->execute(" . implode(" . ", $sqlPartExpressions) . ", [" . implode(", ", $sqlParameterExpression) . "]);");

} else if ($returnType->isBuiltin() && !in_array($returnType->getName(), ["array", "iterable"], true)) {
if (!in_array($returnType->getName(), ["int", "float", "string", "bool"], true)) {
Expand All @@ -89,7 +148,7 @@ public function compile(Result $result, ResultMethod $method, $attribute): void
$returnType->getName(),
));
}
$method->line("\$result = \$this->persistence->selectScalar(" . Compiler::varExport($sql) . ", [" . implode(", ", $sqlParameters) . "]);");
$method->line("\$result = \$this->persistence->selectScalar(" . implode(" . ", $sqlPartExpressions) . ", [" . implode(", ", $sqlParameterExpression) . "]);");
if ($returnType->allowsNull()) {
$method->line("return \$result === null ? null : ({$returnType->getName()})\$result;");
} else {
Expand All @@ -116,7 +175,7 @@ public function compile(Result $result, ResultMethod $method, $attribute): void
$itemAlias = $itemType;
}

$method->line("\$result = \$this->persistence->select({$itemAlias}::class, " . Compiler::varExport($sql) . ", [" . implode(", ", $sqlParameters) . "]);");
$method->line("\$result = \$this->persistence->select({$itemAlias}::class, " . implode(" . ", $sqlPartExpressions) . ", [" . implode(", ", $sqlParameterExpression) . "]);");
$method->line();
if ($returnType->getName() === "iterable") {
$method->line("return \$result;");
Expand Down Expand Up @@ -151,7 +210,7 @@ private function expandSQLMacrosAndVariables(ResultMethod $method, Result $resul
$reflectionParametersByName[$rp->getName()] = $rp;
}

$sqlParameters = [];
$sqlParameterExpressions = [];
$usedVariables = [];
$sql = preg_replace_callback(
'/
Expand All @@ -166,7 +225,7 @@ private function expandSQLMacrosAndVariables(ResultMethod $method, Result $resul
|
%(?P<macro>[a-zA-Z0-9_]+\b(?:\([^)]*\))?)
/xi',
static function ($m) use ($result, $method, $table, $reflectionParametersByName, &$sqlParameters, &$usedVariables) {
static function ($m) use ($result, $method, $table, $reflectionParametersByName, &$sqlParameterExpressions, &$usedVariables) {
if (!empty($m["variable"])) {
$name = $m["variable"];
if (!isset($reflectionParametersByName[$name])) {
Expand All @@ -177,12 +236,29 @@ static function ($m) use ($result, $method, $table, $reflectionParametersByName,
$name,
));
}
/** @var ReflectionParameter $rp */
$rp = $reflectionParametersByName[$name];

$sqlParameters[] = '$arguments[' . Compiler::varExport($rp->getName()) . ']';
$usedVariables[$name] = true;

return "?";
if ($rp->getType() instanceof ReflectionNamedType && $rp->getType()->getName() === "array") {
$argumentVariableName = "argument" . ucfirst($rp->getName());
$method
->line("\${$argumentVariableName} = [];")
->line("foreach (\${$rp->getName()} as \$item) {")
->indent()
->line("\$itemObject = clone \$this->" . $method->reflection->getName() . ucfirst($rp->getName()) . "ArgumentItem;")
->line("\$itemObject->value = \$item;")
->line("\${$argumentVariableName}[] = \$this->persistence->toRow(\$itemObject)['value'];")
->dedent()
->line("}");

$sqlParameterExpressions[] = "...\${$argumentVariableName}";
return static::DELIMITER . $argumentVariableName . static::DELIMITER;

} else {
$sqlParameterExpressions[] = '$arguments[' . Compiler::varExport($rp->getName()) . ']';
return "?";
}

} else if (!empty($m["table"])) {
return $table->name;
Expand Down Expand Up @@ -247,9 +323,18 @@ static function ($m) use ($result, $method, $table, $reflectionParametersByName,
}
}

$sqlPartExpressions = [];
foreach (explode(static::DELIMITER, $sql) as $index => $part) {
if ($index % 2 === 0) {
$sqlPartExpressions[] = Compiler::varExport($part);
} else {
$sqlPartExpressions[] = "(count(\${$part}) === 0 ? 'NULL' : '?' . str_repeat(', ?', count(\${$part}) - 1))";
}
}

return [
$sql,
$sqlParameters,
$sqlPartExpressions,
$sqlParameterExpressions,
];
}
}
6 changes: 6 additions & 0 deletions data-access-kit/test/Repository/CompilerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
use DataAccessKit\Registry;
use DataAccessKit\Repository\Exception\CompilerException;
use DataAccessKit\Repository\Fixture\AbsoluteSQLFileRepositoryInterface;
use DataAccessKit\Repository\Fixture\VariableArrayDocCommentMissingParamSQLRepositoryInterface;
use DataAccessKit\Repository\Fixture\VariableArrayNoDocCommentSQLRepositoryInterface;
use DataAccessKit\Repository\Fixture\VariableArraySQLRepositoryInterface;
use DataAccessKit\Repository\Fixture\CountBadParameterNameRepositoryInterface;
use DataAccessKit\Repository\Fixture\CountBadReturnTypeRepositoryInterface;
use DataAccessKit\Repository\Fixture\CountRepositoryInterface;
Expand Down Expand Up @@ -113,6 +116,7 @@ public static function provideCompile()
SimpleSQLObjectRepositoryInterface::class,
SimpleSQLNullableObjectRepositoryInterface::class,
VariableSQLRepositoryInterface::class,
VariableArraySQLRepositoryInterface::class,
NullableScalarSQLRepositoryInterface::class,
VoidSQLRepositoryInterface::class,
MacroTableSQLRepositoryInterface::class,
Expand Down Expand Up @@ -165,6 +169,8 @@ public static function provideCompileError(): iterable
UnsupportedReturnTypeMixedRepositoryInterface::class,
UnsupportedReturnTypeObjectRepositoryInterface::class,
UnknownVariableSQLRepositoryInterface::class,
VariableArrayNoDocCommentSQLRepositoryInterface::class,
VariableArrayDocCommentMissingParamSQLRepositoryInterface::class,
MacroColumnsExceptUnknownColumnRepositoryInterface::class,
MacroColumnsExceptAllColumnRepositoryInterface::class,
MacroUnknownRepositoryInterface::class,
Expand Down
4 changes: 4 additions & 0 deletions data-access-kit/test/Repository/Fixture/Foo.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use DataAccessKit\Attribute\Column;
use DataAccessKit\Attribute\Table;
use DateTimeImmutable;

#[Table]
class Foo
Expand All @@ -17,5 +18,8 @@ class Foo
#[Column]
public string $description;

#[Column]
public DateTimeImmutable $createdAt;

public int $notColumn;
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,6 @@
#[Repository(Foo::class)]
interface MacroColumnsExceptAllColumnRepositoryInterface
{
#[SQL("SELECT %columns(except id, title, description) FROM foos")]
#[SQL("SELECT %columns(except id, title, description, created_at) FROM foos")]
public function allColumnsExcept(): iterable;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php declare(strict_types=1);

namespace DataAccessKit\Repository\Fixture;

use DataAccessKit\Repository\Attribute\Repository;
use DataAccessKit\Repository\Attribute\SQL;

#[Repository(Foo::class)]
interface VariableArrayDocCommentMissingParamSQLRepositoryInterface
{
/**
* Find by ids.
*
* @return Foo[]
*/
#[SQL("SELECT * FROM foo WHERE id IN (@ids)")]
public function findByIds(array $ids): array;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php declare(strict_types=1);

namespace DataAccessKit\Repository\Fixture;

use DataAccessKit\Repository\Attribute\Repository;
use DataAccessKit\Repository\Attribute\SQL;

#[Repository(Foo::class)]
interface VariableArrayNoDocCommentSQLRepositoryInterface
{
#[SQL("SELECT * FROM foo WHERE id IN (@ids)")]
public function findByIds(array $ids): array;
}
Loading

0 comments on commit 8ecaa94

Please sign in to comment.