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: private function __clone()
32: {
33: // prevent cloning
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: #[\Override]
45: public function getName(): string
46: {
47: return Types::LOCAL_OBJECT;
48: }
49:
50: #[\Override]
51: public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform): string
52: {
53: return DbalTypes\Type::getType(DbalTypes\Types::STRING)->getSQLDeclaration($fieldDeclaration, $platform);
54: }
55:
56: #[\Override]
57: public function convertToDatabaseValue($value, AbstractPlatform $platform): ?string
58: {
59: if ($value === null) {
60: return null;
61: }
62:
63: if ($this->instanceUid === null) {
64: $this->init();
65: }
66:
67: $handle = $this->handles->offsetExists($value)
68: ? $this->handles->offsetGet($value)
69: : null;
70:
71: if ($handle === null) {
72: $handle = new LocalObjectHandle(++$this->localUidCounter, $value, function (LocalObjectHandle $handle): void {
73: unset($this->handlesIndex[$handle->getLocalUid()]);
74: });
75: $this->handles->offsetSet($value, $handle);
76: $this->handlesIndex[$handle->getLocalUid()] = \WeakReference::create($handle);
77: }
78:
79: $className = get_debug_type($value);
80: if (strlen($className) > 160) { // keep result below 255 bytes
81: $className = mb_strcut($className, 0, 80)
82: . '...'
83: . mb_strcut(substr($className, strlen(mb_strcut($className, 0, 80))), -80);
84: }
85:
86: return $className . '-' . $this->instanceUid . '-' . $handle->getLocalUid();
87: }
88:
89: #[\Override]
90: public function convertToPHPValue($value, AbstractPlatform $platform): ?object
91: {
92: if ($value === null || trim($value) === '') {
93: return null;
94: }
95:
96: $valueExploded = explode('-', $value, 3);
97: if (count($valueExploded) !== 3
98: || $valueExploded[1] !== $this->instanceUid
99: || $valueExploded[2] !== (string) (int) $valueExploded[2]
100: ) {
101: throw new Exception('Local object does not match the DBAL type instance');
102: }
103: $handle = $this->handlesIndex[(int) $valueExploded[2]] ?? null;
104: if ($handle !== null) {
105: $handle = $handle->get();
106: }
107: $res = $handle !== null ? $handle->getValue() : null;
108: if ($res === null) {
109: throw new Exception('Local object does no longer exist');
110: }
111:
112: return $res;
113: }
114:
115: #[\Override]
116: public function requiresSQLCommentHint(AbstractPlatform $platform): bool
117: {
118: return true;
119: }
120: }
121: