Skip to content
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
- Enh #806, #964: Build `Expression` instances inside `Expression::$params` when build a query using `QueryBuilder` (@Tigrov)
- Enh #766: Allow `ColumnInterface` as column type. (@Tigrov)
- Bug #828: Fix `float` type when use `AbstractCommand::getRawSql()` method (@Tigrov)
- New #752, #974: Implement `ColumnSchemaInterface` classes according to the data type of database table columns
- New #752, #974, #1013: Implement `ColumnInterface` classes according to the data type of database table columns
for type casting performance (@Tigrov)
- Enh #829: Rename `batchInsert()` to `insertBatch()` in `DMLQueryBuilderInterface` and `CommandInterface`
and change parameters from `$table, $columns, $rows` to `$table, $rows, $columns = []` (@Tigrov)
Expand Down Expand Up @@ -115,6 +115,7 @@
- Enh #1010: Improve `Quoter::getTableNameParts()` method (@Tigrov)
- Enh #1011: Refactor `TableSchemaInterface` and `AbstractSchema` (@Tigrov)
- Enh #1011: Remove `AbstractTableSchema` and add `TableSchema` instead (@Tigrov)
- New #1013: Add `StringableStream` class to cast binary column values to `string` using `(string) $value` (@Tigrov)
- Chg #1014: Replace `getEscapingReplacements()`/`setEscapingReplacements()` methods with `escape` constructor parameter
in `Like` condition (@vjik)

Expand Down
8 changes: 8 additions & 0 deletions rector.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
use Rector\CodeQuality\Rector\Class_\InlineConstructorDefaultToPropertyRector;
use Rector\Config\RectorConfig;
use Rector\Php74\Rector\Property\RestoreDefaultNullToNullableTypePropertyRector;
use Rector\Php80\Rector\Class_\ClassPropertyAssignToConstructorPromotionRector;
use Rector\Php80\Rector\Class_\StringableForToStringRector;
use Rector\Php80\Rector\Ternary\GetDebugTypeRector;
use Rector\Php81\Rector\Property\ReadOnlyPropertyRector;
use Rector\Php81\Rector\FuncCall\NullToStrictStringFuncCallArgRector;
Expand All @@ -19,9 +21,15 @@
InlineConstructorDefaultToPropertyRector::class,
])
->withSkip([
ClassPropertyAssignToConstructorPromotionRector::class => [
__DIR__ . '/src/Schema/Data/StringableStream.php',
],
RestoreDefaultNullToNullableTypePropertyRector::class => [
__DIR__ . '/src/Expression/CaseExpression.php',
],
StringableForToStringRector::class => [
__DIR__ . '/src/Schema/Data/StringableStream.php',
],
GetDebugTypeRector::class => [
__DIR__ . '/tests/AbstractColumnTest.php',
],
Expand Down
3 changes: 3 additions & 0 deletions src/QueryBuilder/AbstractQueryBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
use Yiisoft\Db\QueryBuilder\Condition\ConditionInterface;
use Yiisoft\Db\Schema\Column\ColumnFactoryInterface;
use Yiisoft\Db\Schema\Column\ColumnInterface;
use Yiisoft\Db\Schema\Data\StringableStream;
use Yiisoft\Db\Schema\QuoterInterface;
use Yiisoft\Db\Syntax\AbstractSqlParser;

Expand Down Expand Up @@ -279,6 +280,7 @@ public function buildValue(mixed $value, array &$params): string
GettypeResult::OBJECT => match (true) {
$value instanceof Param => $this->bindParam($value, $params),
$value instanceof ExpressionInterface => $this->buildExpression($value, $params),
$value instanceof StringableStream => $this->bindParam(new Param($value->getValue(), DataType::LOB), $params),
$value instanceof Stringable => $this->bindParam(new Param((string) $value, DataType::STRING), $params),
$value instanceof BackedEnum => is_string($value->value)
? $this->bindParam(new Param($value->value, DataType::STRING), $params)
Expand Down Expand Up @@ -475,6 +477,7 @@ public function prepareValue(mixed $value): string
$this->buildExpression($value, $params),
array_map($this->prepareValue(...), $params),
),
$value instanceof StringableStream => $this->prepareBinary((string) $value),
$value instanceof BackedEnum => is_string($value->value)
? $this->db->getQuoter()->quoteValue($value->value)
: (string) $value->value,
Expand Down
12 changes: 8 additions & 4 deletions src/Schema/Column/BinaryColumn.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@
use Yiisoft\Db\Constant\ColumnType;
use Yiisoft\Db\Expression\ExpressionInterface;
use Yiisoft\Db\Constant\GettypeResult;
use Yiisoft\Db\Schema\Data\StringableStream;

use function gettype;
use function is_resource;

/**
* Represents the metadata for a binary column.
Expand All @@ -31,17 +33,19 @@ public function dbTypecast(mixed $value): mixed
GettypeResult::DOUBLE => (string) $value,
GettypeResult::BOOLEAN => $value ? '1' : '0',
GettypeResult::OBJECT => match (true) {
$value instanceof StringableStream => new Param($value->getValue(), PDO::PARAM_LOB),
$value instanceof ExpressionInterface => $value,
$value instanceof BackedEnum => (string) $value->value,
$value instanceof Stringable => (string) $value,
$value instanceof Stringable => new Param((string) $value, PDO::PARAM_LOB),
$value instanceof BackedEnum => new Param((string) $value->value, PDO::PARAM_LOB),
default => $this->throwWrongTypeException($value::class),
},
default => $this->throwWrongTypeException(gettype($value)),
};
}

public function phpTypecast(mixed $value): mixed
public function phpTypecast(mixed $value): StringableStream|string|null
{
return $value;
/** @var string|StringableStream|null */
return is_resource($value) ? new StringableStream($value) : $value;
}
}
84 changes: 84 additions & 0 deletions src/Schema/Data/StringableStream.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Db\Schema\Data;

use LogicException;
use Stringable;
use Yiisoft\Db\Constant\GettypeResult;

use function fclose;
use function gettype;
use function is_resource;
use function stream_get_contents;

/**
* Represents a resource stream to be used as a {@see Stringable} value.
*
* ```php
* use Yiisoft\Db\Schema\Data\ResourceStream;
*
* // @var resource $resource
* $stream = new StringableStream($resource);
*
* echo $stream;
* ```
*/
final class StringableStream implements Stringable
{
/**
* @var resource|string $value The resource stream or the result of reading the stream.
*/
private mixed $value;

/**
* @param resource $value The open resource stream.
*/
public function __construct(mixed $value)
{
$this->value = $value;
}

/**
* Closes the resource.
*/
public function __destruct()
{
if (is_resource($this->value)) {
fclose($this->value);
}
}

/**
* @return string[] Prepared values for serialization.
*/
public function __serialize(): array
{
return ['value' => $this->__toString()];
}

/**
* @return string The result of reading the resource stream.
*/
public function __toString(): string
{
/**
* @psalm-suppress PossiblyFalsePropertyAssignmentValue, PossiblyInvalidArgument
* @var string
*/
return match (gettype($this->value)) {
GettypeResult::RESOURCE => $this->value = stream_get_contents($this->value),
GettypeResult::RESOURCE_CLOSED => throw new LogicException('Resource is closed.'),
default => $this->value,
};
}

/**
* @return resource|string The resource stream or the result of reading the stream.
*/
public function getValue(): mixed
{
return $this->value;
}
}
2 changes: 1 addition & 1 deletion tests/Common/CommonCommandTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -480,7 +480,7 @@ public function testCreateTable(): void

$nameCol = $schema->getTableSchema('{{testCreateTable}}', true)->getColumn('name');

$this->assertFalse($nameCol->isAllowNull());
$this->assertTrue($nameCol->isNotNull());
$this->assertEquals([['id' => 1, 'bar' => 1, 'name' => 'Lilo']], $records);

