From 8e5706aff19a526c8c04598374e1c747c2d17c3a Mon Sep 17 00:00:00 2001 From: Jeremy Dormitzer Date: Sat, 8 Dec 2018 19:12:08 -0500 Subject: [PATCH] Delete orphaned nodes and test update() --- src/Entities/ActivityPubObject.php | 17 +- src/Entities/Field.php | 14 +- src/Objects/ObjectsService.php | 9 +- test/ObjectsServiceTest.php | 246 +++++++++++++++++++++++++++++ 4 files changed, 282 insertions(+), 4 deletions(-) diff --git a/src/Entities/ActivityPubObject.php b/src/Entities/ActivityPubObject.php index 38d0302..d89a7b1 100644 --- a/src/Entities/ActivityPubObject.php +++ b/src/Entities/ActivityPubObject.php @@ -21,7 +21,7 @@ class ActivityPubObject /** * This object's fields - * @OneToMany(targetEntity="Field", mappedBy="object") + * @OneToMany(targetEntity="Field", mappedBy="object", cascade={"persist", "remove"}) * @var Field[] An ArrayCollection of Fields */ protected $fields; @@ -144,6 +144,21 @@ class ActivityPubObject return $this->lastUpdated; } + /** + * Returns true if the object contains a field with key $name + * + * @return boolean + */ + public function hasField( string $name ) + { + foreach( $this->getFields() as $field ) { + if ( $field->getName() === $name ) { + return true; + } + } + return false; + } + /** * Adds a new field on the object * diff --git a/src/Entities/Field.php b/src/Entities/Field.php index 74c3116..bbe68f6 100644 --- a/src/Entities/Field.php +++ b/src/Entities/Field.php @@ -113,7 +113,7 @@ class Field $this->object= $object; } - protected function setTargetObject( ActivityPubObject $targetObject ) + public function setTargetObject( ActivityPubObject $targetObject ) { $this->value = null; $targetObject->addReferencingField( $this ); @@ -164,7 +164,17 @@ class Field } /** - * Returns either the value or the target object of the field, depending on which was set + * Returns the target object of the field or null if there isn't one + * + * @return ActivityPubObject|null + */ + public function getTargetObject() + { + return $this->targetObject; + } + + /** + * Returns the value or the target object of the field, depending on which was set * * @return string|ActivityPubObject */ diff --git a/src/Objects/ObjectsService.php b/src/Objects/ObjectsService.php index d93015b..517456f 100644 --- a/src/Objects/ObjectsService.php +++ b/src/Objects/ObjectsService.php @@ -163,6 +163,10 @@ class ObjectsService * to update and the value is the field's new value. If the value is * null, the field will be deleted. * + * If the update results in an orphaned anonymous node (an ActivityPubObject + * with no 'id' field that no longer has any references to it), then the + * orphaned node will be deleted. + * * @return ActivityPubObject|null The updated object, * or null if an object with that id isn't in the DB */ @@ -176,10 +180,13 @@ class ObjectsService if ( array_key_exists( $field->getName(), $updatedFields ) ) { $newValue = $updatedFields[$field->getName()]; if ( is_array( $newValue ) ) { - // Should I handle orphaned nodes here? $referencedObject = $this->createObject( $newValue ); + $oldTargetObject = $field->getTargetObject(); $field->setTargetObject( $referencedObject ); $this->entityManager->persist( $field ); + if ( $oldTargetObject && ! $oldTargetObject->hasField( 'id' ) ) { + $this->entityManager->remove( $oldTargetObject ); + } } else if ( ! $newValue ) { $object->removeField( $field ); $this->entityManager->persist( $object ); diff --git a/test/ObjectsServiceTest.php b/test/ObjectsServiceTest.php index ea5b3f8..6ffbdb1 100644 --- a/test/ObjectsServiceTest.php +++ b/test/ObjectsServiceTest.php @@ -835,14 +835,260 @@ class ObjectsServiceTest extends SQLiteTestCase public function testItUpdatesObjectFieldToNewObject() { + $fields = array( + 'id' => 'https://example.com/notes/1', + 'type' => 'Note', + 'content' => 'This is a note', + 'attributedTo' => array( + 'id' => 'https://example.com/actors/1', + ), + ); + $createTime = self::getNow(); + $object = $this->objectsService->createObject( $fields ); + $update = array( 'attributedTo' => array( + 'id' => 'https://example.com/actors/2', + ) ); + $updateTime = self::getNow(); + $this->objectsService->updateObject( 'https://example.com/notes/1', $update ); + $expected = new ArrayDataSet( array( + 'objects' => array( + array( + 'id' => 1, + 'created' => $createTime, + 'lastUpdated' => $updateTime + ), + array( + 'id' => 2, + 'created' => $createTime, + 'lastUpdated' => $createTime, + ), + array( + 'id' => 3, + 'created' => $updateTime, + 'lastUpdated' => $updateTime, + ), + ), + 'fields' => array( + array( + 'id' => 1, + 'object_id' => 1, + 'name' => 'id', + 'value' => 'https://example.com/notes/1', + 'created' => $createTime, + 'lastUpdated' => $createTime, + 'targetObject_id' => null, + ), + array( + 'id' => 2, + 'object_id' => 1, + 'name' => 'type', + 'value' => 'Note', + 'created' => $createTime, + 'lastUpdated' => $createTime, + 'targetObject_id' => null, + ), + array( + 'id' => 3, + 'object_id' => 1, + 'name' => 'content', + 'value' => 'This is a note', + 'created' => $createTime, + 'lastUpdated' => $createTime, + 'targetObject_id' => null, + ), + array( + 'id' => 4, + 'object_id' => 2, + 'name' => 'id', + 'value' => 'https://example.com/actors/1', + 'created' => $createTime, + 'lastUpdated' => $createTime, + 'targetObject_id' => null, + ), + array( + 'id' => 5, + 'object_id' => 1, + 'name' => 'attributedTo', + 'value' => null, + 'created' => $createTime, + 'lastUpdated' => $updateTime, + 'targetObject_id' => 3, + ), + array( + 'id' => 6, + 'object_id' => 3, + 'name' => 'id', + 'value' => 'https://example.com/actors/2', + 'created' => $updateTime, + 'lastUpdated' => $updateTime, + 'targetObject_id' => null, + ), + ), + ) ); + $expectedObjectsTable = $expected->getTable('objects'); + $expectedFieldsTable = $expected->getTable('fields'); + $objectsQueryTable = $this->getConnection()->createQueryTable( + 'objects', 'SELECT * FROM objects' + ); + $fieldsQueryTable = $this->getConnection()->createQueryTable( + 'fields', 'SELECT * FROM fields' + ); + $this->assertTablesEqual( $expectedObjectsTable, $objectsQueryTable ); + $this->assertTablesEqual( $expectedFieldsTable, $fieldsQueryTable ); } public function testItUpdatedObjectFieldArray() { + $fields = array( + 'id' => 'https://example.com/notes/1', + 'type' => 'Note', + 'content' => 'This is a note', + 'likes' => array( + 'https://example.com/likes/1', + 'https://example.com/likes/2', + ), + ); + $createTime = self::getNow(); + $object = $this->objectsService->createObject( $fields ); + $update = array( 'likes' => array( + 'https://example.com/likes/3', + 'https://example.com/likes/4', + ) ); + $updateTime = self::getNow(); + $this->objectsService->updateObject( 'https://example.com/notes/1', $update ); + $expected = new ArrayDataSet( array( + 'objects' => array( + array( + 'id' => 1, + 'created' => $createTime, + 'lastUpdated' => $updateTime + ), + array( + 'id' => 3, + 'created' => $updateTime, + 'lastUpdated' => $updateTime, + ), + ), + 'fields' => array( + array( + 'id' => 1, + 'object_id' => 1, + 'name' => 'id', + 'value' => 'https://example.com/notes/1', + 'created' => $createTime, + 'lastUpdated' => $createTime, + 'targetObject_id' => null, + ), + array( + 'id' => 2, + 'object_id' => 1, + 'name' => 'type', + 'value' => 'Note', + 'created' => $createTime, + 'lastUpdated' => $createTime, + 'targetObject_id' => null, + ), + array( + 'id' => 3, + 'object_id' => 1, + 'name' => 'content', + 'value' => 'This is a note', + 'created' => $createTime, + 'lastUpdated' => $createTime, + 'targetObject_id' => null, + ), + array( + 'id' => 6, + 'object_id' => 1, + 'name' => 'likes', + 'value' => null, + 'created' => $createTime, + 'lastUpdated' => $updateTime, + 'targetObject_id' => 3, + ), + array( + 'id' => 7, + 'object_id' => 3, + 'name' => '0', + 'value' => 'https://example.com/likes/3', + 'created' => $updateTime, + 'lastUpdated' => $updateTime, + 'targetObject_id' => null, + ), + array( + 'id' => 8, + 'object_id' => 3, + 'name' => '1', + 'value' => 'https://example.com/likes/4', + 'created' => $updateTime, + 'lastUpdated' => $updateTime, + 'targetObject_id' => null, + ), + ), + ) ); + $expectedObjectsTable = $expected->getTable('objects'); + $expectedFieldsTable = $expected->getTable('fields'); + $objectsQueryTable = $this->getConnection()->createQueryTable( + 'objects', 'SELECT * FROM objects' + ); + $fieldsQueryTable = $this->getConnection()->createQueryTable( + 'fields', 'SELECT * FROM fields' + ); + $this->assertTablesEqual( $expectedObjectsTable, $objectsQueryTable ); + $this->assertTablesEqual( $expectedFieldsTable, $fieldsQueryTable ); } public function testItDeletesObjectField() { + $fields = array( + 'id' => 'https://example.com/notes/1', + 'type' => 'Note', + 'content' => 'This is a note' + ); + $createTime = self::getNow(); + $object = $this->objectsService->createObject( $fields ); + $update = array( 'content' => null ); + $updateTime = self::getNow(); + $this->objectsService->updateObject( 'https://example.com/notes/1', $update ); + $expected = new ArrayDataSet( array( + 'objects' => array( + array( + 'id' => 1, + 'created' => $createTime, + 'lastUpdated' => $updateTime + ), + ), + 'fields' => array( + array( + 'id' => 1, + 'object_id' => 1, + 'name' => 'id', + 'value' => 'https://example.com/notes/1', + 'created' => $createTime, + 'lastUpdated' => $createTime, + 'targetObject_id' => null, + ), + array( + 'id' => 2, + 'object_id' => 1, + 'name' => 'type', + 'value' => 'Note', + 'created' => $createTime, + 'lastUpdated' => $createTime, + 'targetObject_id' => null, + ), + ), + ) ); + $expectedObjectsTable = $expected->getTable('objects'); + $expectedFieldsTable = $expected->getTable('fields'); + $objectsQueryTable = $this->getConnection()->createQueryTable( + 'objects', 'SELECT * FROM objects' + ); + $fieldsQueryTable = $this->getConnection()->createQueryTable( + 'fields', 'SELECT * FROM fields' + ); + $this->assertTablesEqual( $expectedObjectsTable, $objectsQueryTable ); + $this->assertTablesEqual( $expectedFieldsTable, $fieldsQueryTable ); } } ?>