Created
November 3, 2022 08:25
-
-
Save jmikola/b44353a2d1879bff2e801281a72baf59 to your computer and use it in GitHub Desktop.
Change tracking using ext-mongodb's BSON API
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
$ php tracked_persistable.php | |
object(MongoDB\Examples\User)#29 (3) { | |
["name"]=> | |
string(7) "alcaeus" | |
["emails"]=> | |
object(MongoDB\Examples\TrackedBSONArray)#24 (0) { | |
} | |
["_id"]=> | |
object(MongoDB\BSON\ObjectId)#15 (1) { | |
["oid"]=> | |
string(24) "63637a94909ef8efcf01fc92" | |
} | |
} | |
array(1) { | |
["$set"]=> | |
array(2) { | |
["name"]=> | |
string(7) "andreas" | |
["emails.2"]=> | |
array(2) { | |
["type"]=> | |
string(7) "private" | |
["address"]=> | |
string(19) "[email protected]" | |
} | |
} | |
} | |
object(MongoDB\Examples\User)#32 (3) { | |
["name"]=> | |
string(7) "andreas" | |
["emails"]=> | |
object(MongoDB\Examples\TrackedBSONArray)#35 (0) { | |
} | |
["_id"]=> | |
object(MongoDB\BSON\ObjectId)#31 (1) { | |
["oid"]=> | |
string(24) "63637a94909ef8efcf01fc92" | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
declare(strict_types=1); | |
namespace MongoDB\Examples; | |
use MongoDB\BSON\ObjectId; | |
use MongoDB\BSON\Persistable; | |
use MongoDB\BSON\Serializable; | |
use MongoDB\BSON\Unserializable; | |
use MongoDB\Client; | |
use MongoDB\Model\BSONArray; | |
use ReflectionObject; | |
use ReflectionProperty; | |
use function array_filter; | |
use function array_key_exists; | |
use function count; | |
use function get_object_vars; | |
use function getenv; | |
use function var_dump; | |
require __DIR__ . '/../vendor/autoload.php'; | |
#[Attribute(Attribute::TARGET_PROPERTY)] | |
class TrackChanges | |
{ | |
} | |
interface Tracked extends Serializable, Unserializable | |
{ | |
public function getInitialData(): ?array; | |
} | |
trait TrackedTrait | |
{ | |
private readonly array $__initialData; | |
public function getInitialData(): ?array | |
{ | |
return $this->__initialData ?? null; | |
} | |
private function setInitialData(array $initialData): void | |
{ | |
$this->__initialData = $initialData; | |
} | |
public function __debugInfo(): array | |
{ | |
if ($this instanceof ArrayObject) { | |
return $this->getArrayCopy(); | |
} | |
$data = get_object_vars($this); | |
unset($data['__initialData']); | |
return $data; | |
} | |
} | |
interface TrackedPersistable extends Tracked, Persistable | |
{ | |
} | |
trait TrackedPersistableTrait | |
{ | |
use TrackedTrait; | |
public function bsonSerialize(): array | |
{ | |
$data = []; | |
foreach (getTrackedProperties($this) as $rp) { | |
if ($rp->getValue($this) === null) { | |
continue; | |
} | |
$data[$rp->getName()] = $rp->getValue($this); | |
} | |
return $data; | |
} | |
public function bsonUnserialize(array $data): void | |
{ | |
$initialData = []; | |
foreach (getTrackedProperties($this) as $rp) { | |
$key = $rp->getName(); | |
if (array_key_exists($key, $data)) { | |
$rp->setValue($this, $data[$key]); | |
$initialData[$key] = $data[$key]; | |
} | |
} | |
$this->setInitialData($initialData); | |
} | |
} | |
class TrackedBSONArray extends BSONArray implements Tracked | |
{ | |
use TrackedTrait; | |
public function bsonUnserialize(array $data): void | |
{ | |
$this->setInitialData($data); | |
parent::bsonUnserialize($data); | |
} | |
} | |
/** @return array<ReflectionProperty> */ | |
function getTrackedProperties(Tracked $ct): array | |
{ | |
$filter = ReflectionProperty::IS_PUBLIC | ReflectionProperty::IS_PROTECTED | ReflectionProperty::IS_PRIVATE; | |
return array_filter( | |
(new ReflectionObject($ct))->getProperties($filter), | |
fn (ReflectionProperty $rp) => count($rp->getAttributes(TrackChanges::class)) > 0 | |
); | |
} | |
function getUpdate(TrackedPersistable $ctp): array | |
{ | |
return addChangesToUpdate($ctp); | |
} | |
function addChangesToUpdate(Tracked $ct, array &$update = [], string $prefix = ''): array | |
{ | |
$oldData = $ct->getInitialData(); | |
$newData = $ct->bsonSerialize(); | |
foreach ($newData as $key => $value) { | |
if ($value instanceof Tracked && $value->getInitialData() !== null) { | |
addChangesToUpdate($value, $update, "{$prefix}{$key}."); | |
continue; | |
} | |
if ($value instanceof Tracked && $value->getInitialData() === null) { | |
$value = $value->bsonSerialize(); | |
} | |
// Set fields that are only present in $newData | |
if (! isset($oldData[$key])) { | |
$update['$set']["{$prefix}{$key}"] = $value; | |
continue; | |
} | |
// TODO: Handle changed fields recursively | |
if ($oldData[$key] != $newData[$key]) { | |
$update['$set']["{$prefix}{$key}"] = $value; | |
} | |
} | |
// Unset fields that are only present in $oldData | |
foreach ($oldData as $key => $_) { | |
if (! isset($newData[$key])) { | |
$update['$unset']["{$prefix}{$key}"] = 1; | |
} | |
} | |
return $update; | |
} | |
class Email implements TrackedPersistable | |
{ | |
use TrackedPersistableTrait; | |
public function __construct( | |
#[TrackChanges] | |
public string $type, | |
#[TrackChanges] | |
public string $address | |
) { | |
} | |
} | |
class User implements TrackedPersistable | |
{ | |
use TrackedPersistableTrait; | |
public function __construct( | |
#[TrackChanges] | |
public string $name, | |
#[TrackChanges] | |
public TrackedBSONArray $emails = new TrackedBSONArray(), | |
#[TrackChanges] | |
public readonly ObjectId $_id = new ObjectId(), | |
) { | |
} | |
} | |
$client = new Client( | |
getenv('MONGODB_URI') ?: 'mongodb://127.0.0.1/', | |
[], | |
['typeMap' => ['array' => TrackedBSONArray::class]] | |
); | |
$collection = $client->test->users; | |
$collection->drop(); | |
$user = new User(name: 'alcaeus'); | |
$user->emails[] = new Email('personal', '[email protected]'); | |
$user->emails[] = new Email('work', '[email protected]'); | |
$collection->insertOne($user); | |
$trackedUser = $collection->findOne(); | |
var_dump($trackedUser); | |
$trackedUser->name = 'andreas'; | |
/* Note: this absolutely does not work for removing array elements. That will | |
* likely require more active change tracking at the BSONArray level. */ | |
$trackedUser->emails[] = new Email('private', '[email protected]'); | |
var_dump(getUpdate($trackedUser)); | |
$collection->updateOne(['_id' => $trackedUser->_id], getUpdate($trackedUser)); | |
var_dump($collection->findOne()); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment