Skip to content

Commit

Permalink
Merge pull request #15 from BaguettePHP/feature/type-interface
Browse files Browse the repository at this point in the history
Add TypeInterface for escape to non-standard values
  • Loading branch information
zonuexe authored May 30, 2022
2 parents ca07f33 + fde8158 commit 59dde9d
Show file tree
Hide file tree
Showing 7 changed files with 281 additions and 3 deletions.
61 changes: 61 additions & 0 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
parameters:
ignoreErrors:
-
message: "#^Generic type Teto\\\\SQL\\\\PDOAggregate\\<PDO\\|Teto\\\\SQL\\\\PDOInterface\\<PDOStatement\\|Teto\\\\SQL\\\\PDOStatementInterface\\>\\> in PHPDoc tag @param for parameter \\$pdo does not specify all template types of interface Teto\\\\SQL\\\\PDOAggregate\\: S, T$#"
count: 2
path: src/AbstractStaticQuery.php

-
message: "#^Type PDO\\|Teto\\\\SQL\\\\PDOInterface\\<PDOStatement\\|Teto\\\\SQL\\\\PDOStatementInterface\\> in generic type Teto\\\\SQL\\\\PDOAggregate\\<PDO\\|Teto\\\\SQL\\\\PDOInterface\\<PDOStatement\\|Teto\\\\SQL\\\\PDOStatementInterface\\>\\> in PHPDoc tag @param for parameter \\$pdo is not subtype of template type S of PDOStatement\\|Teto\\\\SQL\\\\PDOStatementInterface of interface Teto\\\\SQL\\\\PDOAggregate\\.$#"
count: 2
path: src/AbstractStaticQuery.php

-
message: "#^Method Teto\\\\SQL\\\\Processor\\\\CallbackProcessor\\:\\:processQuery\\(\\) should return string but returns mixed\\.$#"
count: 1
path: src/Processor/CallbackProcessor.php

-
message: "#^Property Teto\\\\SQL\\\\Processor\\\\CallbackProcessor\\:\\:\\$callback with generic interface Teto\\\\SQL\\\\PDOInterface does not specify its types\\: T$#"
count: 1
path: src/Processor/CallbackProcessor.php

-
message: "#^Parameter \\#2 \\$callback of function preg_replace_callback expects callable\\(array\\<int\\|string, string\\>\\)\\: string, Closure\\(array\\)\\: int\\|string given\\.$#"
count: 1
path: src/Processor/PregCallbackReplacer.php

-
message: "#^Parameter \\#2 \\$matches of method Teto\\\\SQL\\\\ReplacerInterface\\:\\:replaceQuery\\(\\) expects array\\<non\\-empty\\-string, string\\>, array\\<int\\|string, string\\> given\\.$#"
count: 1
path: src/Processor/PregCallbackReplacer.php

-
message: "#^Argument of an invalid type mixed supplied for foreach, only iterables are supported\\.$#"
count: 1
path: src/Type/PgIdentifier.php

-
message: "#^Method Teto\\\\SQL\\\\Type\\\\PgIdentifier\\:\\:escapeValue\\(\\) should return int\\|string but return statement is missing\\.$#"
count: 1
path: src/Type/PgIdentifier.php

-
message: "#^Parameter \\#1 \\$value of method Teto\\\\SQL\\\\Type\\\\PgIdentifier\\:\\:quote\\(\\) expects string, mixed given\\.$#"
count: 1
path: src/Type/PgIdentifier.php

-
message: "#^Property Teto\\\\SQL\\\\Type\\\\PgIdentifier\\:\\:\\$types has no type specified\\.$#"
count: 1
path: src/Type/PgIdentifier.php

-
message: "#^Part \\$value \\(mixed\\) of encapsed string cannot be cast to string\\.$#"
count: 1
path: tests/Replacer/Sample/DummyType.php

