Skip to content

Commit 5ae8751

Browse files
authored
Change BinaryColumn::phpTypecast() to return StringableStream|string|null (#1013)
1 parent 99764af commit 5ae8751

9 files changed

Lines changed: 212 additions & 17 deletions

File tree

CHANGELOG.md

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

rector.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
use Rector\CodeQuality\Rector\Class_\InlineConstructorDefaultToPropertyRector;
66
use Rector\Config\RectorConfig;
77
use Rector\Php74\Rector\Property\RestoreDefaultNullToNullableTypePropertyRector;
8+
use Rector\Php80\Rector\Class_\ClassPropertyAssignToConstructorPromotionRector;
9+
use Rector\Php80\Rector\Class_\StringableForToStringRector;
810
use Rector\Php80\Rector\Ternary\GetDebugTypeRector;
911
use Rector\Php81\Rector\Property\ReadOnlyPropertyRector;
1012
use Rector\Php81\Rector\FuncCall\NullToStrictStringFuncCallArgRector;
@@ -19,9 +21,15 @@
1921
InlineConstructorDefaultToPropertyRector::class,
2022
])
2123
->withSkip([
24+
ClassPropertyAssignToConstructorPromotionRector::class => [
25+
__DIR__ . '/src/Schema/Data/StringableStream.php',
26+
],
2227
RestoreDefaultNullToNullableTypePropertyRector::class => [
2328
__DIR__ . '/src/Expression/CaseExpression.php',
2429
],
30+
StringableForToStringRector::class => [
31+
__DIR__ . '/src/Schema/Data/StringableStream.php',
32+
],
2533
GetDebugTypeRector::class => [
2634
__DIR__ . '/tests/AbstractColumnTest.php',
2735
],

src/QueryBuilder/AbstractQueryBuilder.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
use Yiisoft\Db\QueryBuilder\Condition\ConditionInterface;
2424
use Yiisoft\Db\Schema\Column\ColumnFactoryInterface;
2525
use Yiisoft\Db\Schema\Column\ColumnInterface;
26+
use Yiisoft\Db\Schema\Data\StringableStream;
2627
use Yiisoft\Db\Schema\QuoterInterface;
2728
use Yiisoft\Db\Syntax\AbstractSqlParser;
2829

@@ -279,6 +280,7 @@ public function buildValue(mixed $value, array &$params): string
279280
GettypeResult::OBJECT => match (true) {
280281
$value instanceof Param => $this->bindParam($value, $params),
281282
$value instanceof ExpressionInterface => $this->buildExpression($value, $params),
283+
$value instanceof StringableStream => $this->bindParam(new Param($value->getValue(), DataType::LOB), $params),
282284
$value instanceof Stringable => $this->bindParam(new Param((string) $value, DataType::STRING), $params),
283285
$value instanceof BackedEnum => is_string($value->value)
284286
? $this->bindParam(new Param($value->value, DataType::STRING), $params)
@@ -475,6 +477,7 @@ public function prepareValue(mixed $value): string
475477
$this->buildExpression($value, $params),
476478
array_map($this->prepareValue(...), $params),
477479
),
480+
$value instanceof StringableStream => $this->prepareBinary((string) $value),
478481
$value instanceof BackedEnum => is_string($value->value)
479482
? $this->db->getQuoter()->quoteValue($value->value)
480483
: (string) $value->value,

src/Schema/Column/BinaryColumn.php

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@
1111
use Yiisoft\Db\Constant\ColumnType;
1212
use Yiisoft\Db\Expression\ExpressionInterface;
1313
use Yiisoft\Db\Constant\GettypeResult;
14+
use Yiisoft\Db\Schema\Data\StringableStream;
1415

1516
use function gettype;
17+
use function is_resource;
1618

1719
/**
1820
* Represents the metadata for a binary column.
@@ -31,17 +33,19 @@ public function dbTypecast(mixed $value): mixed
3133
GettypeResult::DOUBLE => (string) $value,
3234
GettypeResult::BOOLEAN => $value ? '1' : '0',
3335
GettypeResult::OBJECT => match (true) {
36+
$value instanceof StringableStream => new Param($value->getValue(), PDO::PARAM_LOB),
3437
$value instanceof ExpressionInterface => $value,
35-
$value instanceof BackedEnum => (string) $value->value,
36-
$value instanceof Stringable => (string) $value,
38+
$value instanceof Stringable => new Param((string) $value, PDO::PARAM_LOB),
39+
$value instanceof BackedEnum => new Param((string) $value->value, PDO::PARAM_LOB),
3740
default => $this->throwWrongTypeException($value::class),
3841
},
3942
default => $this->throwWrongTypeException(gettype($value)),
4043
};
4144
}
4245

43-
public function phpTypecast(mixed $value): mixed
46+
public function phpTypecast(mixed $value): StringableStream|string|null
4447
{
45-
return $value;
48+
/** @var string|StringableStream|null */
49+
return is_resource($value) ? new StringableStream($value) : $value;
4650
}
4751
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Yiisoft\Db\Schema\Data;
6+
7+
use LogicException;
8+
use Stringable;
9+
use Yiisoft\Db\Constant\GettypeResult;
10+
11+
use function fclose;
12+
use function gettype;
13+
use function is_resource;
14+
use function stream_get_contents;
15+
16+
/**
17+
* Represents a resource stream to be used as a {@see Stringable} value.
18+
*
19+
* ```php
20+
* use Yiisoft\Db\Schema\Data\ResourceStream;
21+
*
22+
* // @var resource $resource
23+
* $stream = new StringableStream($resource);
24+
*
25+
* echo $stream;
26+
* ```
27+
*/
28+
final class StringableStream implements Stringable
29+
{
30+
/**
31+
* @var resource|string $value The resource stream or the result of reading the stream.
32+
*/
33+
private mixed $value;
34+
35+
/**
36+
* @param resource $value The open resource stream.
37+
*/
38+
public function __construct(mixed $value)
39+
{
40+
$this->value = $value;
41+
}
42+
43+
/**
44+
* Closes the resource.
45+
*/
46+
public function __destruct()
47+
{
48+
if (is_resource($this->value)) {
49+
fclose($this->value);
50+
}
51+
}
52+
53+
/**
54+
* @return string[] Prepared values for serialization.
55+
*/
56+
public function __serialize(): array
57+
{
58+
return ['value' => $this->__toString()];
59+
}
60+
61+
/**
62+
* @return string The result of reading the resource stream.
63+
*/
64+
public function __toString(): string
65+
{
66+
/**
67+
* @psalm-suppress PossiblyFalsePropertyAssignmentValue, PossiblyInvalidArgument
68+
* @var string
69+
*/
70+
return match (gettype($this->value)) {
71+
GettypeResult::RESOURCE => $this->value = stream_get_contents($this->value),
72+
GettypeResult::RESOURCE_CLOSED => throw new LogicException('Resource is closed.'),
73+
default => $this->value,
74+
};
75+
}
76+
77+
/**
78+
* @return resource|string The resource stream or the result of reading the stream.
79+
*/
80+
public function getValue(): mixed
81+
{
82+
return $this->value;
83+
}
84+
}

tests/Common/CommonCommandTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -480,7 +480,7 @@ public function testCreateTable(): void
480480

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

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

486486
$db->close();
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Yiisoft\Db\Tests\Db\Schema\Data;
6+
7+
use LogicException;
8+
use PHPUnit\Framework\TestCase;
9+
use Yiisoft\Db\Constant\GettypeResult;
10+
use Yiisoft\Db\Schema\Data\StringableStream;
11+
12+
use function fclose;
13+
use function fopen;
14+
use function gettype;
15+
use function serialize;
16+
use function unserialize;
17+
18+
/**
19+
* @group db
20+
*/
21+
final class ResourceStreamTest extends TestCase
22+
{
23+
public function testConstruct(): void
24+
{
25+
$resource = fopen(__DIR__ . '/../../../Support/string.txt', 'rb');
26+
$stringableSteam = new StringableStream($resource);
27+
28+
$this->assertSame($resource, $stringableSteam->getValue());
29+
}
30+
31+
public function testDestruct(): void
32+
{
33+
$resource = fopen(__DIR__ . '/../../../Support/string.txt', 'rb');
34+
$stringableSteam = new StringableStream($resource);
35+
36+
$this->assertSame(GettypeResult::RESOURCE, gettype($resource));
37+
38+
unset($stringableSteam);
39+
40+
$this->assertSame(GettypeResult::RESOURCE_CLOSED, gettype($resource));
41+
}
42+
43+
public function testSerialize(): void
44+
{
45+
$resource = fopen(__DIR__ . '/../../../Support/string.txt', 'rb');
46+
$stringableSteam = new StringableStream($resource);
47+
$serialized = serialize($stringableSteam);
48+
49+
$this->assertSame('O:39:"Yiisoft\Db\Schema\Data\StringableStream":1:{s:5:"value";s:6:"string";}', $serialized);
50+
$this->assertEquals($stringableSteam, unserialize($serialized));
51+
}
52+
53+
public function testToString(): void
54+
{
55+
$resource = fopen(__DIR__ . '/../../../Support/string.txt', 'rb');
56+
$stringableSteam = new StringableStream($resource);
57+
58+
// Can be read twice and more
59+
$this->assertSame('string', (string) $stringableSteam);
60+
$this->assertSame('string', (string) $stringableSteam);
61+
}
62+
63+
public function testToStringClosedResource(): void
64+
{
65+
$resource = fopen(__DIR__ . '/../../../Support/string.txt', 'rb');
66+
$stringableSteam = new StringableStream($resource);
67+
68+
fclose($resource);
69+
70+
$this->expectException(LogicException::class);
71+
$this->expectExceptionMessage('Resource is closed.');
72+
73+
(string) $stringableSteam;
74+
}
75+
76+
public function testGetValue(): void
77+
{
78+
$resource = fopen(__DIR__ . '/../../../Support/string.txt', 'rb');
79+
$stringableSteam = new StringableStream($resource);
80+
81+
$this->assertSame($resource, $stringableSteam->getValue());
82+
$this->assertSame('string', (string) $stringableSteam);
83+
$this->assertSame('string', $stringableSteam->getValue());
84+
}
85+
}

tests/Provider/ColumnProvider.php

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,13 @@
3434
use Yiisoft\Db\Schema\Column\StructuredLazyColumn;
3535
use Yiisoft\Db\Schema\Data\LazyArray;
3636
use Yiisoft\Db\Schema\Data\JsonLazyArray;
37+
use Yiisoft\Db\Schema\Data\StringableStream;
3738
use Yiisoft\Db\Schema\Data\StructuredLazyArray;
3839
use Yiisoft\Db\Tests\Support\IntEnum;
3940
use Yiisoft\Db\Tests\Support\Stringable;
4041
use Yiisoft\Db\Tests\Support\StringEnum;
4142

43+
use function fclose;
4244
use function fopen;
4345

4446
class ColumnProvider
@@ -147,10 +149,12 @@ public static function dbTypecastColumns(): array
147149
['1', true],
148150
['0', false],
149151
[new Param("\x10\x11\x12", PDO::PARAM_LOB), "\x10\x11\x12"],
150-
['1', IntEnum::ONE],
151-
['one', StringEnum::ONE],
152-
['string', new Stringable('string')],
152+
[new Param('1', PDO::PARAM_LOB), IntEnum::ONE],
153+
[new Param('one', PDO::PARAM_LOB), StringEnum::ONE],
154+
[new Param('string', PDO::PARAM_LOB), new Stringable('string')],
153155
[$resource = fopen('php://memory', 'rb'), $resource],
156+
[new Param($resource = fopen('php://memory', 'rb'), PDO::PARAM_LOB), new StringableStream($resource)],
157+
[new Param("\x10\x11\x12", PDO::PARAM_LOB), new StringableStream("\x10\x11\x12")],
154158
[$expression = new Expression('expression'), $expression],
155159
],
156160
],
@@ -473,6 +477,9 @@ public static function dbTypecastColumns(): array
473477

