Skip to content

Commit

Permalink
Add (weakly referenced) local object DBAL type (#1083)
Browse files Browse the repository at this point in the history
  • Loading branch information
mvorisek committed Jan 3, 2023
1 parent 448e308 commit 24e5a96
Show file tree
Hide file tree
Showing 10 changed files with 416 additions and 46 deletions.
46 changes: 5 additions & 41 deletions bootstrap-types.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,48 +2,12 @@

declare(strict_types=1);

namespace Atk4\Data\Types;
namespace Atk4\Data\Bootstrap;

use Doctrine\DBAL\Platforms\AbstractPlatform;
use Atk4\Data\Type\LocalObjectType;
use Atk4\Data\Type\MoneyType;
use Atk4\Data\Type\Types;
use Doctrine\DBAL\Types as DbalTypes;

final class Types
{
public const MONEY = 'atk4_money';
}

class MoneyType extends DbalTypes\Type
{
public function getName(): string
{
return Types::MONEY;
}

public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform): string
{
return DbalTypes\Type::getType(DbalTypes\Types::FLOAT)->getSQLDeclaration($fieldDeclaration, $platform);
}

public function convertToDatabaseValue($value, AbstractPlatform $platform): ?string
{
if ($value === null || trim((string) $value) === '') {
return null;
}

return (string) round((float) $value, 4);
}

public function convertToPHPValue($value, AbstractPlatform $platform): ?float
{
$v = $this->convertToDatabaseValue($value, $platform);

return $v === null ? null : (float) $v;
}

public function requiresSQLCommentHint(AbstractPlatform $platform): bool
{
return true;
}
}

DbalTypes\Type::addType(Types::LOCAL_OBJECT, LocalObjectType::class);
DbalTypes\Type::addType(Types::MONEY, MoneyType::class);
3 changes: 2 additions & 1 deletion src/Model.php
Original file line number Diff line number Diff line change
Expand Up @@ -1049,7 +1049,7 @@ public function setOrder($field, string $direction = 'asc')
}
} else {
// format "field" => direction
$this->setOrder($k, $v); // @phpstan-ignore-line https://github.com/phpstan/phpstan/issues/7924
$this->setOrder($k, $v);
}
}