$db->close();
Expand Down
85 changes: 85 additions & 0 deletions tests/Db/Schema/Data/ResourceStreamTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Db\Tests\Db\Schema\Data;

use LogicException;
use PHPUnit\Framework\TestCase;
use Yiisoft\Db\Constant\GettypeResult;
use Yiisoft\Db\Schema\Data\StringableStream;

use function fclose;
use function fopen;
use function gettype;
use function serialize;
use function unserialize;

/**
* @group db
*/
final class ResourceStreamTest extends TestCase
{
public function testConstruct(): void
{
$resource = fopen(__DIR__ . '/../../../Support/string.txt', 'rb');
$stringableSteam = new StringableStream($resource);

$this->assertSame($resource, $stringableSteam->getValue());
}

public function testDestruct(): void
{
$resource = fopen(__DIR__ . '/../../../Support/string.txt', 'rb');
$stringableSteam = new StringableStream($resource);

$this->assertSame(GettypeResult::RESOURCE, gettype($resource));

unset($stringableSteam);

$this->assertSame(GettypeResult::RESOURCE_CLOSED, gettype($resource));
}

public function testSerialize(): void
{
$resource = fopen(__DIR__ . '/../../../Support/string.txt', 'rb');
$stringableSteam = new StringableStream($resource);
$serialized = serialize($stringableSteam);

$this->assertSame('O:39:"Yiisoft\Db\Schema\Data\StringableStream":1:{s:5:"value";s:6:"string";}', $serialized);
$this->assertEquals($stringableSteam, unserialize($serialized));
}

public function testToString(): void
{
$resource = fopen(__DIR__ . '/../../../Support/string.txt', 'rb');
$stringableSteam = new StringableStream($resource);

// Can be read twice and more
$this->assertSame('string', (string) $stringableSteam);
$this->assertSame('string', (string) $stringableSteam);
}

public function testToStringClosedResource(): void
{
$resource = fopen(__DIR__ . '/../../../Support/string.txt', 'rb');
$stringableSteam = new StringableStream($resource);

fclose($resource);

$this->expectException(LogicException::class);
$this->expectExceptionMessage('Resource is closed.');

(string) $stringableSteam;
}

public function testGetValue(): void
{
$resource = fopen(__DIR__ . '/../../../Support/string.txt', 'rb');
$stringableSteam = new StringableStream($resource);

$this->assertSame($resource, $stringableSteam->getValue());
$this->assertSame('string', (string) $stringableSteam);
$this->assertSame('string', $stringableSteam->getValue());
}
}
16 changes: 12 additions & 4 deletions tests/Provider/ColumnProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,13 @@
use Yiisoft\Db\Schema\Column\StructuredLazyColumn;
use Yiisoft\Db\Schema\Data\LazyArray;
use Yiisoft\Db\Schema\Data\JsonLazyArray;
use Yiisoft\Db\Schema\Data\StringableStream;
use Yiisoft\Db\Schema\Data\StructuredLazyArray;
use Yiisoft\Db\Tests\Support\IntEnum;
use Yiisoft\Db\Tests\Support\Stringable;
use Yiisoft\Db\Tests\Support\StringEnum;

use function fclose;
use function fopen;

class ColumnProvider
Expand Down Expand Up @@ -147,10 +149,12 @@ public static function dbTypecastColumns(): array
['1', true],
['0', false],
[new Param("\x10\x11\x12", PDO::PARAM_LOB), "\x10\x11\x12"],
['1', IntEnum::ONE],
['one', StringEnum::ONE],
['string', new Stringable('string')],
[new Param('1', PDO::PARAM_LOB), IntEnum::ONE],
[new Param('one', PDO::PARAM_LOB), StringEnum::ONE],
[new Param('string', PDO::PARAM_LOB), new Stringable('string')],
[$resource = fopen('php://memory', 'rb'), $resource],
[new Param($resource = fopen('php://memory', 'rb'), PDO::PARAM_LOB), new StringableStream($resource)],
[new Param("\x10\x11\x12", PDO::PARAM_LOB), new StringableStream("\x10\x11\x12")],
[$expression = new Expression('expression'), $expression],
],
],
Expand Down Expand Up @@ -473,6 +477,9 @@ public static function dbTypecastColumns(): array

