diff --git a/src/Extensions/FamilySearch/Platform/Tree/AdditionalAttribution.php b/src/Extensions/FamilySearch/Platform/Tree/AdditionalAttribution.php new file mode 100644 index 00000000..17f45d15 --- /dev/null +++ b/src/Extensions/FamilySearch/Platform/Tree/AdditionalAttribution.php @@ -0,0 +1,133 @@ +setResource('https://familysearch.org/platform/users/USER123'); + * $additionalAttr->setContributor($contributor); + * + * $additionalAttr->setModified(time() * 1000); // milliseconds + * $additionalAttr->setChangeMessage('Added birth date from parish records'); + * + * // This can then be attached to a person, fact, or relationship + * ``` + * + * ## XML Example + * + * ```xml + * + * + * 1625097600000 + * Added birth date from parish records + * + * ``` + * + * ## JSON Example + * + * ```json + * { + * "contributor": { + * "resource": "https://familysearch.org/platform/users/USER123" + * }, + * "modified": 1625097600000, + * "changeMessage": "Added birth date from parish records" + * } + * ``` + * + * @package Gedcomx\Extensions\FamilySearch\Platform\Tree + */ +class AdditionalAttribution extends Attribution +{ + /** + * Constructs an AdditionalAttribution from a (parsed) JSON hash or XML reader. + * + * @param mixed $o Either an array (JSON) or an XMLReader. + * + * @throws \Exception + */ + public function __construct($o = null) + { + parent::__construct($o); + } + + /** + * Writes this AdditionalAttribution to an XML writer. + * + * Overrides the parent method to use the FamilySearch-specific element name. + * + * @param \XMLWriter $writer The XML writer. + * @param bool $includeNamespaces Whether to write out the namespaces in the element. + */ + public function toXml($writer, $includeNamespaces = true) + { + $writer->startElementNS('fs', 'additionalAttribution', null); + if ($includeNamespaces) { + $writer->writeAttributeNs('xmlns', 'fs', null, 'http://familysearch.org/v1/'); + $writer->writeAttributeNs('xmlns', 'gx', null, 'http://gedcomx.org/v1/'); + } + $this->writeXmlContents($writer); + $writer->endElement(); + } +} diff --git a/src/Extensions/FamilySearch/Platform/Tree/ChangeType.php b/src/Extensions/FamilySearch/Platform/Tree/ChangeType.php new file mode 100644 index 00000000..92e8bf4a --- /dev/null +++ b/src/Extensions/FamilySearch/Platform/Tree/ChangeType.php @@ -0,0 +1,279 @@ +initFromArray($o); + } + else if ($o instanceof \XMLReader) { + $success = true; + while ($success && $o->nodeType != \XMLReader::ELEMENT) { + $success = $o->read(); + } + if ($o->nodeType != \XMLReader::ELEMENT) { + throw new \Exception("Unable to read XML: no start element found."); + } + + $this->initFromReader($o); + } + } + + /** + * Get the group id. + * + * @return string + */ + public function getId() + { + return $this->id; + } + + /** + * Set the group id. + * + * @param string $id + */ + public function setId($id) + { + $this->id = $id; + } + + /** + * Get the group name. + * + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * Set the group name. + * + * @param string $name + */ + public function setName($name) + { + $this->name = $name; + } + + /** + * Get the group description. + * + * @return string + */ + public function getDescription() + { + return $this->description; + } + + /** + * Set the group description. + * + * @param string $description + */ + public function setDescription($description) + { + $this->description = $description; + } + + /** + * Get the group code of conduct. + * + * @return string + */ + public function getCodeOfConduct() + { + return $this->codeOfConduct; + } + + /** + * Set the group code of conduct. + * + * @param string $codeOfConduct + */ + public function setCodeOfConduct($codeOfConduct) + { + $this->codeOfConduct = $codeOfConduct; + } + + /** + * Get the ids of the trees associated with the group. + * + * @return string[] + */ + public function getTreeIds() + { + return $this->treeIds; + } + + /** + * Set the ids of the trees associated with the group. + * + * @param string[] $treeIds + */ + public function setTreeIds($treeIds) + { + $this->treeIds = $treeIds; + } + + /** + * Get the members of the group. + * + * @return GroupMember[] + */ + public function getMembers() + { + return $this->members; + } + + /** + * Set the members of the group. + * + * @param GroupMember[] $members + */ + public function setMembers($members) + { + $this->members = $members; + } + + /** + * Add a member to the group. + * + * @param GroupMember $member + */ + public function addMember($member) + { + if ($this->members === null) { + $this->members = array(); + } + $this->members[] = $member; + } + + /** + * Returns the associative array for this Group. + * + * @return array + */ + public function toArray() + { + $a = array(); + if ($this->id) { + $a["id"] = $this->id; + } + if ($this->name) { + $a["name"] = $this->name; + } + if ($this->description) { + $a["description"] = $this->description; + } + if ($this->codeOfConduct) { + $a["codeOfConduct"] = $this->codeOfConduct; + } + if ($this->treeIds) { + $a["treeIds"] = $this->treeIds; + } + if ($this->members) { + $ab = array(); + foreach ($this->members as $i => $x) { + $ab[$i] = $x->toArray(); + } + $a["members"] = $ab; + } + return $a; + } + + /** + * Returns the JSON string for this Group. + * + * @return string + */ + public function toJson() + { + return json_encode($this->toArray()); + } + + /** + * Initializes this Group from an associative array. + * + * @param array $o + */ + public function initFromArray($o) + { + if (isset($o['id'])) { + $this->id = $o["id"]; + } + if (isset($o['name'])) { + $this->name = $o["name"]; + } + if (isset($o['description'])) { + $this->description = $o["description"]; + } + if (isset($o['codeOfConduct'])) { + $this->codeOfConduct = $o["codeOfConduct"]; + } + if (isset($o['treeIds'])) { + $this->treeIds = $o["treeIds"]; + } + if (isset($o['members'])) { + $this->members = array(); + foreach ($o['members'] as $i => $x) { + $this->members[$i] = new GroupMember($x); + } + } + } + + /** + * Initializes this Group from an XML reader. + * + * @param \XMLReader $xml The reader to use to initialize this object. + */ + public function initFromReader($xml) + { + $empty = $xml->isEmptyElement; + + if ($xml->hasAttributes) { + $moreAttributes = $xml->moveToFirstAttribute(); + while ($moreAttributes) { + if (!$this->setKnownAttribute($xml)) { + //skip unknown attributes... + } + $moreAttributes = $xml->moveToNextAttribute(); + } + } + + if (!$empty) { + $xml->read(); + while ($xml->nodeType != \XMLReader::END_ELEMENT) { + if ($xml->nodeType != \XMLReader::ELEMENT) { + //no-op: skip any insignificant whitespace, comments, etc. + } + else if (!$this->setKnownChildElement($xml)) { + $n = $xml->localName; + $ns = $xml->namespaceURI; + //skip the unknown element + while ($xml->nodeType != \XMLReader::END_ELEMENT && $xml->localName != $n && $xml->namespaceURI != $ns) { + $xml->read(); + } + } + $xml->read(); //advance the reader. + } + } + } + + /** + * Sets a known child element of Group from an XML reader. + * + * @param \XMLReader $xml The reader. + * + * @return bool Whether a child element was set. + */ + protected function setKnownChildElement($xml) + { + $happened = false; + if (($xml->localName == 'member') && ($xml->namespaceURI == 'http://familysearch.org/v1/')) { + if ($this->members === null) { + $this->members = array(); + } + $child = new GroupMember($xml); + array_push($this->members, $child); + $happened = true; + } + else if (($xml->localName == 'treeId') && ($xml->namespaceURI == 'http://familysearch.org/v1/')) { + if ($this->treeIds === null) { + $this->treeIds = array(); + } + $this->treeIds[] = $xml->readString(); + $happened = true; + } + return $happened; + } + + /** + * Sets a known attribute of Group from an XML reader. + * + * @param \XMLReader $xml The reader. + * + * @return bool Whether an attribute was set. + */ + protected function setKnownAttribute($xml) + { + return false; + } + + /** + * Writes this Group to an XML writer. + * + * @param \XMLWriter $writer The XML writer. + * @param bool $includeNamespaces Whether to write out the namespaces in the element. + */ + public function toXml($writer, $includeNamespaces = true) + { + $writer->startElementNS('fs', 'group', null); + if ($includeNamespaces) { + $writer->writeAttributeNs('xmlns', 'fs', null, 'http://familysearch.org/v1/'); + } + $this->writeXmlContents($writer); + $writer->endElement(); + } + + /** + * Writes the contents of this Group to an XML writer. + * The startElement is expected to be already provided. + * + * @param \XMLWriter $writer The XML writer. + */ + public function writeXmlContents($writer) + { + if ($this->id) { + $writer->writeElement('id', $this->id); + } + if ($this->name) { + $writer->writeElement('name', $this->name); + } + if ($this->description) { + $writer->writeElement('description', $this->description); + } + if ($this->codeOfConduct) { + $writer->writeElement('codeOfConduct', $this->codeOfConduct); + } + if ($this->treeIds) { + foreach ($this->treeIds as $treeId) { + $writer->writeElement('treeId', $treeId); + } + } + if ($this->members) { + foreach ($this->members as $member) { + $writer->startElementNS('fs', 'member', null); + $member->writeXmlContents($writer); + $writer->endElement(); + } + } + } +} diff --git a/src/Extensions/FamilySearch/Platform/Tree/GroupMember.php b/src/Extensions/FamilySearch/Platform/Tree/GroupMember.php new file mode 100644 index 00000000..0519adc9 --- /dev/null +++ b/src/Extensions/FamilySearch/Platform/Tree/GroupMember.php @@ -0,0 +1,327 @@ +initFromArray($o); + } + else if ($o instanceof \XMLReader) { + $success = true; + while ($success && $o->nodeType != \XMLReader::ELEMENT) { + $success = $o->read(); + } + if ($o->nodeType != \XMLReader::ELEMENT) { + throw new \Exception("Unable to read XML: no start element found."); + } + + $this->initFromReader($o); + } + } + + /** + * Get the group id. + * + * @return string + */ + public function getGroupId() + { + return $this->groupId; + } + + /** + * Set the group id. + * + * @param string $groupId + */ + public function setGroupId($groupId) + { + $this->groupId = $groupId; + } + + /** + * Get the cisId of the group member. + * + * @return string + */ + public function getCisId() + { + return $this->cisId; + } + + /** + * Set the cisId of the group member. + * + * @param string $cisId + */ + public function setCisId($cisId) + { + $this->cisId = $cisId; + } + + /** + * Get the contact name of the group member. + * + * @return string + */ + public function getContactName() + { + return $this->contactName; + } + + /** + * Set the contact name of the group member. + * + * @param string $contactName + */ + public function setContactName($contactName) + { + $this->contactName = $contactName; + } + + /** + * Get the status of the group member. + * + * @return string + */ + public function getStatus() + { + return $this->status; + } + + /** + * Set the status of the group member. + * + * @param string $status + */ + public function setStatus($status) + { + $this->status = $status; + } + + /** + * Returns the associative array for this GroupMember. + * + * @return array + */ + public function toArray() + { + $a = array(); + if ($this->groupId) { + $a["groupId"] = $this->groupId; + } + if ($this->cisId) { + $a["cisId"] = $this->cisId; + } + if ($this->contactName) { + $a["contactName"] = $this->contactName; + } + if ($this->status) { + $a["status"] = $this->status; + } + return $a; + } + + /** + * Returns the JSON string for this GroupMember. + * + * @return string + */ + public function toJson() + { + return json_encode($this->toArray()); + } + + /** + * Initializes this GroupMember from an associative array. + * + * @param array $o + */ + public function initFromArray($o) + { + if (isset($o['groupId'])) { + $this->groupId = $o["groupId"]; + } + if (isset($o['cisId'])) { + $this->cisId = $o["cisId"]; + } + if (isset($o['contactName'])) { + $this->contactName = $o["contactName"]; + } + if (isset($o['status'])) { + $this->status = $o["status"]; + } + } + + /** + * Initializes this GroupMember from an XML reader. + * + * @param \XMLReader $xml The reader to use to initialize this object. + */ + public function initFromReader($xml) + { + $empty = $xml->isEmptyElement; + + if ($xml->hasAttributes) { + $moreAttributes = $xml->moveToFirstAttribute(); + while ($moreAttributes) { + if (!$this->setKnownAttribute($xml)) { + //skip unknown attributes... + } + $moreAttributes = $xml->moveToNextAttribute(); + } + } + + if (!$empty) { + $xml->read(); + while ($xml->nodeType != \XMLReader::END_ELEMENT) { + if ($xml->nodeType != \XMLReader::ELEMENT) { + //no-op: skip any insignificant whitespace, comments, etc. + } + else if (!$this->setKnownChildElement($xml)) { + $n = $xml->localName; + $ns = $xml->namespaceURI; + //skip the unknown element + while ($xml->nodeType != \XMLReader::END_ELEMENT && $xml->localName != $n && $xml->namespaceURI != $ns) { + $xml->read(); + } + } + $xml->read(); //advance the reader. + } + } + } + + /** + * Sets a known child element of GroupMember from an XML reader. + * + * @param \XMLReader $xml The reader. + * + * @return bool Whether a child element was set. + */ + protected function setKnownChildElement($xml) + { + return false; + } + + /** + * Sets a known attribute of GroupMember from an XML reader. + * + * @param \XMLReader $xml The reader. + * + * @return bool Whether an attribute was set. + */ + protected function setKnownAttribute($xml) + { + return false; + } + + /** + * Writes this GroupMember to an XML writer. + * + * @param \XMLWriter $writer The XML writer. + * @param bool $includeNamespaces Whether to write out the namespaces in the element. + */ + public function toXml($writer, $includeNamespaces = true) + { + $writer->startElementNS('fs', 'groupMember', null); + if ($includeNamespaces) { + $writer->writeAttributeNs('xmlns', 'fs', null, 'http://familysearch.org/v1/'); + } + $this->writeXmlContents($writer); + $writer->endElement(); + } + + /** + * Writes the contents of this GroupMember to an XML writer. + * The startElement is expected to be already provided. + * + * @param \XMLWriter $writer The XML writer. + */ + public function writeXmlContents($writer) + { + if ($this->groupId) { + $writer->writeElement('groupId', $this->groupId); + } + if ($this->cisId) { + $writer->writeElement('cisId', $this->cisId); + } + if ($this->contactName) { + $writer->writeElement('contactName', $this->contactName); + } + if ($this->status) { + $writer->writeElement('status', $this->status); + } + } +} diff --git a/src/Extensions/FamilySearch/Platform/Tree/MatchCollection.php b/src/Extensions/FamilySearch/Platform/Tree/MatchCollection.php new file mode 100644 index 00000000..f35acf73 --- /dev/null +++ b/src/Extensions/FamilySearch/Platform/Tree/MatchCollection.php @@ -0,0 +1,88 @@ +initFromArray($o); + } + else if ($o instanceof \XMLReader) { + $success = true; + while ($success && $o->nodeType != \XMLReader::ELEMENT) { + $success = $o->read(); + } + if ($o->nodeType != \XMLReader::ELEMENT) { + throw new \Exception("Unable to read XML: no start element found."); + } + + $this->initFromReader($o); + } + } + + /** + * The ordering of the name form. + * + * @return string The URI representing the name form order (Eurotypic or Sinotypic). + */ + public function getOrder() + { + return $this->order; + } + + /** + * The ordering of the name form. + * + * @param string $order The URI representing the name form order. + */ + public function setOrder($order) + { + $this->order = $order; + } + + /** + * Returns the associative array for this NameFormInfo. + * + * @return array + */ + public function toArray() + { + $a = array(); + if ($this->order) { + $a["order"] = $this->order; + } + return $a; + } + + /** + * Returns the JSON string for this NameFormInfo. + * + * @return string + */ + public function toJson() + { + return json_encode($this->toArray()); + } + + /** + * Initializes this NameFormInfo from an associative array. + * + * @param array $o + */ + public function initFromArray($o) + { + if (isset($o['order'])) { + $this->order = $o["order"]; + } + } + + /** + * Initializes this NameFormInfo from an XML reader. + * + * @param \XMLReader $xml The reader to use to initialize this object. + */ + public function initFromReader($xml) + { + $empty = $xml->isEmptyElement; + + if ($xml->hasAttributes) { + $moreAttributes = $xml->moveToFirstAttribute(); + while ($moreAttributes) { + if (!$this->setKnownAttribute($xml)) { + //skip unknown attributes... + } + $moreAttributes = $xml->moveToNextAttribute(); + } + } + + if (!$empty) { + $xml->read(); + while ($xml->nodeType != \XMLReader::END_ELEMENT) { + if ($xml->nodeType != \XMLReader::ELEMENT) { + //no-op: skip any insignificant whitespace, comments, etc. + } + else if (!$this->setKnownChildElement($xml)) { + $n = $xml->localName; + $ns = $xml->namespaceURI; + //skip the unknown element + while ($xml->nodeType != \XMLReader::END_ELEMENT && $xml->localName != $n && $xml->namespaceURI != $ns) { + $xml->read(); + } + } + $xml->read(); //advance the reader. + } + } + } + + /** + * Sets a known child element of NameFormInfo from an XML reader. + * + * @param \XMLReader $xml The reader. + * + * @return bool Whether a child element was set. + */ + protected function setKnownChildElement($xml) + { + return false; + } + + /** + * Sets a known attribute of NameFormInfo from an XML reader. + * + * @param \XMLReader $xml The reader. + * + * @return bool Whether an attribute was set. + */ + protected function setKnownAttribute($xml) + { + if (($xml->localName == 'order') && (empty($xml->namespaceURI))) { + $this->order = $xml->value; + return true; + } + + return false; + } + + /** + * Writes this NameFormInfo to an XML writer. + * + * @param \XMLWriter $writer The XML writer. + * @param bool $includeNamespaces Whether to write out the namespaces in the element. + */ + public function toXml($writer, $includeNamespaces = true) + { + $writer->startElementNS('fs', 'nameFormInfo', null); + if ($includeNamespaces) { + $writer->writeAttributeNs('xmlns', 'fs', null, 'http://familysearch.org/v1/'); + } + $this->writeXmlContents($writer); + $writer->endElement(); + } + + /** + * Writes the contents of this NameFormInfo to an XML writer. + * The startElement is expected to be already provided. + * + * @param \XMLWriter $writer The XML writer. + */ + public function writeXmlContents($writer) + { + if ($this->order) { + $writer->writeAttribute('order', $this->order); + } + } +} diff --git a/src/Extensions/FamilySearch/Platform/Tree/NameFormOrder.php b/src/Extensions/FamilySearch/Platform/Tree/NameFormOrder.php new file mode 100644 index 00000000..66a403e2 --- /dev/null +++ b/src/Extensions/FamilySearch/Platform/Tree/NameFormOrder.php @@ -0,0 +1,56 @@ +initFromArray($o); + } + else if ($o instanceof \XMLReader) { + $success = true; + while ($success && $o->nodeType != \XMLReader::ELEMENT) { + $success = $o->read(); + } + if ($o->nodeType != \XMLReader::ELEMENT) { + throw new \Exception("Unable to read XML: no start element found."); + } + + $this->initFromReader($o); + } + } + + /** + * True if the person is editable by the current user; false otherwise. + * + * @return bool + */ + public function getCanUserEdit() + { + return $this->canUserEdit; + } + + /** + * True if the person is editable by the current user; false otherwise. + * + * @param bool $canUserEdit + */ + public function setCanUserEdit($canUserEdit) + { + $this->canUserEdit = $canUserEdit; + } + + /** + * True if the person is visible to all sessions authenticated from any client; false otherwise. + * + * @return bool + */ + public function getVisibleToAll() + { + return $this->visibleToAll; + } + + /** + * True if the person is visible to all sessions authenticated from any client; false otherwise. + * + * @param bool $visibleToAll + */ + public function setVisibleToAll($visibleToAll) + { + $this->visibleToAll = $visibleToAll; + } + + /** + * True if the person is only visible to sessions authenticated from a FamilySearch client; false otherwise. + * + * @return bool + */ + public function getVisibleToAllWhenUsingFamilySearchApps() + { + return $this->visibleToAllWhenUsingFamilySearchApps; + } + + /** + * True if the person is only visible to sessions authenticated from a FamilySearch client; false otherwise. + * + * @param bool $visibleToAllWhenUsingFamilySearchApps + */ + public function setVisibleToAllWhenUsingFamilySearchApps($visibleToAllWhenUsingFamilySearchApps) + { + $this->visibleToAllWhenUsingFamilySearchApps = $visibleToAllWhenUsingFamilySearchApps; + } + + /** + * The tree id for this person. + * Note: This attribute is prototype only and may be removed or changed at any time. + * + * @return string + */ + public function getTreeId() + { + return $this->treeId; + } + + /** + * The tree id for this person. + * Note: This attribute is prototype only and may be removed or changed at any time. + * + * @param string $treeId + */ + public function setTreeId($treeId) + { + $this->treeId = $treeId; + } + + /** + * Returns the associative array for this PersonInfo. + * + * @return array + */ + public function toArray() + { + $a = array(); + if ($this->canUserEdit !== null) { + $a["canUserEdit"] = $this->canUserEdit; + } + if ($this->visibleToAll !== null) { + $a["visibleToAll"] = $this->visibleToAll; + } + if ($this->visibleToAllWhenUsingFamilySearchApps !== null) { + $a["visibleToAllWhenUsingFamilySearchApps"] = $this->visibleToAllWhenUsingFamilySearchApps; + } + if ($this->treeId) { + $a["treeId"] = $this->treeId; + } + return $a; + } + + /** + * Returns the JSON string for this PersonInfo. + * + * @return string + */ + public function toJson() + { + return json_encode($this->toArray()); + } + + /** + * Initializes this PersonInfo from an associative array. + * + * @param array $o + */ + public function initFromArray($o) + { + if (isset($o['canUserEdit'])) { + $this->canUserEdit = $o["canUserEdit"]; + } + if (isset($o['visibleToAll'])) { + $this->visibleToAll = $o["visibleToAll"]; + } + if (isset($o['visibleToAllWhenUsingFamilySearchApps'])) { + $this->visibleToAllWhenUsingFamilySearchApps = $o["visibleToAllWhenUsingFamilySearchApps"]; + } + if (isset($o['treeId'])) { + $this->treeId = $o["treeId"]; + } + } + + /** + * Initializes this PersonInfo from an XML reader. + * + * @param \XMLReader $xml The reader to use to initialize this object. + */ + public function initFromReader($xml) + { + $empty = $xml->isEmptyElement; + + if ($xml->hasAttributes) { + $moreAttributes = $xml->moveToFirstAttribute(); + while ($moreAttributes) { + if (!$this->setKnownAttribute($xml)) { + //skip unknown attributes... + } + $moreAttributes = $xml->moveToNextAttribute(); + } + } + + if (!$empty) { + $xml->read(); + while ($xml->nodeType != \XMLReader::END_ELEMENT) { + if ($xml->nodeType != \XMLReader::ELEMENT) { + //no-op: skip any insignificant whitespace, comments, etc. + } + else if (!$this->setKnownChildElement($xml)) { + $n = $xml->localName; + $ns = $xml->namespaceURI; + //skip the unknown element + while ($xml->nodeType != \XMLReader::END_ELEMENT && $xml->localName != $n && $xml->namespaceURI != $ns) { + $xml->read(); + } + } + $xml->read(); //advance the reader. + } + } + } + + /** + * Sets a known child element of PersonInfo from an XML reader. + * + * @param \XMLReader $xml The reader. + * + * @return bool Whether a child element was set. + */ + protected function setKnownChildElement($xml) + { + return false; + } + + /** + * Sets a known attribute of PersonInfo from an XML reader. + * + * @param \XMLReader $xml The reader. + * + * @return bool Whether an attribute was set. + */ + protected function setKnownAttribute($xml) + { + if (($xml->localName == 'canUserEdit') && (empty($xml->namespaceURI))) { + $this->canUserEdit = (strtolower($xml->value) === 'true' || $xml->value === '1'); + return true; + } + if (($xml->localName == 'visibleToAll') && (empty($xml->namespaceURI))) { + $this->visibleToAll = (strtolower($xml->value) === 'true' || $xml->value === '1'); + return true; + } + if (($xml->localName == 'visibleToAllWhenUsingFamilySearchApps') && (empty($xml->namespaceURI))) { + $this->visibleToAllWhenUsingFamilySearchApps = (strtolower($xml->value) === 'true' || $xml->value === '1'); + return true; + } + if (($xml->localName == 'treeId') && (empty($xml->namespaceURI))) { + $this->treeId = $xml->value; + return true; + } + + return false; + } + + /** + * Writes this PersonInfo to an XML writer. + * + * @param \XMLWriter $writer The XML writer. + * @param bool $includeNamespaces Whether to write out the namespaces in the element. + */ + public function toXml($writer, $includeNamespaces = true) + { + $writer->startElementNS('fs', 'personInfo', null); + if ($includeNamespaces) { + $writer->writeAttributeNs('xmlns', 'fs', null, 'http://familysearch.org/v1/'); + } + $this->writeXmlContents($writer); + $writer->endElement(); + } + + /** + * Writes the contents of this PersonInfo to an XML writer. + * The startElement is expected to be already provided. + * + * @param \XMLWriter $writer The XML writer. + */ + public function writeXmlContents($writer) + { + if ($this->canUserEdit !== null) { + $writer->writeAttribute('canUserEdit', $this->canUserEdit ? 'true' : 'false'); + } + if ($this->visibleToAll !== null) { + $writer->writeAttribute('visibleToAll', $this->visibleToAll ? 'true' : 'false'); + } + if ($this->visibleToAllWhenUsingFamilySearchApps !== null) { + $writer->writeAttribute('visibleToAllWhenUsingFamilySearchApps', $this->visibleToAllWhenUsingFamilySearchApps ? 'true' : 'false'); + } + if ($this->treeId) { + $writer->writeAttribute('treeId', $this->treeId); + } + } +} diff --git a/src/Extensions/FamilySearch/Platform/Tree/RelationshipRole.php b/src/Extensions/FamilySearch/Platform/Tree/RelationshipRole.php new file mode 100644 index 00000000..20a99caf --- /dev/null +++ b/src/Extensions/FamilySearch/Platform/Tree/RelationshipRole.php @@ -0,0 +1,78 @@ +initFromArray($o); + } + else if ($o instanceof \XMLReader) { + $success = true; + while ($success && $o->nodeType != \XMLReader::ELEMENT) { + $success = $o->read(); + } + if ($o->nodeType != \XMLReader::ELEMENT) { + throw new \Exception("Unable to read XML: no start element found."); + } + + $this->initFromReader($o); + } + } + + /** + * The total number of search hits. + * + * @return int + */ + public function getTotalHits() + { + return $this->totalHits; + } + + /** + * The total number of search hits. + * + * @param int $totalHits + */ + public function setTotalHits($totalHits) + { + $this->totalHits = $totalHits; + } + + /** + * The number of close hits. + * + * @return int + */ + public function getCloseHits() + { + return $this->closeHits; + } + + /** + * The number of close hits. + * + * @param int $closeHits + */ + public function setCloseHits($closeHits) + { + $this->closeHits = $closeHits; + } + + /** + * Returns the associative array for this SearchInfo. + * + * @return array + */ + public function toArray() + { + $a = array(); + if ($this->totalHits !== null) { + $a["totalHits"] = $this->totalHits; + } + if ($this->closeHits !== null) { + $a["closeHits"] = $this->closeHits; + } + return $a; + } + + /** + * Returns the JSON string for this SearchInfo. + * + * @return string + */ + public function toJson() + { + return json_encode($this->toArray()); + } + + /** + * Initializes this SearchInfo from an associative array. + * + * @param array $o + */ + public function initFromArray($o) + { + if (isset($o['totalHits'])) { + $this->totalHits = $o["totalHits"]; + } + if (isset($o['closeHits'])) { + $this->closeHits = $o["closeHits"]; + } + } + + /** + * Initializes this SearchInfo from an XML reader. + * + * @param \XMLReader $xml The reader to use to initialize this object. + */ + public function initFromReader($xml) + { + $empty = $xml->isEmptyElement; + + if ($xml->hasAttributes) { + $moreAttributes = $xml->moveToFirstAttribute(); + while ($moreAttributes) { + if (!$this->setKnownAttribute($xml)) { + //skip unknown attributes... + } + $moreAttributes = $xml->moveToNextAttribute(); + } + } + + if (!$empty) { + $xml->read(); + while ($xml->nodeType != \XMLReader::END_ELEMENT) { + if ($xml->nodeType != \XMLReader::ELEMENT) { + //no-op: skip any insignificant whitespace, comments, etc. + } + else if (!$this->setKnownChildElement($xml)) { + $n = $xml->localName; + $ns = $xml->namespaceURI; + //skip the unknown element + while ($xml->nodeType != \XMLReader::END_ELEMENT && $xml->localName != $n && $xml->namespaceURI != $ns) { + $xml->read(); + } + } + $xml->read(); //advance the reader. + } + } + } + + /** + * Sets a known child element of SearchInfo from an XML reader. + * + * @param \XMLReader $xml The reader. + * + * @return bool Whether a child element was set. + */ + protected function setKnownChildElement($xml) + { + return false; + } + + /** + * Sets a known attribute of SearchInfo from an XML reader. + * + * @param \XMLReader $xml The reader. + * + * @return bool Whether an attribute was set. + */ + protected function setKnownAttribute($xml) + { + return false; + } + + /** + * Writes this SearchInfo to an XML writer. + * + * @param \XMLWriter $writer The XML writer. + * @param bool $includeNamespaces Whether to write out the namespaces in the element. + */ + public function toXml($writer, $includeNamespaces = true) + { + $writer->startElementNS('fs', 'searchInfo', null); + if ($includeNamespaces) { + $writer->writeAttributeNs('xmlns', 'fs', null, 'http://familysearch.org/v1/'); + } + $this->writeXmlContents($writer); + $writer->endElement(); + } + + /** + * Writes the contents of this SearchInfo to an XML writer. + * The startElement is expected to be already provided. + * + * @param \XMLWriter $writer The XML writer. + */ + public function writeXmlContents($writer) + { + if ($this->totalHits !== null) { + $writer->writeElement('totalHits', $this->totalHits); + } + if ($this->closeHits !== null) { + $writer->writeElement('closeHits', $this->closeHits); + } + } +} diff --git a/src/Extensions/FamilySearch/Platform/Tree/SourceReferenceTagType.php b/src/Extensions/FamilySearch/Platform/Tree/SourceReferenceTagType.php new file mode 100644 index 00000000..4a937650 --- /dev/null +++ b/src/Extensions/FamilySearch/Platform/Tree/SourceReferenceTagType.php @@ -0,0 +1,54 @@ +initFromArray($o); + } + else if ($o instanceof \XMLReader) { + $success = true; + while ($success && $o->nodeType != \XMLReader::ELEMENT) { + $success = $o->read(); + } + if ($o->nodeType != \XMLReader::ELEMENT) { + throw new \Exception("Unable to read XML: no start element found."); + } + + $this->initFromReader($o); + } + } + + /** + * Get the tree id. + * + * @return string + */ + public function getId() + { + return $this->id; + } + + /** + * Set the tree id. + * + * @param string $id + */ + public function setId($id) + { + $this->id = $id; + } + + /** + * Get the ids of the groups this tree belongs to. + * + * @return string[] + */ + public function getGroupIds() + { + return $this->groupIds; + } + + /** + * Set the ids of the groups this tree belongs to. + * + * @param string[] $groupIds + */ + public function setGroupIds($groupIds) + { + $this->groupIds = $groupIds; + } + + /** + * Get the tree name. + * + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * Set the tree name. + * + * @param string $name + */ + public function setName($name) + { + $this->name = $name; + } + + /** + * Get the tree description. + * + * @return string + */ + public function getDescription() + { + return $this->description; + } + + /** + * Set the tree description. + * + * @param string $description + */ + public function setDescription($description) + { + $this->description = $description; + } + + /** + * Get the tree starting person id. + * + * @return string + */ + public function getStartingPersonId() + { + return $this->startingPersonId; + } + + /** + * Set the tree starting person id. + * + * @param string $startingPersonId + */ + public function setStartingPersonId($startingPersonId) + { + $this->startingPersonId = $startingPersonId; + } + + /** + * Get the hidden state of the tree. + * + * @return bool + */ + public function getHidden() + { + return $this->hidden; + } + + /** + * Set the hidden state of the tree. + * + * @param bool $hidden + */ + public function setHidden($hidden) + { + $this->hidden = $hidden; + } + + /** + * Get the private state of the tree. + * + * @return bool + */ + public function getPrivate() + { + return $this->isPrivate; + } + + /** + * Set the private state of the tree. + * + * @param bool $isPrivate + */ + public function setPrivate($isPrivate) + { + $this->isPrivate = $isPrivate; + } + + /** + * Get the id of the collection the tree belongs to. + * + * @return string + */ + public function getCollectionId() + { + return $this->collectionId; + } + + /** + * Set the id of the collection the tree belongs to. + * + * @param string $collectionId + */ + public function setCollectionId($collectionId) + { + $this->collectionId = $collectionId; + } + + /** + * Get the owner third party access state of the tree. + * + * @return string + */ + public function getOwnerAccess() + { + return $this->ownerAccess; + } + + /** + * Set the owner third party access state of the tree. + * + * @param string $ownerAccess + */ + public function setOwnerAccess($ownerAccess) + { + $this->ownerAccess = $ownerAccess; + } + + /** + * Get the group third party access state of the tree. + * + * @return string + */ + public function getGroupAccess() + { + return $this->groupAccess; + } + + /** + * Set the group third party access state of the tree. + * + * @param string $groupAccess + */ + public function setGroupAccess($groupAccess) + { + $this->groupAccess = $groupAccess; + } + + /** + * Get the all third party access state of the tree. + * + * @return string + */ + public function getAllAccess() + { + return $this->allAccess; + } + + /** + * Set the all third party access state of the tree. + * + * @param string $allAccess + */ + public function setAllAccess($allAccess) + { + $this->allAccess = $allAccess; + } + + /** + * Returns the associative array for this Tree. + * + * @return array + */ + public function toArray() + { + $a = array(); + if ($this->id) { + $a["id"] = $this->id; + } + if ($this->groupIds) { + $a["groupIds"] = $this->groupIds; + } + if ($this->name) { + $a["name"] = $this->name; + } + if ($this->description) { + $a["description"] = $this->description; + } + if ($this->startingPersonId) { + $a["startingPersonId"] = $this->startingPersonId; + } + if ($this->hidden !== null) { + $a["hidden"] = $this->hidden; + } + if ($this->isPrivate !== null) { + $a["private"] = $this->isPrivate; + } + if ($this->collectionId) { + $a["collectionId"] = $this->collectionId; + } + if ($this->ownerAccess) { + $a["ownerAccess"] = $this->ownerAccess; + } + if ($this->groupAccess) { + $a["groupAccess"] = $this->groupAccess; + } + if ($this->allAccess) { + $a["allAccess"] = $this->allAccess; + } + return $a; + } + + /** + * Returns the JSON string for this Tree. + * + * @return string + */ + public function toJson() + { + return json_encode($this->toArray()); + } + + /** + * Initializes this Tree from an associative array. + * + * @param array $o + */ + public function initFromArray($o) + { + if (isset($o['id'])) { + $this->id = $o["id"]; + } + if (isset($o['groupIds'])) { + $this->groupIds = $o["groupIds"]; + } + if (isset($o['name'])) { + $this->name = $o["name"]; + } + if (isset($o['description'])) { + $this->description = $o["description"]; + } + if (isset($o['startingPersonId'])) { + $this->startingPersonId = $o["startingPersonId"]; + } + if (isset($o['hidden'])) { + $this->hidden = $o["hidden"]; + } + if (isset($o['private'])) { + $this->isPrivate = $o["private"]; + } + if (isset($o['collectionId'])) { + $this->collectionId = $o["collectionId"]; + } + if (isset($o['ownerAccess'])) { + $this->ownerAccess = $o["ownerAccess"]; + } + if (isset($o['groupAccess'])) { + $this->groupAccess = $o["groupAccess"]; + } + if (isset($o['allAccess'])) { + $this->allAccess = $o["allAccess"]; + } + } + + /** + * Initializes this Tree from an XML reader. + * + * @param \XMLReader $xml The reader to use to initialize this object. + */ + public function initFromReader($xml) + { + $empty = $xml->isEmptyElement; + + if ($xml->hasAttributes) { + $moreAttributes = $xml->moveToFirstAttribute(); + while ($moreAttributes) { + if (!$this->setKnownAttribute($xml)) { + //skip unknown attributes... + } + $moreAttributes = $xml->moveToNextAttribute(); + } + } + + if (!$empty) { + $xml->read(); + while ($xml->nodeType != \XMLReader::END_ELEMENT) { + if ($xml->nodeType != \XMLReader::ELEMENT) { + //no-op: skip any insignificant whitespace, comments, etc. + } + else if (!$this->setKnownChildElement($xml)) { + $n = $xml->localName; + $ns = $xml->namespaceURI; + //skip the unknown element + while ($xml->nodeType != \XMLReader::END_ELEMENT && $xml->localName != $n && $xml->namespaceURI != $ns) { + $xml->read(); + } + } + $xml->read(); //advance the reader. + } + } + } + + /** + * Sets a known child element of Tree from an XML reader. + * + * @param \XMLReader $xml The reader. + * + * @return bool Whether a child element was set. + */ + protected function setKnownChildElement($xml) + { + $happened = false; + if (($xml->localName == 'groupId') && ($xml->namespaceURI == 'http://familysearch.org/v1/')) { + if ($this->groupIds === null) { + $this->groupIds = array(); + } + $this->groupIds[] = $xml->readString(); + $happened = true; + } + return $happened; + } + + /** + * Sets a known attribute of Tree from an XML reader. + * + * @param \XMLReader $xml The reader. + * + * @return bool Whether an attribute was set. + */ + protected function setKnownAttribute($xml) + { + return false; + } + + /** + * Writes this Tree to an XML writer. + * + * @param \XMLWriter $writer The XML writer. + * @param bool $includeNamespaces Whether to write out the namespaces in the element. + */ + public function toXml($writer, $includeNamespaces = true) + { + $writer->startElementNS('fs', 'tree', null); + if ($includeNamespaces) { + $writer->writeAttributeNs('xmlns', 'fs', null, 'http://familysearch.org/v1/'); + } + $this->writeXmlContents($writer); + $writer->endElement(); + } + + /** + * Writes the contents of this Tree to an XML writer. + * The startElement is expected to be already provided. + * + * @param \XMLWriter $writer The XML writer. + */ + public function writeXmlContents($writer) + { + if ($this->id) { + $writer->writeElement('id', $this->id); + } + if ($this->groupIds) { + foreach ($this->groupIds as $groupId) { + $writer->writeElement('groupId', $groupId); + } + } + if ($this->name) { + $writer->writeElement('name', $this->name); + } + if ($this->description) { + $writer->writeElement('description', $this->description); + } + if ($this->startingPersonId) { + $writer->writeElement('startingPersonId', $this->startingPersonId); + } + if ($this->hidden !== null) { + $writer->writeElement('hidden', $this->hidden ? 'true' : 'false'); + } + if ($this->isPrivate !== null) { + $writer->writeElement('private', $this->isPrivate ? 'true' : 'false'); + } + if ($this->collectionId) { + $writer->writeElement('collectionId', $this->collectionId); + } + if ($this->ownerAccess) { + $writer->writeElement('ownerAccess', $this->ownerAccess); + } + if ($this->groupAccess) { + $writer->writeElement('groupAccess', $this->groupAccess); + } + if ($this->allAccess) { + $writer->writeElement('allAccess', $this->allAccess); + } + } +} diff --git a/src/Extensions/FamilySearch/Platform/Tree/TreePersonReference.php b/src/Extensions/FamilySearch/Platform/Tree/TreePersonReference.php new file mode 100644 index 00000000..6eea9de2 --- /dev/null +++ b/src/Extensions/FamilySearch/Platform/Tree/TreePersonReference.php @@ -0,0 +1,286 @@ +treePerson; + } + + /** + * Sets the reference to the person in the tree. + * + * @param ResourceReference $treePerson + */ + public function setTreePerson($treePerson) + { + $this->treePerson = $treePerson; + } + + /** + * Gets the reference to the tree containing the person. + * + * @return ResourceReference + */ + public function getTree() + { + return $this->tree; + } + + /** + * Sets the reference to the tree containing the person. + * + * @param ResourceReference $tree + */ + public function setTree($tree) + { + $this->tree = $tree; + } + + /** + * Gets the attribution metadata for this tree person reference. + * + * @return Attribution + */ + public function getAttribution() + { + return $this->attribution; + } + + /** + * Sets the attribution metadata for this tree person reference. + * + * @param Attribution $attribution + */ + public function setAttribution($attribution) + { + $this->attribution = $attribution; + } + + /** + * Returns the associative array for this TreePersonReference. + * + * @return array + */ + public function toArray() + { + $a = parent::toArray(); + if ($this->treePerson) { + $a["treePerson"] = $this->treePerson->toArray(); + } + if ($this->tree) { + $a["tree"] = $this->tree->toArray(); + } + if ($this->attribution) { + $a["attribution"] = $this->attribution->toArray(); + } + return $a; + } + + /** + * Returns the JSON string for this TreePersonReference. + * + * @return string + */ + public function toJson() + { + return json_encode($this->toArray()); + } + + /** + * Initializes this TreePersonReference from an associative array. + * + * @param array $o + */ + public function initFromArray($o) + { + parent::initFromArray($o); + if (isset($o['treePerson'])) { + $this->treePerson = new ResourceReference($o["treePerson"]); + } + if (isset($o['tree'])) { + $this->tree = new ResourceReference($o["tree"]); + } + if (isset($o['attribution'])) { + $this->attribution = new Attribution($o["attribution"]); + } + } + + /** + * Initializes this TreePersonReference from an XML reader. + * + * @param \XMLReader $xml The reader to use to initialize this object. + */ + public function initFromReader($xml) + { + parent::initFromReader($xml); + } + + /** + * Sets a known child element of TreePersonReference from an XML reader. + * + * @param \XMLReader $xml The reader. + * + * @return bool Whether a child element was set. + */ + protected function setKnownChildElement($xml) + { + $happened = parent::setKnownChildElement($xml); + if ($happened) { + return true; + } + + if (($xml->localName == 'treePerson') && ($xml->namespaceURI == 'http://familysearch.org/v1/')) { + $child = new ResourceReference($xml); + $this->treePerson = $child; + return true; + } + else if (($xml->localName == 'tree') && ($xml->namespaceURI == 'http://familysearch.org/v1/')) { + $child = new ResourceReference($xml); + $this->tree = $child; + return true; + } + else if (($xml->localName == 'attribution') && ($xml->namespaceURI == 'http://gedcomx.org/v1/')) { + $child = new Attribution($xml); + $this->attribution = $child; + return true; + } + return false; + } + + /** + * Sets a known attribute of TreePersonReference from an XML reader. + * + * @param \XMLReader $xml The reader. + * + * @return bool Whether an attribute was set. + */ + protected function setKnownAttribute($xml) + { + return parent::setKnownAttribute($xml); + } + + /** + * Writes this TreePersonReference to an XML writer. + * + * @param \XMLWriter $writer The XML writer. + * @param bool $includeNamespaces Whether to write out the namespaces in the element. + */ + public function toXml($writer, $includeNamespaces = true) + { + $writer->startElementNS('fs', 'tree-person-reference', null); + if ($includeNamespaces) { + $writer->writeAttributeNs('xmlns', 'fs', null, 'http://familysearch.org/v1/'); + $writer->writeAttributeNs('xmlns', 'gx', null, 'http://gedcomx.org/v1/'); + } + $this->writeXmlContents($writer); + $writer->endElement(); + } + + /** + * Writes the contents of this TreePersonReference to an XML writer. + * The startElement is expected to be already provided. + * + * @param \XMLWriter $writer The XML writer. + */ + public function writeXmlContents($writer) + { + parent::writeXmlContents($writer); + if ($this->treePerson) { + $writer->startElementNS('fs', 'treePerson', null); + $this->treePerson->writeXmlContents($writer); + $writer->endElement(); + } + if ($this->tree) { + $writer->startElementNS('fs', 'tree', null); + $this->tree->writeXmlContents($writer); + $writer->endElement(); + } + if ($this->attribution) { + $writer->startElementNS('gx', 'attribution', null); + $this->attribution->writeXmlContents($writer); + $writer->endElement(); + } + } +} diff --git a/tests/fixtures/familysearch-api-examples.json b/tests/fixtures/familysearch-api-examples.json new file mode 100644 index 00000000..8c3fec4b --- /dev/null +++ b/tests/fixtures/familysearch-api-examples.json @@ -0,0 +1,168 @@ +{ + "description": "Realistic FamilySearch API response examples for testing new extension classes", + + "tree": { + "id": "TREE-12345", + "groupIds": ["GROUP-001", "GROUP-002", "GROUP-003"], + "name": "Smith Family Tree", + "description": "Collaborative research on the Smith family lineage from England to America", + "startingPersonId": "KWQS-BBQ", + "hidden": false, + "private": true, + "collectionId": "TREE-COLLECTION-1", + "ownerAccess": "http://familysearch.org/v1/AnyApps", + "groupAccess": "http://familysearch.org/v1/CompanyApps", + "allAccess": "http://familysearch.org/v1/None" + }, + + "group": { + "id": "GROUP-001", + "name": "Smith Family Researchers", + "description": "Group dedicated to researching the Smith family line", + "codeOfConduct": "Be respectful, collaborative, and cite your sources", + "treeIds": ["TREE-12345", "TREE-67890"], + "members": [ + { + "groupId": "GROUP-001", + "cisId": "cis-user-12345", + "contactName": "John Smith", + "status": "active" + }, + { + "groupId": "GROUP-001", + "cisId": "cis-user-67890", + "contactName": "Jane Doe", + "status": "active" + }, + { + "groupId": "GROUP-001", + "cisId": "cis-user-11111", + "contactName": "Robert Johnson", + "status": "invited" + } + ] + }, + + "personInfo": { + "canUserEdit": true, + "visibleToAll": true, + "visibleToAllWhenUsingFamilySearchApps": true, + "treeId": "TREE-12345" + }, + + "searchInfo": { + "totalHits": 1543, + "closeHits": 127 + }, + + "nameFormInfo": { + "order": "http://familysearch.org/v1/Eurotypic" + }, + + "nameFormInfoSinotypic": { + "order": "http://familysearch.org/v1/Sinotypic" + }, + + "additionalAttribution": { + "contributor": { + "resource": "https://familysearch.org/platform/users/cis-user-12345", + "resourceId": "cis-user-12345" + }, + "modified": 1625097600000, + "changeMessage": "Added birth date from 1850 census record" + }, + + "treePersonReference": { + "treePerson": { + "resource": "https://familysearch.org/platform/tree/persons/KWQS-BBQ", + "resourceId": "KWQS-BBQ" + }, + "tree": { + "resource": "https://familysearch.org/platform/tree/trees/TREE-12345", + "resourceId": "TREE-12345" + }, + "attribution": { + "contributor": { + "resource": "https://familysearch.org/platform/users/cis-user-12345", + "resourceId": "cis-user-12345" + }, + "modified": 1625097600000, + "changeMessage": "Linked from alternate tree for comparison" + } + }, + + "changeTypes": { + "personChanges": [ + "http://familysearch.org/v1/CreatePerson", + "http://familysearch.org/v1/UpdatePerson", + "http://familysearch.org/v1/DeletePerson", + "http://familysearch.org/v1/RestorePerson", + "http://familysearch.org/v1/MergePerson", + "http://familysearch.org/v1/UnmergePerson" + ], + "factChanges": [ + "http://familysearch.org/v1/AddBirth", + "http://familysearch.org/v1/UpdateBirth", + "http://familysearch.org/v1/DeleteBirth", + "http://familysearch.org/v1/AddDeath", + "http://familysearch.org/v1/UpdateDeath", + "http://familysearch.org/v1/DeleteDeath" + ], + "relationshipChanges": [ + "http://familysearch.org/v1/CreateCoupleRelationship", + "http://familysearch.org/v1/DeleteCoupleRelationship", + "http://familysearch.org/v1/CreateChildAndParentsRelationship", + "http://familysearch.org/v1/DeleteChildAndParentsRelationship" + ] + }, + + "familySearchFactTypes": [ + "http://familysearch.org/v1/Affiliation", + "http://familysearch.org/v1/LifeSketch", + "http://familysearch.org/v1/TribeName", + "http://familysearch.org/v1/TitleOfNobility" + ], + + "familySearchIdentifierTypes": [ + "http://familysearch.org/v1/ChildAndParentsRelationship", + "http://familysearch.org/v1/MemoryPerson", + "http://familysearch.org/v1/FamilyTreePerson" + ], + + "relationshipRoles": [ + "http://familysearch.org/v1/Parent1", + "http://familysearch.org/v1/Parent2", + "http://familysearch.org/v1/Child", + "http://familysearch.org/v1/Spouse1", + "http://familysearch.org/v1/Spouse2" + ], + + "thirdPartyAccessLevels": { + "mostPermissive": "http://familysearch.org/v1/AnyApps", + "restricted": "http://familysearch.org/v1/CompanyApps", + "blocked": "http://familysearch.org/v1/None" + }, + + "matchCollections": [ + "https://familysearch.org/platform/collections/tree", + "https://familysearch.org/platform/collections/records", + "https://familysearch.org/platform/collections/trees", + "https://familysearch.org/platform/collections/user_trees" + ], + + "searchCollections": { + "tree": { + "uri": "https://familysearch.org/platform/collections/tree", + "id": "0" + }, + "userTrees": { + "uri": "https://familysearch.org/platform/collections/user_trees", + "id": "10" + } + }, + + "sourceReferenceTagTypes": [ + "http://gedcomx.org/Name", + "http://gedcomx.org/Gender" + ] +} diff --git a/tests/unit/NewFamilySearchExtensionsTests.php b/tests/unit/NewFamilySearchExtensionsTests.php new file mode 100644 index 00000000..991a1662 --- /dev/null +++ b/tests/unit/NewFamilySearchExtensionsTests.php @@ -0,0 +1,779 @@ +assertEquals('http://familysearch.org/v1/CreatePerson', ChangeType::CREATE_PERSON); + $this->assertEquals('http://familysearch.org/v1/DeletePerson', ChangeType::DELETE_PERSON); + $this->assertEquals('http://familysearch.org/v1/AddBirth', ChangeType::ADD_BIRTH); + $this->assertEquals('http://familysearch.org/v1/MergePerson', ChangeType::MERGE_PERSON); + $this->assertEquals('http://familysearch.org/v1/CreateCoupleRelationship', ChangeType::CREATE_COUPLE_RELATIONSHIP); + $this->assertEquals('http://familysearch.org/v1/CreateChildAndParentsRelationship', ChangeType::CREATE_CHILD_AND_PARENTS_RELATIONSHIP); + } + + /** + * Test FamilySearchFactType enum constants + */ + public function testFamilySearchFactTypeConstants() + { + $this->assertEquals('http://familysearch.org/v1/Affiliation', FamilySearchFactType::AFFILIATION); + $this->assertEquals('http://familysearch.org/v1/LifeSketch', FamilySearchFactType::LIFE_SKETCH); + $this->assertEquals('http://familysearch.org/v1/TribeName', FamilySearchFactType::TRIBE_NAME); + $this->assertEquals('http://familysearch.org/v1/TitleOfNobility', FamilySearchFactType::TITLE_OF_NOBILITY); + } + + /** + * Test FamilySearchIdentifierType enum constants + */ + public function testFamilySearchIdentifierTypeConstants() + { + $this->assertEquals( + 'http://familysearch.org/v1/ChildAndParentsRelationship', + FamilySearchIdentifierType::CHILD_AND_PARENTS_RELATIONSHIP + ); + $this->assertEquals( + 'http://familysearch.org/v1/MemoryPerson', + FamilySearchIdentifierType::MEMORY_PERSON + ); + $this->assertEquals( + 'http://familysearch.org/v1/FamilyTreePerson', + FamilySearchIdentifierType::FAMILY_TREE_PERSON + ); + } + + /** + * Test FamilyTreeFactQualifierType enum constants + */ + public function testFamilyTreeFactQualifierTypeConstants() + { + $this->assertEquals('http://familysearch.org/v1/Event', FamilyTreeFactQualifierType::EVENT); + } + + /** + * Test SourceReferenceTagType enum constants + */ + public function testSourceReferenceTagTypeConstants() + { + $this->assertEquals('http://gedcomx.org/Name', SourceReferenceTagType::NAME); + $this->assertEquals('http://gedcomx.org/Gender', SourceReferenceTagType::GENDER); + } + + /** + * Test RelationshipRole enum constants + */ + public function testRelationshipRoleConstants() + { + $this->assertEquals('http://familysearch.org/v1/Parent1', RelationshipRole::PARENT1); + $this->assertEquals('http://familysearch.org/v1/Parent2', RelationshipRole::PARENT2); + $this->assertEquals('http://familysearch.org/v1/Child', RelationshipRole::CHILD); + $this->assertEquals('http://familysearch.org/v1/Spouse1', RelationshipRole::SPOUSE1); + $this->assertEquals('http://familysearch.org/v1/Spouse2', RelationshipRole::SPOUSE2); + } + + /** + * Test ThirdPartyAccess enum constants + */ + public function testThirdPartyAccessConstants() + { + $this->assertEquals('http://familysearch.org/v1/AnyApps', ThirdPartyAccess::ANY_APPS); + $this->assertEquals('http://familysearch.org/v1/CompanyApps', ThirdPartyAccess::COMPANY_APPS); + $this->assertEquals('http://familysearch.org/v1/None', ThirdPartyAccess::NONE); + $this->assertEquals('http://familysearch.org/v1/OTHER', ThirdPartyAccess::OTHER); + } + + /** + * Test MatchCollection enum constants + */ + public function testMatchCollectionConstants() + { + $this->assertEquals('https://familysearch.org/platform/collections/tree', MatchCollection::TREE); + $this->assertEquals('https://familysearch.org/platform/collections/records', MatchCollection::RECORDS); + $this->assertEquals('https://familysearch.org/platform/collections/trees', MatchCollection::LLS); + $this->assertEquals('https://familysearch.org/platform/collections/user_trees', MatchCollection::USER_TREES); + } + + /** + * Test SearchCollection enum constants and getId method + */ + public function testSearchCollectionConstants() + { + $this->assertEquals('https://familysearch.org/platform/collections/tree', SearchCollection::TREE); + $this->assertEquals('0', SearchCollection::TREE_ID); + $this->assertEquals('https://familysearch.org/platform/collections/user_trees', SearchCollection::USER_TREES); + $this->assertEquals('10', SearchCollection::USER_TREES_ID); + + // Test static getId method + $this->assertEquals('0', SearchCollection::getId(SearchCollection::TREE)); + $this->assertEquals('10', SearchCollection::getId(SearchCollection::USER_TREES)); + } + + /** + * Test NameFormOrder enum constants + */ + public function testNameFormOrderConstants() + { + $this->assertEquals('http://familysearch.org/v1/Eurotypic', NameFormOrder::EUROTYPIC); + $this->assertEquals('http://familysearch.org/v1/Sinotypic', NameFormOrder::SINOTYPIC); + } + + // ========== DATA CLASS TESTS ========== + + /** + * Test AdditionalAttribution construction + */ + public function testAdditionalAttributionConstruction() + { + $additionalAttr = new AdditionalAttribution(); + + $contributor = new ResourceReference(); + $contributor->setResource('https://familysearch.org/platform/users/USER123'); + $additionalAttr->setContributor($contributor); + + $additionalAttr->setModified(1625097600000); + $additionalAttr->setChangeMessage('Added birth date from parish records'); + + $this->assertNotNull($additionalAttr->getContributor()); + $this->assertEquals('https://familysearch.org/platform/users/USER123', $additionalAttr->getContributor()->getResource()); + $this->assertEquals(1625097600000, $additionalAttr->getModified()); + $this->assertEquals('Added birth date from parish records', $additionalAttr->getChangeMessage()); + } + + /** + * Test AdditionalAttribution from array + */ + public function testAdditionalAttributionFromArray() + { + $additionalAttr = new AdditionalAttribution([ + 'contributor' => [ + 'resource' => 'https://familysearch.org/platform/users/USER456' + ], + 'modified' => 1625097600000, + 'changeMessage' => 'Verified with census record' + ]); + + $this->assertNotNull($additionalAttr->getContributor()); + $this->assertEquals(1625097600000, $additionalAttr->getModified()); + $this->assertEquals('Verified with census record', $additionalAttr->getChangeMessage()); + } + + /** + * Test AdditionalAttribution JSON round-trip + */ + public function testAdditionalAttributionJsonRoundTrip() + { + $additionalAttr = new AdditionalAttribution([ + 'contributor' => ['resource' => '#USER1'], + 'modified' => 1625097600000, + 'changeMessage' => 'Test change' + ]); + + $json = $additionalAttr->toJson(); + $this->assertStringContainsString('USER1', $json); + $this->assertStringContainsString('Test change', $json); + + $decoded = json_decode($json, true); + $additionalAttr2 = new AdditionalAttribution($decoded); + $this->assertEquals('Test change', $additionalAttr2->getChangeMessage()); + } + + /** + * Test NameFormInfo construction + */ + public function testNameFormInfoConstruction() + { + $nameInfo = new NameFormInfo(); + $nameInfo->setOrder(NameFormOrder::SINOTYPIC); + + $this->assertEquals(NameFormOrder::SINOTYPIC, $nameInfo->getOrder()); + } + + /** + * Test NameFormInfo from array + */ + public function testNameFormInfoFromArray() + { + $nameInfo = new NameFormInfo([ + 'order' => NameFormOrder::EUROTYPIC + ]); + + $this->assertEquals(NameFormOrder::EUROTYPIC, $nameInfo->getOrder()); + } + + /** + * Test NameFormInfo JSON round-trip + */ + public function testNameFormInfoJsonRoundTrip() + { + $nameInfo = new NameFormInfo(['order' => NameFormOrder::SINOTYPIC]); + + $json = $nameInfo->toJson(); + $this->assertStringContainsString('Sinotypic', $json); + + $decoded = json_decode($json, true); + $nameInfo2 = new NameFormInfo($decoded); + $this->assertEquals(NameFormOrder::SINOTYPIC, $nameInfo2->getOrder()); + } + + /** + * Test SearchInfo construction + */ + public function testSearchInfoConstruction() + { + $searchInfo = new SearchInfo(); + $searchInfo->setTotalHits(150); + $searchInfo->setCloseHits(25); + + $this->assertEquals(150, $searchInfo->getTotalHits()); + $this->assertEquals(25, $searchInfo->getCloseHits()); + } + + /** + * Test SearchInfo from array + */ + public function testSearchInfoFromArray() + { + $searchInfo = new SearchInfo([ + 'totalHits' => 200, + 'closeHits' => 30 + ]); + + $this->assertEquals(200, $searchInfo->getTotalHits()); + $this->assertEquals(30, $searchInfo->getCloseHits()); + } + + /** + * Test SearchInfo JSON round-trip + */ + public function testSearchInfoJsonRoundTrip() + { + $searchInfo = new SearchInfo(['totalHits' => 100, 'closeHits' => 15]); + + $json = $searchInfo->toJson(); + $this->assertStringContainsString('100', $json); + $this->assertStringContainsString('15', $json); + + $decoded = json_decode($json, true); + $searchInfo2 = new SearchInfo($decoded); + $this->assertEquals(100, $searchInfo2->getTotalHits()); + $this->assertEquals(15, $searchInfo2->getCloseHits()); + } + + /** + * Test PersonInfo construction + */ + public function testPersonInfoConstruction() + { + $personInfo = new PersonInfo(); + $personInfo->setCanUserEdit(true); + $personInfo->setVisibleToAll(false); + $personInfo->setVisibleToAllWhenUsingFamilySearchApps(true); + $personInfo->setTreeId('TREE123'); + + $this->assertTrue($personInfo->getCanUserEdit()); + $this->assertFalse($personInfo->getVisibleToAll()); + $this->assertTrue($personInfo->getVisibleToAllWhenUsingFamilySearchApps()); + $this->assertEquals('TREE123', $personInfo->getTreeId()); + } + + /** + * Test PersonInfo from array + */ + public function testPersonInfoFromArray() + { + $personInfo = new PersonInfo([ + 'canUserEdit' => true, + 'visibleToAll' => true, + 'treeId' => 'TREE456' + ]); + + $this->assertTrue($personInfo->getCanUserEdit()); + $this->assertTrue($personInfo->getVisibleToAll()); + $this->assertEquals('TREE456', $personInfo->getTreeId()); + } + + /** + * Test PersonInfo JSON round-trip + */ + public function testPersonInfoJsonRoundTrip() + { + $personInfo = new PersonInfo([ + 'canUserEdit' => true, + 'visibleToAll' => false, + 'treeId' => 'TREE789' + ]); + + $json = $personInfo->toJson(); + $this->assertStringContainsString('TREE789', $json); + + $decoded = json_decode($json, true); + $personInfo2 = new PersonInfo($decoded); + $this->assertTrue($personInfo2->getCanUserEdit()); + $this->assertEquals('TREE789', $personInfo2->getTreeId()); + } + + /** + * Test GroupMember construction + */ + public function testGroupMemberConstruction() + { + $member = new GroupMember(); + $member->setGroupId('GROUP1'); + $member->setCisId('USER001'); + $member->setContactName('John Smith'); + $member->setStatus('active'); + + $this->assertEquals('GROUP1', $member->getGroupId()); + $this->assertEquals('USER001', $member->getCisId()); + $this->assertEquals('John Smith', $member->getContactName()); + $this->assertEquals('active', $member->getStatus()); + } + + /** + * Test GroupMember from array + */ + public function testGroupMemberFromArray() + { + $member = new GroupMember([ + 'groupId' => 'GROUP2', + 'cisId' => 'USER002', + 'contactName' => 'Jane Doe', + 'status' => 'invited' + ]); + + $this->assertEquals('GROUP2', $member->getGroupId()); + $this->assertEquals('USER002', $member->getCisId()); + $this->assertEquals('Jane Doe', $member->getContactName()); + $this->assertEquals('invited', $member->getStatus()); + } + + /** + * Test GroupMember JSON round-trip + */ + public function testGroupMemberJsonRoundTrip() + { + $member = new GroupMember([ + 'cisId' => 'USER999', + 'contactName' => 'Test User', + 'status' => 'active' + ]); + + $json = $member->toJson(); + $this->assertStringContainsString('USER999', $json); + $this->assertStringContainsString('Test User', $json); + + $decoded = json_decode($json, true); + $member2 = new GroupMember($decoded); + $this->assertEquals('USER999', $member2->getCisId()); + $this->assertEquals('Test User', $member2->getContactName()); + } + + // ========== CONTAINER/COLLECTION TESTS ========== + + /** + * Test Group construction with arrays + */ + public function testGroupConstruction() + { + $group = new Group(); + $group->setId('GROUP123'); + $group->setName('Smith Family Research'); + $group->setDescription('Collaborative research on Smith family'); + $group->setTreeIds(['TREE1', 'TREE2', 'TREE3']); + + $member1 = new GroupMember(); + $member1->setCisId('USER001'); + $member1->setContactName('John Smith'); + $group->addMember($member1); + + $member2 = new GroupMember(); + $member2->setCisId('USER002'); + $member2->setContactName('Jane Doe'); + $group->addMember($member2); + + $this->assertEquals('GROUP123', $group->getId()); + $this->assertEquals('Smith Family Research', $group->getName()); + $this->assertCount(3, $group->getTreeIds()); + $this->assertCount(2, $group->getMembers()); + $this->assertEquals('USER001', $group->getMembers()[0]->getCisId()); + } + + /** + * Test Group from array + */ + public function testGroupFromArray() + { + $group = new Group([ + 'id' => 'GROUP456', + 'name' => 'Research Group', + 'treeIds' => ['TREE1', 'TREE2'], + 'members' => [ + ['cisId' => 'USER1', 'contactName' => 'User One'], + ['cisId' => 'USER2', 'contactName' => 'User Two'] + ] + ]); + + $this->assertEquals('GROUP456', $group->getId()); + $this->assertCount(2, $group->getTreeIds()); + $this->assertCount(2, $group->getMembers()); + $this->assertEquals('USER1', $group->getMembers()[0]->getCisId()); + } + + /** + * Test Group JSON round-trip + */ + public function testGroupJsonRoundTrip() + { + $group = new Group([ + 'id' => 'GROUP789', + 'name' => 'Test Group', + 'treeIds' => ['TREE1'], + 'members' => [ + ['cisId' => 'USER123', 'contactName' => 'Test User'] + ] + ]); + + $json = $group->toJson(); + $this->assertStringContainsString('GROUP789', $json); + $this->assertStringContainsString('Test Group', $json); + + $decoded = json_decode($json, true); + $group2 = new Group($decoded); + $this->assertEquals('GROUP789', $group2->getId()); + $this->assertCount(1, $group2->getMembers()); + } + + /** + * Test Tree construction with access controls + */ + public function testTreeConstruction() + { + $tree = new Tree(); + $tree->setId('TREE123'); + $tree->setName('Smith Family Tree'); + $tree->setDescription('Main family tree'); + $tree->setGroupIds(['GROUP1', 'GROUP2']); + $tree->setOwnerAccess(ThirdPartyAccess::ANY_APPS); + $tree->setGroupAccess(ThirdPartyAccess::COMPANY_APPS); + $tree->setAllAccess(ThirdPartyAccess::NONE); + $tree->setHidden(false); + $tree->setPrivate(true); + + $this->assertEquals('TREE123', $tree->getId()); + $this->assertEquals('Smith Family Tree', $tree->getName()); + $this->assertCount(2, $tree->getGroupIds()); + $this->assertEquals(ThirdPartyAccess::ANY_APPS, $tree->getOwnerAccess()); + $this->assertFalse($tree->getHidden()); + $this->assertTrue($tree->getPrivate()); + } + + /** + * Test Tree from array + */ + public function testTreeFromArray() + { + $tree = new Tree([ + 'id' => 'TREE456', + 'name' => 'Test Tree', + 'groupIds' => ['GROUP1'], + 'ownerAccess' => ThirdPartyAccess::ANY_APPS, + 'hidden' => false, + 'private' => true + ]); + + $this->assertEquals('TREE456', $tree->getId()); + $this->assertEquals('Test Tree', $tree->getName()); + $this->assertCount(1, $tree->getGroupIds()); + $this->assertEquals(ThirdPartyAccess::ANY_APPS, $tree->getOwnerAccess()); + } + + /** + * Test Tree JSON round-trip + */ + public function testTreeJsonRoundTrip() + { + $tree = new Tree([ + 'id' => 'TREE789', + 'name' => 'Round Trip Tree', + 'groupIds' => ['G1', 'G2'], + 'ownerAccess' => ThirdPartyAccess::COMPANY_APPS + ]); + + $json = $tree->toJson(); + $this->assertStringContainsString('TREE789', $json); + $this->assertStringContainsString('Round Trip Tree', $json); + + $decoded = json_decode($json, true); + $tree2 = new Tree($decoded); + $this->assertEquals('TREE789', $tree2->getId()); + $this->assertCount(2, $tree2->getGroupIds()); + } + + /** + * Test TreePersonReference construction (inheritance test) + */ + public function testTreePersonReferenceConstruction() + { + $ref = new TreePersonReference(); + + $personRef = new ResourceReference(); + $personRef->setResource('https://familysearch.org/platform/tree/persons/PERSON123'); + $ref->setTreePerson($personRef); + + $treeRef = new ResourceReference(); + $treeRef->setResource('https://familysearch.org/platform/tree/trees/TREE456'); + $ref->setTree($treeRef); + + $attribution = new Attribution(); + $contributor = new ResourceReference(); + $contributor->setResource('#USER1'); + $attribution->setContributor($contributor); + $ref->setAttribution($attribution); + + $this->assertNotNull($ref->getTreePerson()); + $this->assertNotNull($ref->getTree()); + $this->assertNotNull($ref->getAttribution()); + $this->assertEquals('https://familysearch.org/platform/tree/persons/PERSON123', $ref->getTreePerson()->getResource()); + } + + /** + * Test TreePersonReference from array + */ + public function testTreePersonReferenceFromArray() + { + $ref = new TreePersonReference([ + 'treePerson' => ['resource' => '#PERSON1'], + 'tree' => ['resource' => '#TREE1'], + 'attribution' => [ + 'contributor' => ['resource' => '#USER1'] + ] + ]); + + $this->assertNotNull($ref->getTreePerson()); + $this->assertNotNull($ref->getTree()); + $this->assertNotNull($ref->getAttribution()); + } + + /** + * Test TreePersonReference JSON round-trip + */ + public function testTreePersonReferenceJsonRoundTrip() + { + $ref = new TreePersonReference([ + 'treePerson' => ['resource' => '#PERSON999'], + 'tree' => ['resource' => '#TREE999'] + ]); + + $json = $ref->toJson(); + $this->assertStringContainsString('PERSON999', $json); + $this->assertStringContainsString('TREE999', $json); + + $decoded = json_decode($json, true); + $ref2 = new TreePersonReference($decoded); + $this->assertNotNull($ref2->getTreePerson()); + $this->assertNotNull($ref2->getTree()); + } + + // ========== NULL/EMPTY TESTS ========== + + /** + * Test classes handle null values gracefully + */ + public function testNullValues() + { + // Test empty construction + $searchInfo = new SearchInfo(); + $this->assertNull($searchInfo->getTotalHits()); + $this->assertNull($searchInfo->getCloseHits()); + + $personInfo = new PersonInfo(); + // PersonInfo has default values, not null + $this->assertFalse($personInfo->getCanUserEdit()); + $this->assertTrue($personInfo->getVisibleToAll()); + $this->assertNull($personInfo->getTreeId()); + + $group = new Group(); + $this->assertNull($group->getId()); + $this->assertNull($group->getMembers()); + + $tree = new Tree(); + $this->assertNull($tree->getId()); + $this->assertNull($tree->getGroupIds()); + } + + /** + * Test empty arrays + */ + public function testEmptyArrays() + { + $group = new Group(); + $group->setTreeIds([]); + $group->setMembers([]); + + $this->assertIsArray($group->getTreeIds()); + $this->assertIsArray($group->getMembers()); + $this->assertCount(0, $group->getTreeIds()); + $this->assertCount(0, $group->getMembers()); + } + + // ========== COMPLEX NESTED OBJECT TESTS ========== + + /** + * Test complex nested group structure + */ + public function testComplexGroupStructure() + { + $group = new Group([ + 'id' => 'COMPLEX-GROUP', + 'name' => 'Complex Research Group', + 'description' => 'A group with multiple members and trees', + 'codeOfConduct' => 'Be respectful and collaborative', + 'treeIds' => ['TREE1', 'TREE2', 'TREE3', 'TREE4'], + 'members' => [ + [ + 'groupId' => 'COMPLEX-GROUP', + 'cisId' => 'USER001', + 'contactName' => 'Alice Johnson', + 'status' => 'active' + ], + [ + 'groupId' => 'COMPLEX-GROUP', + 'cisId' => 'USER002', + 'contactName' => 'Bob Smith', + 'status' => 'active' + ], + [ + 'groupId' => 'COMPLEX-GROUP', + 'cisId' => 'USER003', + 'contactName' => 'Carol Williams', + 'status' => 'invited' + ] + ] + ]); + + // Verify structure + $this->assertEquals('COMPLEX-GROUP', $group->getId()); + $this->assertCount(4, $group->getTreeIds()); + $this->assertCount(3, $group->getMembers()); + $this->assertEquals('Alice Johnson', $group->getMembers()[0]->getContactName()); + $this->assertEquals('invited', $group->getMembers()[2]->getStatus()); + + // Test JSON round-trip + $json = $group->toJson(); + $decoded = json_decode($json, true); + $group2 = new Group($decoded); + + $this->assertEquals('COMPLEX-GROUP', $group2->getId()); + $this->assertCount(4, $group2->getTreeIds()); + $this->assertCount(3, $group2->getMembers()); + } + + /** + * Test complex tree with all properties + */ + public function testComplexTreeStructure() + { + $tree = new Tree([ + 'id' => 'COMPLEX-TREE', + 'groupIds' => ['GROUP1', 'GROUP2', 'GROUP3'], + 'name' => 'Complex Family Tree', + 'description' => 'A comprehensive family tree', + 'startingPersonId' => 'PERSON-ROOT', + 'hidden' => false, + 'private' => true, + 'collectionId' => 'COLLECTION-1', + 'ownerAccess' => ThirdPartyAccess::ANY_APPS, + 'groupAccess' => ThirdPartyAccess::COMPANY_APPS, + 'allAccess' => ThirdPartyAccess::NONE + ]); + + $this->assertEquals('COMPLEX-TREE', $tree->getId()); + $this->assertCount(3, $tree->getGroupIds()); + $this->assertEquals('PERSON-ROOT', $tree->getStartingPersonId()); + $this->assertFalse($tree->getHidden()); + $this->assertTrue($tree->getPrivate()); + $this->assertEquals(ThirdPartyAccess::ANY_APPS, $tree->getOwnerAccess()); + $this->assertEquals(ThirdPartyAccess::COMPANY_APPS, $tree->getGroupAccess()); + $this->assertEquals(ThirdPartyAccess::NONE, $tree->getAllAccess()); + + // Test JSON round-trip + $json = $tree->toJson(); + $decoded = json_decode($json, true); + $tree2 = new Tree($decoded); + + $this->assertEquals('COMPLEX-TREE', $tree2->getId()); + $this->assertCount(3, $tree2->getGroupIds()); + } + + /** + * Test TreePersonReference with full attribution + */ + public function testComplexTreePersonReference() + { + $ref = new TreePersonReference([ + 'treePerson' => [ + 'resource' => 'https://familysearch.org/platform/tree/persons/PERSON-123', + 'resourceId' => 'PERSON-123' + ], + 'tree' => [ + 'resource' => 'https://familysearch.org/platform/tree/trees/TREE-456', + 'resourceId' => 'TREE-456' + ], + 'attribution' => [ + 'contributor' => [ + 'resource' => 'https://familysearch.org/platform/users/USER-789', + 'resourceId' => 'USER-789' + ], + 'modified' => 1625097600000, + 'changeMessage' => 'Linked from alternate tree' + ] + ]); + + $this->assertNotNull($ref->getTreePerson()); + $this->assertNotNull($ref->getTree()); + $this->assertNotNull($ref->getAttribution()); + $this->assertEquals(1625097600000, $ref->getAttribution()->getModified()); + $this->assertEquals('Linked from alternate tree', $ref->getAttribution()->getChangeMessage()); + + // Test JSON round-trip + $json = $ref->toJson(); + $decoded = json_decode($json, true); + $ref2 = new TreePersonReference($decoded); + + $this->assertNotNull($ref2->getAttribution()); + $this->assertEquals('Linked from alternate tree', $ref2->getAttribution()->getChangeMessage()); + } +}