Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
## [Unreleased]
### Added
- Add PHP v8 type declarations.
### Changed
- An object passed to a quote method now specifically checks for an
implementation of `\Stringable`.
This is equivalent to the previous behaviour of checking for `__toString()`.
Arbitrary objects will be blocked by type declarations.
### Removed
- **BC break**: Removed support for PHP versions <= v8.0 as they are no longer
[actively supported](https://php.net/supported-versions.php) by the PHP project.
Expand Down
2 changes: 1 addition & 1 deletion src/Adapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public function __construct(array $config = [])
public function quote(): Adapter\QuoteHandler
{
if (!isset($this->quoter)) {
$this->quoter = new Adapter\QuoteHandler(function ($value): string {
$this->quoter = new Adapter\QuoteHandler(function (string $value): string {
return $this->getConnection()->quote($value);
});
}
Expand Down
75 changes: 39 additions & 36 deletions src/Adapter/QuoteHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@
namespace Phlib\Db\Adapter;

use Phlib\Db\Exception\InvalidArgumentException;
use Phlib\Db\SqlStatement;

class QuoteHandler
{
/**
* @param \Closure{
* value: mixed,
* value: string,
* }:string $quoteFn
*/
public function __construct(
Expand All @@ -22,13 +23,11 @@ public function __construct(
/**
* Quote a value for the database
*/
public function value(mixed $value): string
{
public function value(
string|\Stringable|array|bool|int|float|null $value,
): string {
switch (true) {
case is_object($value):
if (!method_exists($value, '__toString')) {
throw new InvalidArgumentException('Object can not be converted to string value.');
}
case $value instanceof \Stringable:
return (string)$value;
case is_bool($value):
return (string)(int)$value;
Expand All @@ -44,24 +43,29 @@ public function value(mixed $value): string
return $this->value($value);
}, $value);
return implode(', ', $value);
default:
case is_string($value):
return ($this->quoteFn)($value);
default:
// Not reachable due to type declarations
throw new InvalidArgumentException('Value can not be converted to string for quoting');
}
}

/**
* Quote a value to replace the `?` placeholder
*/
public function into(string $text, mixed $value): string
{
public function into(
string $text,
string|\Stringable|array|bool|int|float|null $value,
): string {
return str_replace('?', $this->value($value), $text);
}

/**
* Quote a column identifier and alias
*/
public function columnAs(
string|array|object $ident,
string|\Stringable|array $ident,
string $alias,
bool $auto = false,
): string {
Expand All @@ -72,7 +76,7 @@ public function columnAs(
* Quote a table identifier and alias
*/
public function tableAs(
string|array|object $ident,
string|\Stringable|array $ident,
string $alias,
bool $auto = false,
): string {
Expand All @@ -83,45 +87,42 @@ public function tableAs(
* Quote an identifier
*/
public function identifier(
string|array|object $ident,
string|\Stringable|array $ident,
bool $auto = false,
): string {
return $this->quoteIdentifierAs($ident, null, $auto);
}

private function quoteIdentifierAs(
string|array|object $ident,
string|\Stringable|array $ident,
?string $alias = null,
bool $auto = false,
string $as = ' AS ',
): string {
if (is_object($ident) && method_exists($ident, 'assemble')) {
$quoted = '(' . $ident->assemble() . ')';
} elseif (is_object($ident)) {
if (!method_exists($ident, '__toString')) {
throw new InvalidArgumentException('Object can not be converted to string identifier.');
}
if ($ident instanceof \Stringable) {
$quoted = (string)$ident;
if ($ident instanceof SqlStatement) {
$quoted = '(' . $quoted . ')';
}
} else {
if (is_string($ident)) {
$ident = explode('.', $ident);
}
if (is_array($ident)) {
$segments = [];
foreach ($ident as $segment) {
if (is_object($segment)) {
$segments[] = (string)$segment;
} else {
$segments[] = $this->performQuoteIdentifier($segment, $auto);
}
}
if ($alias !== null && end($ident) === $alias) {
$alias = null;

$segments = [];
foreach ($ident as $segment) {
if ($segment instanceof \Stringable) {
$segments[] = (string)$segment;
} elseif (is_string($segment)) {
$segments[] = $this->performQuoteIdentifier($segment, $auto);
} else {
throw new InvalidArgumentException('Ident array values must be stringable');
}
$quoted = implode('.', $segments);
} else {
$quoted = $this->performQuoteIdentifier($ident, $auto);
}
if ($alias !== null && end($ident) === $alias) {
$alias = null;
}
$quoted = implode('.', $segments);
}

if ($alias !== null) {
Expand All @@ -131,8 +132,10 @@ private function quoteIdentifierAs(
return $quoted;
}

private function performQuoteIdentifier(string $value, bool $auto = false): string
{
private function performQuoteIdentifier(
string $value,
bool $auto = false,
): string {
if ($auto === false || $this->autoQuoteIdentifiers === true) {
$q = '`';
return $q . str_replace("{$q}", "{$q}{$q}", $value) . $q;
Expand Down
2 changes: 1 addition & 1 deletion src/SqlFragment.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
* This class will hold a SQL fragment and return it when cast to a string by Db::quote()
* to avoid the string otherwise being quoted as a value
*/
class SqlFragment
class SqlFragment implements \Stringable
{
public function __construct(
private readonly string $value,
Expand Down
13 changes: 13 additions & 0 deletions src/SqlStatement.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

declare(strict_types=1);

namespace Phlib\Db;

/**
* Interface for SQL statements, e.g. those that may be built dynamically, and rendered as a string.
* @private Not part of the BC promise. Placeholder for future implementation.
*/
interface SqlStatement extends \Stringable
{
}
45 changes: 45 additions & 0 deletions tests/Adapter/QuoteHandlerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
namespace Phlib\Db\Tests\Adapter;

use Phlib\Db\Adapter\QuoteHandler;
use Phlib\Db\Exception\InvalidArgumentException;
use Phlib\Db\SqlFragment;
use Phlib\Db\SqlStatement;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;

Expand Down Expand Up @@ -107,6 +109,30 @@ public static function tableAsData(): array
];
}

public function testTableAsWithSqlStatement(): void
{
$statement = sha1(uniqid('statement'));
$alias = sha1(uniqid('alias'));

$sqlStatement = new class($statement) implements SqlStatement {
public function __construct(
private readonly string $statement,
) {
}

public function __toString(): string
{
return $this->statement;
}
};

$actual = $this->handler->tableAs($sqlStatement, $alias);

// SQL Statement should be wrapped in parentheses
$expected = "({$statement}) AS `{$alias}`";
static::assertSame($expected, $actual);
}

#[DataProvider('identifierData')]
public function testIdentifier(string $expected, string|array|SqlFragment $ident, ?bool $auto): void
{
Expand All @@ -126,4 +152,23 @@ public static function identifierData(): array
['`table1`.`*`', 'table1.*', true],
];
}

public static function dataIdentifierWithInvalidArrayValue(): array
{
return [
'int' => [rand()],
'float' => [rand() / 100],
'bool' => [true],
'objectNotStringable' => [new \stdClass()],
];
}

#[DataProvider('dataIdentifierWithInvalidArrayValue')]
public function testIdentifierWithInvalidArrayValue(mixed $value): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Ident array values must be stringable');

$this->handler->identifier([$value]);
}
}