-
message: "#^Method Teto\\\\SQL\\\\Type\\\\PgIdentifierTest\\:\\:escapeValuesProvider\\(\\) should return array\\<array\\{string, string\\}\\> but returns array\\{array\\{'', '@column', '\"\"'\\}, array\\{array\\{'foo'\\}, '@column\\[\\]', '\"foo\"'\\}, array\\{array\\{'foo', 'bar'\\}, '@column\\[\\]', '\"foo\",\"bar\"'\\}, array\\{array\\{foo\\: 'bar'\\}, '@column\\[\\]', 'foo AS \"bar\"'\\}, array\\{array\\{'\"foo\"'\\: 'bar'\\}, '@column\\[\\]', '\"foo\" AS \"bar\"'\\}, array\\{array\\{foo\\: null\\}, '@column\\[\\]', 'foo'\\}, array\\{array\\{'\"foo\"'\\: null\\}, '@column\\[\\]', '\"foo\"'\\}, array\\{array\\{foo\\: null, bar\\: 'buz'\\}, '@column\\[\\]', 'foo,bar AS \"buz\"'\\}, \\.\\.\\.\\}\\.$#"
count: 1
path: tests/Type/PgIdentifierTest.php
14 changes: 12 additions & 2 deletions src/Replacer/Placeholder.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,18 @@ class Placeholder implements ReplacerInterface
/** @var string */
protected $var_prefix;

/** @phpstan-var array<non-empty-string, \Teto\SQL\TypeInterface> */
protected $additional_types;

/**
* @param string $var_prefix An array key prefix of variables
* @phpstan-param array<non-empty-string, \Teto\SQL\TypeInterface> $additional_types
*/
public function __construct($var_prefix = ':')
public function __construct($var_prefix = ':', array $additional_types = [])
{
assert(\is_string($var_prefix));
$this->var_prefix = $var_prefix;
$this->additional_types = $additional_types;
}

public function replaceQuery($pdo, array $matches, array $params, array &$bind_values)
Expand Down Expand Up @@ -57,11 +63,15 @@ public function getPattern()
* @param string $key
* @param string $type
* @param mixed $value
* @param ?array<mixed> $bind_values
* @param array<mixed> $bind_values
* @return string|int
*/
public function replaceHolder($pdo, $key, $type, $value, &$bind_values)
{
if (isset($this->additional_types[$type])) {
return $this->additional_types[$type]->escapeValue($pdo, $key, $type, $value, $bind_values);
}

if ($type === '@ascdesc') {
if (!\in_array($value, ['ASC', 'DESC', 'asc', 'desc'], true)) {
throw new \DomainException(\sprintf('param "%s" must be "ASC", "DESC", "asc" or "desc"', $key));
Expand Down
95 changes: 95 additions & 0 deletions src/Type/PgIdentifier.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
<?php

namespace Teto\SQL\Type;

use DomainException;
use LogicException;
use Teto\SQL\TypeInterface;

/**
* Escape identifier (database, table, field and columns names) in PostgreSQL
*
* Please note that this process is highly dependent on the SQL product.
*/
class PgIdentifier implements TypeInterface
{
/** @phpstan-var array<non-empty-string,non-empty-string> */
protected $types = [];

/**
* @phpstan-param array{'@column'?: non-empty-string, '@column[]'?: non-empty-string, '@table'?: non-empty-string} $type_names
*/
public function __construct(array $type_names)
{
$types = [];

foreach (['@column', '@column[]', '@table'] as $type) {
$key = isset($type_names[$type]) ? $type_names[$type] : $type;
$types[$key] = $type;
}

$this->types = $types;
}

public function escapeValue($pdo, $key, $type, $value, &$bind_values)
{
if (!isset($this->types[$type])) {
throw new LogicException("Passed unexpected type '{$type}', please check your configuration.");
}

$replaced_type = $this->types[$type];
if ($replaced_type === '@column') {
if (\is_string($value)) {
return $this->quote($value);
}

throw new DomainException("Passed unexpected \$value as type '{$type}'. please check your query and parameters.");
}

if ($replaced_type === '@column[]') {
$columns = [];
if (!\is_array($value)) {
throw new DomainException("Passed unexpected \$value as type '{$type}'. please check your query and parameters.");
}
foreach ($value as $k => $v) {
if (\is_string($k)) {
if ($v === null || $v === '') {
$columns[] = $k;
} else {
$columns[] = "{$k} AS {$this->quote($v)}";
}
continue;
} elseif (\is_int($k)) {
if (\is_string($v)) {
$columns[] = $this->quote($v);
continue;
}
throw new DomainException("Passed unexpected \$value[{$k}] as type '{$type}'. please check your query and parameters.");
}

throw new LogicException('Unreachable');
}

return \implode(',', $columns);
}

if ($replaced_type === '@table') {
if (\is_string($value)) {
return $this->quote($value);
}

throw new DomainException("Passed unexpected \$value as type '{$type}'. please check your query and parameters.");
}

throw new LogicException("Unreachable, or {$type} is not implemented yet.");
}

/**
* @phpstan-param string $value
* @phpstan-return non-empty-string
*/
public function quote($value)
{
return '"' . $value . '"';
}
}
18 changes: 18 additions & 0 deletions src/TypeInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

namespace Teto\SQL;

interface TypeInterface
{
/**
* @param \PDO|\Teto\SQL\PDOInterface $pdo
* @template S of \PDOStatement|\Teto\SQL\PDOStatementInterface
* @phpstan-param \PDO|\Teto\SQL\PDOInterface<S> $pdo
* @param string $key
* @param string $type
* @param mixed $value
* @param array<mixed> $bind_values
* @return string|int
*/
public function escapeValue($pdo, $key, $type, $value, &$bind_values);
}
8 changes: 7 additions & 1 deletion tests/Replacer/PlaceholderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,12 @@ final class ReplaceHolderTest extends TestCase

public function set_up()
{
$this->subject = new Placeholder();
$this->subject = new Placeholder(
':',
[
'@dummy' => new Sample\DummyType(),
]
);
}

/**
Expand Down Expand Up @@ -68,6 +73,7 @@ public function acceptDataProvider()
['string', '0', '@0@'],
['string', '', '@@'],
['string[]', ['', ''], '@@,@@'],
['dummy', 'foo', '[foo] is a dummy value.'],
];
}

Expand Down
14 changes: 14 additions & 0 deletions tests/Replacer/Sample/DummyType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

namespace Teto\SQL\Replacer\Sample;

use Teto\SQL\TypeInterface;

class DummyType implements TypeInterface
{
public function escapeValue($pdo, $key, $type, $value, &$bind_values)
{
assert(\is_string($value));
return "[{$value}] is a dummy value.";
}
}
74 changes: 74 additions & 0 deletions tests/Type/PgIdentifierTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<?php

namespace Teto\SQL\Type;

use Teto\SQL\DummyPDO;
use Yoast\PHPUnitPolyfills\TestCases\TestCase;

class PgIdentifierTest extends TestCase
{
/** @var PgIdentifier */
private $subject;

public function set_up()
{
parent::set_up();

$this->subject = new PgIdentifier([]);
}

/**
* @dataProvider escapeValuesProvider
* @phpstan-param string|array<string> $input
* @param string $type
* @param string $expected
* @return void
*/
public function testEscapeValue($input, $type, $expected)
{
$pdo = new DummyPDO();
$bind_values = [];
$this->assertSame($expected, $this->subject->escapeValue($pdo, ':key', $type, $input, $bind_values));
$this->assertEquals([], $bind_values);
}

/**
* @return array<array{string|array<?string>,string,string}>
*/
public function escapeValuesProvider()
{
return [
['' , '@column', '""'],
[['foo'] , '@column[]', '"foo"'],
[['foo','bar'] , '@column[]', '"foo","bar"'],
[['foo' => 'bar'] , '@column[]', 'foo AS "bar"'],
[['"foo"' => 'bar'] , '@column[]', '"foo" AS "bar"'],
[['foo' => null] , '@column[]', 'foo'],
[['"foo"' => null] , '@column[]', '"foo"'],
[['foo' => null, 'bar' => 'buz'] , '@column[]', 'foo,bar AS "buz"'],
[['"foo"' => null, 'bar' => ''] , '@column[]', '"foo",bar'],
];
}

/**
* @dataProvider quoteValueProvider
* @param string $input
* @param string $expected
* @return void
*/
public function testQuote($input, $expected)
{
$this->assertSame($expected, $this->subject->quote($input));
}

/**
* @return array<array{string,string}>
*/
public function quoteValueProvider()
{
return [
['' , '""'],
['foo' , '"foo"'],
];
}
}

0 comments on commit 59dde9d

Please sign in to comment.