474478
public static function dbTypecastColumnsWithException(): array
475479
{
480+
$resource = fopen('php://memory', 'rb');
481+
fclose($resource);
482+
476483
return [
477484
'integer array' => [new IntegerColumn(), []],
478485
'integer resource' => [new IntegerColumn(), fopen('php://memory', 'r')],
@@ -485,6 +492,7 @@ public static function dbTypecastColumnsWithException(): array
485492
'double stdClass' => [new DoubleColumn(), new stdClass()],
486493
'string array' => [new StringColumn(), []],
487494
'string stdClass' => [new StringColumn(), new stdClass()],
495+
'binary closed' => [new BinaryColumn(), $resource],
488496
'binary array' => [new BinaryColumn(), []],
489497
'binary stdClass' => [new BinaryColumn(), new stdClass()],
490498
'datetime array' => [new DateTimeColumn(), []],
@@ -539,7 +547,7 @@ public static function phpTypecastColumns(): array
539547
[null, null],
540548
['', ''],
541549
["\x10\x11\x12", "\x10\x11\x12"],
542-
[$resource = fopen('php://memory', 'rb'), $resource],
550+
[new StringableStream($resource = fopen('php://memory', 'rb')), $resource],
543551
],
544552
],
545553
'bit' => [

tests/Provider/QueryBuilderProvider.php

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
use Yiisoft\Db\QueryBuilder\Condition\Like;
2222
use Yiisoft\Db\QueryBuilder\QueryBuilderInterface;
2323
use Yiisoft\Db\Schema\Column\ColumnBuilder;
24+
use Yiisoft\Db\Schema\Data\StringableStream;
2425
use Yiisoft\Db\Tests\Support\Assert;
2526
use Yiisoft\Db\Tests\Support\DbHelper;
2627
use Yiisoft\Db\Tests\Support\IntEnum;
@@ -32,11 +33,6 @@
3233

3334
use function fopen;
3435

35-
/**
36-
* @psalm-suppress MixedAssignment
37-
* @psalm-suppress MixedArgument
38-
* @psalm-suppress PossiblyUndefinedArrayOffset
39-
*/
4036
class QueryBuilderProvider
4137
{
4238
use TestTrait;
@@ -1774,6 +1770,7 @@ public static function prepareValue(): array
17741770
'paramInteger' => ['1', new Param(1, DataType::INTEGER)],
17751771
'expression' => ['(1 + 2)', new Expression('(1 + 2)')],
17761772
'expression with params' => ['(1 + 2)', new Expression('(:a + :b)', [':a' => 1, 'b' => 2])],
1773+
'ResourceStream' => ['0x737472696e67', new StringableStream(fopen(__DIR__ . '/../Support/string.txt', 'rb'))],
17771774
'Stringable' => ["'string'", new Stringable('string')],
17781775
'StringEnum' => ["'one'", StringEnum::ONE],
17791776
'IntEnum' => ['1', IntEnum::ONE],
@@ -1799,9 +1796,9 @@ public static function buildValue(): array
17991796
[':qp0' => new Param('string', DataType::STRING)],
18001797
],
18011798
'binary' => [
1802-
$param = fopen(__DIR__ . '/../Support/string.txt', 'rb'),
1799+
$resource = fopen(__DIR__ . '/../Support/string.txt', 'rb'),
18031800
':qp0',
1804-
[':qp0' => new Param($param, DataType::LOB)],
1801+
[':qp0' => new Param($resource, DataType::LOB)],
18051802
],
18061803
'paramBinary' => [
18071804
$param = new Param('string', DataType::LOB),
@@ -1827,6 +1824,11 @@ public static function buildValue(): array
18271824
'(:a + :b)',
18281825
[':a' => 1, 'b' => 2],
18291826
],
1827+
'ResourceStream' => [
1828+
new StringableStream($resource = fopen(__DIR__ . '/../Support/string.txt', 'rb')),
1829+
':qp0',
1830+
[':qp0' => new Param($resource, DataType::LOB)],
1831+
],
18301832
'Stringable' => [
18311833
new Stringable('string'),
18321834
':qp0',

0 commit comments

Comments
 (0)