Expand Down Expand Up @@ -1326,6 +1326,7 @@ public function loadAny()
public function reload()
{
$id = $this->getId();
$data = $this->getDataRef(); // keep weakly persisted objects referenced
$this->unload();

$res = $this->_load(true, false, $id);
Expand Down
6 changes: 3 additions & 3 deletions src/Persistence/Sql/Connection.php
Original file line number Diff line number Diff line change
Expand Up @@ -88,10 +88,10 @@ public static function normalizeDsn($dsn, $user = null, $password = null)
$dsn['dsn'] = str_replace('-', '_', $parsed['scheme']) . ':';
unset($parsed['scheme']);
foreach ($parsed as $k => $v) {
if ($k === 'pass') {
if ($k === 'pass') { // @phpstan-ignore-line https://github.com/phpstan/phpstan/issues/8638
unset($parsed[$k]);
$k = 'password';
} elseif ($k === 'path') {
} elseif ($k === 'path') { // @phpstan-ignore-line https://github.com/phpstan/phpstan/issues/8638
unset($parsed[$k]);
$k = 'dbname';
$v = preg_replace('~^/~', '', $v);
Expand Down Expand Up @@ -215,7 +215,7 @@ private static function getDriverNameFromDbalDriverConnection(DbalDriverConnecti
$driver = $connection->getNativeConnection();

if ($driver instanceof \PDO) {
return 'pdo_' . $driver->getAttribute(\PDO::ATTR_DRIVER_NAME); // @phpstan-ignore-line
return 'pdo_' . $driver->getAttribute(\PDO::ATTR_DRIVER_NAME);
} elseif ($driver instanceof \mysqli) {
return 'mysqli';
} elseif (is_resource($driver) && get_resource_type($driver) === 'oci8 connection') {
Expand Down
37 changes: 37 additions & 0 deletions src/Type/LocalObjectHandle.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

declare(strict_types=1);

namespace Atk4\Data\Type;

class LocalObjectHandle
{
private int $localUid;

/** @var \WeakReference<object> */
private \WeakReference $weakValue;

private \Closure $destructFx;

public function __construct(int $localUid, object $value, \Closure $destructFx)
{
$this->localUid = $localUid;
$this->weakValue = \WeakReference::create($value);
$this->destructFx = $destructFx;
}

public function __destruct()
{
($this->destructFx)($this);
}

public function getLocalUid(): int
{
return $this->localUid;
}

public function getValue(): ?object
{
return $this->weakValue->get();
}
}
115 changes: 115 additions & 0 deletions src/Type/LocalObjectType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
<?php

declare(strict_types=1);

namespace Atk4\Data\Type;

use Atk4\Data\Exception;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Types as DbalTypes;

/**
* Type that allows to weakly reference a local PHP object using a scalar string
* and get the original object instance back using the string.
*
* The local object is never serialized.
*
* An exception is thrown when getting an object from a string back and the original
* object instance has been destroyed/released.
*/
class LocalObjectType extends DbalTypes\Type
{
private ?string $instanceUid = null;

private int $localUidCounter;

/** @var \WeakMap<object, LocalObjectHandle> */
private \WeakMap $handles;
/** @var array<int, \WeakReference<LocalObjectHandle>> */
private array $handlesIndex;

protected function __clone()
{
// prevent clonning
}

protected function init(): void
{
$this->instanceUid = hash('sha256', microtime(true) . random_bytes(64));
$this->localUidCounter = 0;
$this->handles = new \WeakMap();
$this->handlesIndex = [];
}

public function getName(): string
{
return Types::LOCAL_OBJECT;
}

public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform): string
{
return DbalTypes\Type::getType(DbalTypes\Types::STRING)->getSQLDeclaration($fieldDeclaration, $platform);
}

public function convertToDatabaseValue($value, AbstractPlatform $platform): ?string
{
if ($value === null) {
return null;
}

if ($this->instanceUid === null) {
$this->init();
}

$handle = $this->handles->offsetExists($value)
? $this->handles->offsetGet($value)
: null;

if ($handle === null) {
$handle = new LocalObjectHandle(++$this->localUidCounter, $value, function (LocalObjectHandle $handle): void {
unset($this->handlesIndex[$handle->getLocalUid()]);
});
$this->handles->offsetSet($value, $handle);
$this->handlesIndex[$handle->getLocalUid()] = \WeakReference::create($handle);
}

$className = get_debug_type($value);
if (strlen($className) > 160) { // keep result below 255 bytes
$className = mb_strcut($className, 0, 80)
. '...'
. mb_strcut(substr($className, strlen(mb_strcut($className, 0, 80))), -80);
}

return $className . '-' . $this->instanceUid . '-' . $handle->getLocalUid();
}

public function convertToPHPValue($value, AbstractPlatform $platform): ?object
{
if ($value === null || trim($value) === '') {
return null;
}

$valueExploded = explode('-', $value, 3);
if (count($valueExploded) !== 3
|| $valueExploded[1] !== $this->instanceUid
|| $valueExploded[2] !== (string) (int) $valueExploded[2]
) {
throw new Exception('Local object does not match the DBAL type instance');
}
$handle = $this->handlesIndex[(int) $valueExploded[2]] ?? null;
if ($handle !== null) {
$handle = $handle->get();
}
$res = $handle !== null ? $handle->getValue() : null;
if ($res === null) {
throw new Exception('Local object does no longer exist');
}

return $res;
}

public function requiresSQLCommentHint(AbstractPlatform $platform): bool
{
return true;
}
}
42 changes: 42 additions & 0 deletions src/Type/MoneyType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php

declare(strict_types=1);

namespace Atk4\Data\Type;

use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Types as DbalTypes;

class MoneyType extends DbalTypes\Type
{
public function getName(): string
{
return Types::MONEY;
}

public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform): string
{
return DbalTypes\Type::getType(DbalTypes\Types::FLOAT)->getSQLDeclaration($fieldDeclaration, $platform);
}

public function convertToDatabaseValue($value, AbstractPlatform $platform): ?string
{
if ($value === null || trim((string) $value) === '') {
return null;
}

return (string) round((float) $value, 4);
}

public function convertToPHPValue($value, AbstractPlatform $platform): ?float
{
$v = $this->convertToDatabaseValue($value, $platform);

return $v === null ? null : (float) $v;
}

public function requiresSQLCommentHint(AbstractPlatform $platform): bool
{
return true;
}
}
11 changes: 11 additions & 0 deletions src/Type/Types.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

declare(strict_types=1);

namespace Atk4\Data\Type;

final class Types
{
public const LOCAL_OBJECT = 'atk4_local_object';
public const MONEY = 'atk4_money';
}

0 comments on commit 24e5a96

Please sign in to comment.