Skip to content

Commit 24e5a96

Browse files
authored
Add (weakly referenced) local object DBAL type (#1083)
1 parent 448e308 commit 24e5a96

10 files changed

+416
-46
lines changed

bootstrap-types.php

+5-41
Original file line numberDiff line numberDiff line change
@@ -2,48 +2,12 @@
22

33
declare(strict_types=1);
44

5-
namespace Atk4\Data\Types;
5+
namespace Atk4\Data\Bootstrap;
66

7-
use Doctrine\DBAL\Platforms\AbstractPlatform;
7+
use Atk4\Data\Type\LocalObjectType;
8+
use Atk4\Data\Type\MoneyType;
9+
use Atk4\Data\Type\Types;
810
use Doctrine\DBAL\Types as DbalTypes;
911

10-
final class Types
11-
{
12-
public const MONEY = 'atk4_money';
13-
}
14-
15-
class MoneyType extends DbalTypes\Type
16-
{
17-
public function getName(): string
18-
{
19-
return Types::MONEY;
20-
}
21-
22-
public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform): string
23-
{
24-
return DbalTypes\Type::getType(DbalTypes\Types::FLOAT)->getSQLDeclaration($fieldDeclaration, $platform);
25-
}
26-
27-
public function convertToDatabaseValue($value, AbstractPlatform $platform): ?string
28-
{
29-
if ($value === null || trim((string) $value) === '') {
30-
return null;
31-
}
32-
33-
return (string) round((float) $value, 4);
34-
}
35-
36-
public function convertToPHPValue($value, AbstractPlatform $platform): ?float
37-
{
38-
$v = $this->convertToDatabaseValue($value, $platform);
39-
40-
return $v === null ? null : (float) $v;
41-
}
42-
43-
public function requiresSQLCommentHint(AbstractPlatform $platform): bool
44-
{
45-
return true;
46-
}
47-
}
48-
12+
DbalTypes\Type::addType(Types::LOCAL_OBJECT, LocalObjectType::class);
4913
DbalTypes\Type::addType(Types::MONEY, MoneyType::class);

src/Model.php

+2-1
Original file line numberDiff line numberDiff line change
@@ -1049,7 +1049,7 @@ public function setOrder($field, string $direction = 'asc')
10491049
}
10501050
} else {
10511051
// format "field" => direction
1052-
$this->setOrder($k, $v); // @phpstan-ignore-line https://github.com/phpstan/phpstan/issues/7924
1052+
$this->setOrder($k, $v);
10531053
}
10541054
}
10551055

@@ -1326,6 +1326,7 @@ public function loadAny()
13261326
public function reload()
13271327
{
13281328
$id = $this->getId();
1329+
$data = $this->getDataRef(); // keep weakly persisted objects referenced
13291330
$this->unload();
13301331

13311332
$res = $this->_load(true, false, $id);

src/Persistence/Sql/Connection.php

+3-3
Original file line numberDiff line numberDiff line change
@@ -88,10 +88,10 @@ public static function normalizeDsn($dsn, $user = null, $password = null)
8888
$dsn['dsn'] = str_replace('-', '_', $parsed['scheme']) . ':';
8989
unset($parsed['scheme']);
9090
foreach ($parsed as $k => $v) {
91-
if ($k === 'pass') {
91+
if ($k === 'pass') { // @phpstan-ignore-line https://github.com/phpstan/phpstan/issues/8638
9292
unset($parsed[$k]);
9393
$k = 'password';
94-
} elseif ($k === 'path') {
94+
} elseif ($k === 'path') { // @phpstan-ignore-line https://github.com/phpstan/phpstan/issues/8638
9595
unset($parsed[$k]);
9696
$k = 'dbname';
9797
$v = preg_replace('~^/~', '', $v);
@@ -215,7 +215,7 @@ private static function getDriverNameFromDbalDriverConnection(DbalDriverConnecti
215215
$driver = $connection->getNativeConnection();
216216

217217
if ($driver instanceof \PDO) {
218-
return 'pdo_' . $driver->getAttribute(\PDO::ATTR_DRIVER_NAME); // @phpstan-ignore-line
218+
return 'pdo_' . $driver->getAttribute(\PDO::ATTR_DRIVER_NAME);
219219
} elseif ($driver instanceof \mysqli) {
220220
return 'mysqli';
221221
} elseif (is_resource($driver) && get_resource_type($driver) === 'oci8 connection') {

src/Type/LocalObjectHandle.php

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Atk4\Data\Type;
6+
7+
class LocalObjectHandle
8+
{
9+
private int $localUid;
10+
11+
/** @var \WeakReference<object> */
12+
private \WeakReference $weakValue;
13+
14+
private \Closure $destructFx;
15+
16+
public function __construct(int $localUid, object $value, \Closure $destructFx)
17+
{
18+
$this->localUid = $localUid;
19+
$this->weakValue = \WeakReference::create($value);
20+
$this->destructFx = $destructFx;
21+
}
22+
23+
public function __destruct()
24+
{
25+
($this->destructFx)($this);
26+
}
27+
28+
public function getLocalUid(): int
29+
{
30+
return $this->localUid;
31+
}
32+
33+
public function getValue(): ?object
34+
{
35+
return $this->weakValue->get();
36+
}
37+
}

src/Type/LocalObjectType.php

+115
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Atk4\Data\Type;
6+
7+
use Atk4\Data\Exception;
8+
use Doctrine\DBAL\Platforms\AbstractPlatform;
9+
use Doctrine\DBAL\Types as DbalTypes;
10+
11+
/**
12+
* Type that allows to weakly reference a local PHP object using a scalar string
13+
* and get the original object instance back using the string.
14+
*
15+
* The local object is never serialized.
16+
*
17+
* An exception is thrown when getting an object from a string back and the original
18+
* object instance has been destroyed/released.
19+
*/
20+
class LocalObjectType extends DbalTypes\Type
21+
{
22+
private ?string $instanceUid = null;
23+
24+
private int $localUidCounter;
25+
26+
/** @var \WeakMap<object, LocalObjectHandle> */
27+
private \WeakMap $handles;
28+
/** @var array<int, \WeakReference<LocalObjectHandle>> */
29+
private array $handlesIndex;
30+
31+
protected function __clone()
32+
{
33+
// prevent clonning
34+
}
35+
36+
protected function init(): void
37+
{
38+
$this->instanceUid = hash('sha256', microtime(true) . random_bytes(64));
39+
$this->localUidCounter = 0;
40+
$this->handles = new \WeakMap();
41+
$this->handlesIndex = [];
42+
}
43+
44+
public function getName(): string
45+
{
46+
return Types::LOCAL_OBJECT;
47+
}
48+
49+
public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform): string
50+
{
51+
return DbalTypes\Type::getType(DbalTypes\Types::STRING)->getSQLDeclaration($fieldDeclaration, $platform);
52+
}
53+
54+
public function convertToDatabaseValue($value, AbstractPlatform $platform): ?string
55+
{
56+
if ($value === null) {
57+
return null;
58+
}
59+
60+
if ($this->instanceUid === null) {
61+
$this->init();
62+
}
63+
64+
$handle = $this->handles->offsetExists($value)
65+
? $this->handles->offsetGet($value)
66+
: null;
67+
68+
if ($handle === null) {
69+
$handle = new LocalObjectHandle(++$this->localUidCounter, $value, function (LocalObjectHandle $handle): void {
70+
unset($this->handlesIndex[$handle->getLocalUid()]);
71+
});
72+
$this->handles->offsetSet($value, $handle);
73+
$this->handlesIndex[$handle->getLocalUid()] = \WeakReference::create($handle);
74+
}
75+
76+
$className = get_debug_type($value);
77+
if (strlen($className) > 160) { // keep result below 255 bytes
78+
$className = mb_strcut($className, 0, 80)
79+
. '...'
80+
. mb_strcut(substr($className, strlen(mb_strcut($className, 0, 80))), -80);
81+
}
82+
83+
return $className . '-' . $this->instanceUid . '-' . $handle->getLocalUid();
84+
}
85+
86+
public function convertToPHPValue($value, AbstractPlatform $platform): ?object
87+
{
88+
if ($value === null || trim($value) === '') {
89+
return null;
90+
}
91+
92+
$valueExploded = explode('-', $value, 3);
93+
if (count($valueExploded) !== 3
94+
|| $valueExploded[1] !== $this->instanceUid
95+
|| $valueExploded[2] !== (string) (int) $valueExploded[2]
96+
) {
97+
throw new Exception('Local object does not match the DBAL type instance');
98+
}
99+
$handle = $this->handlesIndex[(int) $valueExploded[2]] ?? null;
100+
if ($handle !== null) {
101+
$handle = $handle->get();
102+
}
103+
$res = $handle !== null ? $handle->getValue() : null;
104+
if ($res === null) {
105+
throw new Exception('Local object does no longer exist');
106+
}
107+
108+
return $res;
109+
}
110+
111+
public function requiresSQLCommentHint(AbstractPlatform $platform): bool
112+
{
113+
return true;
114+
}
115+
}

src/Type/MoneyType.php

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Atk4\Data\Type;
6+
7+
use Doctrine\DBAL\Platforms\AbstractPlatform;
8+
use Doctrine\DBAL\Types as DbalTypes;
9+
10+
class MoneyType extends DbalTypes\Type
11+
{
12+
public function getName(): string
13+
{
14+
return Types::MONEY;
15+
}
16+
17+
public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform): string
18+
{
19+
return DbalTypes\Type::getType(DbalTypes\Types::FLOAT)->getSQLDeclaration($fieldDeclaration, $platform);
20+
}
21+
22+
public function convertToDatabaseValue($value, AbstractPlatform $platform): ?string
23+
{
24+
if ($value === null || trim((string) $value) === '') {
25+
return null;
26+
}
27+
28+
return (string) round((float) $value, 4);
29+
}
30+
31+
public function convertToPHPValue($value, AbstractPlatform $platform): ?float
32+
{
33+
$v = $this->convertToDatabaseValue($value, $platform);
34+
35+
return $v === null ? null : (float) $v;
36+
}
37+
38+
public function requiresSQLCommentHint(AbstractPlatform $platform): bool
39+
{
40+
return true;
41+
}
42+
}

src/Type/Types.php

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Atk4\Data\Type;
6+
7+
final class Types
8+
{
9+
public const LOCAL_OBJECT = 'atk4_local_object';
10+
public const MONEY = 'atk4_money';
11+
}

0 commit comments

Comments
 (0)