public static function dbTypecastColumnsWithException(): array
{
$resource = fopen('php://memory', 'rb');
fclose($resource);

return [
'integer array' => [new IntegerColumn(), []],
'integer resource' => [new IntegerColumn(), fopen('php://memory', 'r')],
Expand All @@ -485,6 +492,7 @@ public static function dbTypecastColumnsWithException(): array
'double stdClass' => [new DoubleColumn(), new stdClass()],
'string array' => [new StringColumn(), []],
'string stdClass' => [new StringColumn(), new stdClass()],
'binary closed' => [new BinaryColumn(), $resource],
'binary array' => [new BinaryColumn(), []],
'binary stdClass' => [new BinaryColumn(), new stdClass()],
'datetime array' => [new DateTimeColumn(), []],
Expand Down Expand Up @@ -539,7 +547,7 @@ public static function phpTypecastColumns(): array
[null, null],
['', ''],
["\x10\x11\x12", "\x10\x11\x12"],
[$resource = fopen('php://memory', 'rb'), $resource],
[new StringableStream($resource = fopen('php://memory', 'rb')), $resource],
],
],
'bit' => [
Expand Down
16 changes: 9 additions & 7 deletions tests/Provider/QueryBuilderProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
use Yiisoft\Db\QueryBuilder\Condition\Like;
use Yiisoft\Db\QueryBuilder\QueryBuilderInterface;
use Yiisoft\Db\Schema\Column\ColumnBuilder;
use Yiisoft\Db\Schema\Data\StringableStream;
use Yiisoft\Db\Tests\Support\Assert;
use Yiisoft\Db\Tests\Support\DbHelper;
use Yiisoft\Db\Tests\Support\IntEnum;
Expand All @@ -32,11 +33,6 @@

use function fopen;

/**
* @psalm-suppress MixedAssignment
* @psalm-suppress MixedArgument
* @psalm-suppress PossiblyUndefinedArrayOffset
*/
class QueryBuilderProvider
{
use TestTrait;
Expand Down Expand Up @@ -1774,6 +1770,7 @@ public static function prepareValue(): array
'paramInteger' => ['1', new Param(1, DataType::INTEGER)],
'expression' => ['(1 + 2)', new Expression('(1 + 2)')],
'expression with params' => ['(1 + 2)', new Expression('(:a + :b)', [':a' => 1, 'b' => 2])],
'ResourceStream' => ['0x737472696e67', new StringableStream(fopen(__DIR__ . '/../Support/string.txt', 'rb'))],
'Stringable' => ["'string'", new Stringable('string')],
'StringEnum' => ["'one'", StringEnum::ONE],
'IntEnum' => ['1', IntEnum::ONE],
Expand All @@ -1799,9 +1796,9 @@ public static function buildValue(): array
[':qp0' => new Param('string', DataType::STRING)],
],
'binary' => [
$param = fopen(__DIR__ . '/../Support/string.txt', 'rb'),
$resource = fopen(__DIR__ . '/../Support/string.txt', 'rb'),
':qp0',
[':qp0' => new Param($param, DataType::LOB)],
[':qp0' => new Param($resource, DataType::LOB)],
],
'paramBinary' => [
$param = new Param('string', DataType::LOB),
Expand All @@ -1827,6 +1824,11 @@ public static function buildValue(): array
'(:a + :b)',
[':a' => 1, 'b' => 2],
],
'ResourceStream' => [
new StringableStream($resource = fopen(__DIR__ . '/../Support/string.txt', 'rb')),
':qp0',
[':qp0' => new Param($resource, DataType::LOB)],
],
'Stringable' => [
new Stringable('string'),
':qp0',
Expand Down
Loading