diff --git a/README.md b/README.md index 164c00a..64e8b7a 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,9 @@ Include jupitern/cosmosdb in your project, by adding it to your composer.json fi ## Changelog +### v2.7.0 +- adding support for PATCH verb (CosmosDb add-on PATCH API) with Add, Set, Replace, Remove, Increment and Move operations, including "getPatchOp[OP]" helper functions + ### v2.6.0 - code refactor. min PHP verion supported is now 8.0 - selectCollection no longer creates a colletion if not exist. use createCollection for that @@ -43,7 +46,8 @@ Include jupitern/cosmosdb in your project, by adding it to your composer.json fi This package adds additional functionalities to the [AzureDocumentDB-PHP](https://github.com/cocteau666/AzureDocumentDB-PHP) package. All other functionality exists in this package as well. ## Limitations -Use of `limit()` or `order()` in cross-partition queries is currently not supported. +- Use of `limit()` or `order()` in cross-partition queries is currently not supported. +- Multi-document patch using transaction based requests is currently not supported. ## Usage @@ -90,6 +94,52 @@ $rid = \Jupitern\CosmosDb\QueryBuilder::instance() ]); ``` +### Patching Records + +```php + +# Patch operations: ADD | SET | REPLACE | REMOVE | INCR | MOVE +# https://learn.microsoft.com/en-us/azure/cosmos-db/partial-document-update#similarities-and-differences + +# Where a PartitionKey is in use, the PartitionValue should be set on the QueryBuilder instance +# prior to making any PATCH operations, as by the nature of PATCH, there is no document body to find the value in, +# and the value is taken from the class property when the request is made. $rid_doc is also required because PATCH is an item-level operation. + +# Example starting document (as array) + +# [ +# "_rid" => $rid, +# 'id' => '2', +# 'name' => 'Jane Doe Something', +# 'age' => 36, +# 'country' => 'Portugal' +# ] + +$res = \Jupitern\CosmosDb\QueryBuilder::instance() + ->setCollection($collection) + ->setPartitionKey('country') + ->setPartitionValue('Portugal'); + +# Patch operations can be batched, so the $operations argument is an array of arrays +# Batch patch operations are limited to 10 operations per request +$operations[] = $res->getPatchOpSet('/age', 38); +$operations[] = $res->getPatchOpAdd('/region' 'Algarve'); + +$rid_doc = $res->patch($rid_doc, $operations); + +# Example patched document (as array) + +# [ +# "_rid" => $rid, +# 'id' => '2', +# 'name' => 'Jane Doe Something', +# 'age' => 38, +# 'country' => 'Portugal' +# 'region' => 'Algarve' +# ] + +``` + ### Querying Records ```php diff --git a/src/CosmosDb.php b/src/CosmosDb.php index c76ad82..bdb16b6 100644 --- a/src/CosmosDb.php +++ b/src/CosmosDb.php @@ -39,7 +39,7 @@ public function setHttpClientOptions(array $options = []) * * @link http://msdn.microsoft.com/en-us/library/azure/dn783368.aspx * @access private - * @param string $verb Request Method (GET, POST, PUT, DELETE) + * @param string $verb Request Method (GET, POST, PUT, DELETE, PATCH) * @param string $resource_type Resource Type * @param string $resource_id Resource ID * @return array of Request Headers @@ -151,20 +151,20 @@ public function getInfo(): string * @return array JSON response * @throws GuzzleException */ - public function query(string $rid_id, string $rid_col, string $query, bool $isCrossPartition = false, $partitionValue = null): array - { + public function query(string $rid_id, string $rid_col, string $query, bool $isCrossPartition = false, $partitionValue = null): array + { $headers = $this->getAuthHeaders('POST', 'docs', $rid_col); $headers['Content-Length'] = strlen($query); $headers['Content-Type'] = 'application/query+json'; $headers['x-ms-max-item-count'] = -1; $headers['x-ms-documentdb-isquery'] = 'True'; - + if ($isCrossPartition) { $headers['x-ms-documentdb-query-enablecrosspartition'] = 'True'; } - + if ($partitionValue) { - $headers['x-ms-documentdb-partitionkey'] = '["'.$partitionValue.'"]'; + $headers['x-ms-documentdb-partitionkey'] = '["' . $partitionValue . '"]'; } /* * Fix for https://github.com/jupitern/cosmosdb/issues/21 (credits to https://github.com/ElvenSpellmaker). @@ -186,8 +186,8 @@ public function query(string $rid_id, string $rid_col, string $query, bool $isCr $result = $this->request("/dbs/{$rid_id}/colls/{$rid_col}/docs", "POST", $headers, $query); $results[] = $result->getBody()->getContents(); } - } - catch (\GuzzleHttp\Exception\ClientException $e) { + } + catch (\GuzzleHttp\Exception\ClientException $e) { $responseError = \json_decode($e->getResponse()->getBody()->getContents()); // -- Retry the request with PK Ranges -- @@ -220,14 +220,14 @@ public function query(string $rid_id, string $rid_col, string $query, bool $isCr * @return mixed * @throws GuzzleException */ - public function getPkRanges(string $rid_id, string $rid_col): mixed + public function getPkRanges(string $rid_id, string $rid_col): mixed { - $headers = $this->getAuthHeaders('GET', 'pkranges', $rid_col); - $headers['Accept'] = 'application/json'; - $headers['x-ms-max-item-count'] = -1; - $result = $this->request("/dbs/{$rid_id}/colls/{$rid_col}/pkranges", "GET", $headers); - return json_decode($result->getBody()->getContents()); - } + $headers = $this->getAuthHeaders('GET', 'pkranges', $rid_col); + $headers['Accept'] = 'application/json'; + $headers['x-ms-max-item-count'] = -1; + $result = $this->request("/dbs/{$rid_id}/colls/{$rid_col}/pkranges", "GET", $headers); + return json_decode($result->getBody()->getContents()); + } /** * getPkFullRange @@ -238,12 +238,12 @@ public function getPkRanges(string $rid_id, string $rid_col): mixed * @return string * @throws GuzzleException */ - public function getPkFullRange($rid_id, $rid_col): string + public function getPkFullRange($rid_id, $rid_col): string { - $result = $this->getPkRanges($rid_id, $rid_col); - $ids = \array_column($result->PartitionKeyRanges, "id"); - return $result->_rid . "," . \implode(",", $ids); - } + $result = $this->getPkRanges($rid_id, $rid_col); + $ids = \array_column($result->PartitionKeyRanges, "id"); + return $result->_rid . "," . \implode(",", $ids); + } /** * listDatabases @@ -534,7 +534,7 @@ public function createDocument(string $rid_id, string $rid_col, string $json, st $headers = \array_merge($headers, $authHeaders); $headers['Content-Length'] = strlen($json); if ($partitionKey !== null) { - $headers['x-ms-documentdb-partitionkey'] = '["'.$partitionKey.'"]'; + $headers['x-ms-documentdb-partitionkey'] = '["' . $partitionKey . '"]'; } return $this->request("/dbs/{$rid_id}/colls/{$rid_col}/docs", "POST", $headers, $json)->getBody()->getContents(); @@ -560,12 +560,39 @@ public function replaceDocument(string $rid_id, string $rid_col, string $rid_doc $headers = \array_merge($headers, $authHeaders); $headers['Content-Length'] = strlen($json); if ($partitionKey !== null) { - $headers['x-ms-documentdb-partitionkey'] = '["'.$partitionKey.'"]'; + $headers['x-ms-documentdb-partitionkey'] = '["' . $partitionKey . '"]'; } return $this->request("/dbs/{$rid_id}/colls/{$rid_col}/docs/{$rid_doc}", "PUT", $headers, $json)->getBody()->getContents(); } + /** + * patchDocument + * + * @link https://learn.microsoft.com/en-us/azure/cosmos-db/partial-document-update#rest-api-reference-for-partial-document-update + * @access public + * @param string $rid_id Resource ID + * @param string $rid_col Resource Collection ID + * @param string $rid_doc Resource Doc ID + * @param string $json JSON request + * @param string|null $partitionKey + * @param array $headers Optional headers to send along with the request + * @return string JSON response + * @throws GuzzleException + */ + public function patchDocument(string $rid_id, string $rid_col, string $rid_doc, string $json, string $partitionKey = null, array $headers = []): string + { + $authHeaders = $this->getAuthHeaders('PATCH', 'docs', $rid_doc); + $headers = \array_merge($headers, $authHeaders); + $headers['Content-Length'] = strlen($json); + $headers['Content-Type'] = 'application/json_patch+json'; + if ($partitionKey !== null) { + $headers['x-ms-documentdb-partitionkey'] = '["' . $partitionKey . '"]'; + } + + return $this->request("/dbs/{$rid_id}/colls/{$rid_col}/docs/{$rid_doc}", "PATCH", $headers, $json)->getBody()->getContents(); + } + /** * deleteDocument * @@ -585,7 +612,7 @@ public function deleteDocument(string $rid_id, string $rid_col, string $rid_doc, $headers = \array_merge($headers, $authHeaders); $headers['Content-Length'] = '0'; if ($partitionKey !== null) { - $headers['x-ms-documentdb-partitionkey'] = '["'.$partitionKey.'"]'; + $headers['x-ms-documentdb-partitionkey'] = '["' . $partitionKey . '"]'; } return $this->request("/dbs/{$rid_id}/colls/{$rid_col}/docs/{$rid_doc}", "DELETE", $headers)->getBody()->getContents(); @@ -935,7 +962,7 @@ public function replaceStoredProcedure(string $rid_id, string $rid_col, string $ * @throws GuzzleException */ public function deleteStoredProcedure(string $rid_id, string $rid_col, string $rid_sproc): string - { + { $headers = $this->getAuthHeaders('DELETE', 'sprocs', $rid_sproc); $headers['Content-Length'] = '0'; return $this->request("/dbs/{$rid_id}/colls/{$rid_col}/sprocs/{$rid_sproc}", "DELETE", $headers)->getBody()->getContents(); @@ -952,7 +979,7 @@ public function deleteStoredProcedure(string $rid_id, string $rid_col, string $r * @throws GuzzleException */ public function listUserDefinedFunctions(string $rid_id, string $rid_col): string - { + { $headers = $this->getAuthHeaders('GET', 'udfs', $rid_col); $headers['Content-Length'] = '0'; return $this->request("/dbs/{$rid_id}/colls/{$rid_col}/udfs", "GET", $headers)->getBody()->getContents(); @@ -970,7 +997,7 @@ public function listUserDefinedFunctions(string $rid_id, string $rid_col): strin * @throws GuzzleException */ public function createUserDefinedFunction(string $rid_id, string $rid_col, string $json): string - { + { $headers = $this->getAuthHeaders('POST', 'udfs', $rid_col); $headers['Content-Length'] = strlen($json); return $this->request("/dbs/{$rid_id}/colls/{$rid_col}/udfs", "POST", $headers, $json)->getBody()->getContents(); @@ -989,7 +1016,7 @@ public function createUserDefinedFunction(string $rid_id, string $rid_col, strin * @throws GuzzleException */ public function replaceUserDefinedFunction(string $rid_id, string $rid_col, string $rid_udf, string $json): string - { + { $headers = $this->getAuthHeaders('PUT', 'udfs', $rid_udf); $headers['Content-Length'] = strlen($json); return $this->request("/dbs/{$rid_id}/colls/{$rid_col}/udfs/{$rid_udf}", "PUT", $headers, $json)->getBody()->getContents(); @@ -1007,7 +1034,7 @@ public function replaceUserDefinedFunction(string $rid_id, string $rid_col, stri * @throws GuzzleException */ public function deleteUserDefinedFunction(string $rid_id, string $rid_col, string $rid_udf): string - { + { $headers = $this->getAuthHeaders('DELETE', 'udfs', $rid_udf); $headers['Content-Length'] = '0'; return $this->request("/dbs/{$rid_id}/colls/{$rid_col}/udfs/{$rid_udf}", "DELETE", $headers)->getBody()->getContents(); @@ -1024,7 +1051,7 @@ public function deleteUserDefinedFunction(string $rid_id, string $rid_col, strin * @throws GuzzleException */ public function listTriggers(string $rid_id, string $rid_col): string - { + { $headers = $this->getAuthHeaders('GET', 'triggers', $rid_col); $headers['Content-Length'] = '0'; return $this->request("/dbs/{$rid_id}/colls/{$rid_col}/triggers", "GET", $headers)->getBody()->getContents(); @@ -1042,7 +1069,7 @@ public function listTriggers(string $rid_id, string $rid_col): string * @throws GuzzleException */ public function createTrigger(string $rid_id, string $rid_col, string $json): string - { + { $headers = $this->getAuthHeaders('POST', 'triggers', $rid_col); $headers['Content-Length'] = strlen($json); return $this->request("/dbs/{$rid_id}/colls/{$rid_col}/triggers", "POST", $headers, $json)->getBody()->getContents(); @@ -1061,7 +1088,7 @@ public function createTrigger(string $rid_id, string $rid_col, string $json): st * @throws GuzzleException */ public function replaceTrigger(string $rid_id, string $rid_col, string $rid_trigger, string $json): string - { + { $headers = $this->getAuthHeaders('PUT', 'triggers', $rid_trigger); $headers['Content-Length'] = strlen($json); return $this->request("/dbs/{$rid_id}/colls/{$rid_col}/triggers/{$rid_trigger}", "PUT", $headers, $json)->getBody()->getContents(); @@ -1079,7 +1106,7 @@ public function replaceTrigger(string $rid_id, string $rid_col, string $rid_trig * @throws GuzzleException */ public function deleteTrigger(string $rid_id, string $rid_col, string $rid_trigger): string - { + { $headers = $this->getAuthHeaders('DELETE', 'triggers', $rid_trigger); $headers['Content-Length'] = '0'; return $this->request("/dbs/{$rid_id}/colls/{$rid_col}/triggers/{$rid_trigger}", "DELETE", $headers)->getBody()->getContents(); diff --git a/src/CosmosDbCollection.php b/src/CosmosDbCollection.php index 5c5c983..421bc91 100644 --- a/src/CosmosDbCollection.php +++ b/src/CosmosDbCollection.php @@ -35,9 +35,9 @@ public function query($query, $params = [], $isCrossPartition = false, $partitio { $paramsJson = []; foreach ($params as $key => $val) { - $val = is_int($val) || is_float($val) ? $val : '"'. str_replace('"', '\\"', $val) .'"'; + $val = is_int($val) || is_float($val) ? $val : '"' . str_replace('"', '\\"', $val) . '"'; - $paramsJson[] = '{"name": "' . str_replace('"', '\\"', $key) . '", "value": '.$val.'}'; + $paramsJson[] = '{"name": "' . str_replace('"', '\\"', $key) . '", "value": ' . $val . '}'; } $query = '{"query": "' . str_replace('"', '\\"', $query) . '", "parameters": [' . implode(',', $paramsJson) . ']}'; @@ -45,25 +45,25 @@ public function query($query, $params = [], $isCrossPartition = false, $partitio return $this->document_db->query($this->rid_db, $this->rid_col, $query, $isCrossPartition, $partitionValue); } - /** - * getPkRanges - * - * @return mixed - */ - public function getPkRanges() - { - return $this->document_db->getPkRanges($this->rid_db, $this->rid_col); - } - - /** - * getPkFullRange - * - * @return mixed - */ - public function getPkFullRange() - { - return $this->document_db->getPkFullRange($this->rid_db, $this->rid_col); - } + /** + * getPkRanges + * + * @return mixed + */ + public function getPkRanges() + { + return $this->document_db->getPkRanges($this->rid_db, $this->rid_col); + } + + /** + * getPkFullRange + * + * @return mixed + */ + public function getPkFullRange() + { + return $this->document_db->getPkFullRange($this->rid_db, $this->rid_col); + } /** * createDocument @@ -94,6 +94,21 @@ public function replaceDocument($rid, $json, $partitionKey = null, array $header return $this->document_db->replaceDocument($this->rid_db, $this->rid_col, $rid, $json, $partitionKey, $headers); } + /** + * patchDocument + * + * @access public + * @param string $rid document ResourceID (_rid) + * @param string $json JSON formatted operations array + * @param string $partitionKey + * @param array $headers Optional headers to send along with the request + * @return string JSON strings + */ + public function patchDocument($rid_doc, $operations, $partitionKey = null, array $headers = []) + { + return $this->document_db->patchDocument($this->rid_db, $this->rid_col, $rid_doc, $operations, $partitionKey, $headers); + } + /** * deleteDocument * @@ -134,7 +149,7 @@ public function getPermission($uid, $pid) return $this->document_db->getPermission($this->rid_db, $uid, $pid); } */ - + public function listStoredProcedures() { return $this->document_db->listStoredProcedures($this->rid_db, $this->rid_col); diff --git a/src/QueryBuilder.php b/src/QueryBuilder.php index 10904c8..f5d4ffd 100644 --- a/src/QueryBuilder.php +++ b/src/QueryBuilder.php @@ -89,9 +89,9 @@ public function where(string $where): static * @return QueryBuilder */ public function whereStartsWith(string $field, mixed $value): static - { - return $this->where("STARTSWITH($field, '{$value}')"); - } + { + return $this->where("STARTSWITH($field, '{$value}')"); + } /** * @param string $field @@ -100,8 +100,8 @@ public function whereStartsWith(string $field, mixed $value): static */ public function whereEndsWith(string $field, mixed $value): static { - return $this->where("ENDSWITH($field, '{$value}')"); - } + return $this->where("ENDSWITH($field, '{$value}')"); + } /** * @param string $field @@ -110,8 +110,8 @@ public function whereEndsWith(string $field, mixed $value): static */ public function whereContains(string $field, mixed $value): static { - return $this->where("CONTAINS($field, '{$value}'"); - } + return $this->where("CONTAINS($field, '{$value}'"); + } /** * @param string $field @@ -119,11 +119,11 @@ public function whereContains(string $field, mixed $value): static * @return $this|QueryBuilder */ public function whereIn(string $field, array $values): QueryBuilder|static - { - if (empty($values)) return $this; + { + if (empty($values)) return $this; - return $this->where("$field IN('".implode("', '", $values)."')"); - } + return $this->where("$field IN('" . implode("', '", $values) . "')"); + } /** * @param string $field @@ -134,7 +134,7 @@ public function whereNotIn(string $field, array $values): QueryBuilder|static { if (empty($values)) return $this; - return $this->where("$field NOT IN('".implode("', '", $values)."')"); + return $this->where("$field NOT IN('" . implode("', '", $values) . "')"); } /** @@ -171,6 +171,7 @@ public function params(array $params): static * @param boolean $isCrossPartition * @return $this */ + public function findAll(bool $isCrossPartition = false): static { $this->response = null; @@ -229,10 +230,10 @@ public function setPartitionKey($fieldName): static * @return null */ public function getPartitionKey() - { - return $this->partitionKey; + { + return $this->partitionKey; } - + /** * @param $fieldName * @return $this @@ -248,9 +249,9 @@ public function setPartitionValue($fieldName): static * @return null */ public function getPartitionValue() - { - return $this->partitionValue; - } + { + return $this->partitionValue; + } /** * @param string $string @@ -267,8 +268,8 @@ public function setQueryString(string $string): static */ public function getQueryString(): ?string { - return $this->queryString; - } + return $this->queryString; + } /** * @param string $partitionKey @@ -285,6 +286,106 @@ public function isNested(string $partitionKey): bool|static return str_contains($partitionKey, '/'); } + /** + * @param string $path Instances of / in property names within $path must be escaped as /~1 + * @param mixed $value + * @return array Fully formed ADD patch operation element + */ + + public function getPatchOpAdd(string $path, mixed $value): array + { + + $op = [ + 'op' => 'add', + 'path' => str_replace('~', '~0', $path), + 'value' => $value + ]; + return $op; + } + + /** + * @param string $path Instances of / in property names within $path must be escaped as /~1 + * @param mixed $value + * @return array Fully formed SET patch operation element + */ + + public function getPatchOpSet(string $path, mixed $value): array + { + + $op = [ + 'op' => 'set', + 'path' => str_replace('~', '~0', $path), + 'value' => $value + ]; + return $op; + } + + /** + * @param string $path Instances of / in property names within $path must be escaped as /~1 + * @param mixed $value + * @return array Fully formed REPLACE patch operation element + */ + + public function getPatchOpReplace(string $path, mixed $value): array + { + + $op = [ + 'op' => 'replace', + 'path' => str_replace('~', '~0', $path), + 'value' => $value + ]; + return $op; + } + + /** + * @param string $path Instances of / in property names within $path must be escaped as /~1 + * @return array Fully formed REMOVE patch operation element + */ + + public function getPatchOpRemove(string $path): array + { + + $op = [ + 'op' => 'remove', + 'path' => str_replace('~', '~0', $path), + ]; + return $op; + } + + /** + * @param string $path Instances of / in property names within $path must be escaped as /~1 + * @param int $value + * @return array Fully formed INCR patch operation element + */ + + public function getPatchOpIncrement(string $path, int $value): array + { + + $op = [ + 'op' => 'replace', + 'path' => str_replace('~', '~0', $path), + 'value' => $value + ]; + return $op; + } + + /** + * @param string $fromPath Source property - Instances of / in property names within $path must be escaped as /~1 + * @param string $toPath Destination property - Instances of / in property names within $path must be escaped as /~1 + * @return array Fully formed MOVE patch operation element + */ + + public function getPatchOpMove(string $fromPath, string $toPath): array + { + + $op = [ + 'op' => 'replace', + 'from' => str_replace('~', '~0', $fromPath), + 'path' => str_replace('~', '~0', $toPath), + ]; + return $op; + } + /** * Find and set the partition value * @@ -305,7 +406,7 @@ public function findPartitionValue(object $document, bool $toString = false) # formatted as a cosmos query string if ($toString) { - foreach( $properties as $p ) { + foreach ($properties as $p) { $this->setQueryString($p); } @@ -315,7 +416,7 @@ public function findPartitionValue(object $document, bool $toString = false) # and find the value of the property key else { - foreach( $properties as $p ) { + foreach ($properties as $p) { $document = (object)$document->{$p}; } @@ -354,13 +455,49 @@ public function save($document) return $resultObj->_rid ?? null; } + /** + * @param $rid_doc + * @param $patchOps Array Array of operations, max 10 per request + * @return string|null + * @throws \Exception + */ + public function patch(string $rid_doc, array $patchOps) + { + if (count($patchOps) > 10) { + //Throw an equivalent HTTP error rather than waste a request to have the API return the same + throw new \Exception("400 : PATCH supports maximum of 10 operations per request"); + }; + + $partitionValue = $this->partitionKey != null ? $this->partitionValue : null; + //Conditional patch is possible - check if QueryBuilder was set up with a condition and append + $condition = ($this->where) ? 'from ' . $this->from . ' where ' . $this->where : ''; + + $updates = []; + if ($condition) { + $updates['condition'] = $condition; + }; + $updates['operations'] = $patchOps; + $json = json_encode($updates); + + $result = $this->collection->patchDocument($rid_doc, $json, $partitionValue, $this->triggersAsHeaders("patch")); + $resultObj = json_decode($result); + + if (isset($resultObj->code) && isset($resultObj->message)) { + throw new \Exception("$resultObj->code : $resultObj->message"); + } + + return $resultObj->_rid ?? null; + } + + + /* delete */ /** * @param boolean $isCrossPartition * @return boolean */ - public function delete(bool $isCrossPartition = false) :bool + public function delete(bool $isCrossPartition = false): bool { $this->response = null; @@ -382,7 +519,7 @@ public function delete(bool $isCrossPartition = false) :bool * @param boolean $isCrossPartition * @return boolean */ - public function deleteAll(bool $isCrossPartition = false) :bool + public function deleteAll(bool $isCrossPartition = false): bool { $this->response = null; @@ -410,7 +547,7 @@ public function deleteAll(bool $isCrossPartition = false) :bool public function addTrigger(string $operation, string $type, string $id): self { $operation = \strtolower($operation); - if (!\in_array($operation, ["all", "create", "delete", "replace"])) + if (!\in_array($operation, ["all", "create", "delete", "replace", "patch"])) throw new \Exception("Trigger: Invalid operation \"{$operation}\""); $type = \strtolower($type); @@ -432,7 +569,7 @@ protected function triggersAsHeaders(string $operation): array { $headers = []; - // Add headers for the current operation type at $operation (create|delete!replace) + // Add headers for the current operation type at $operation (create|delete!replace|patch) if (isset($this->triggers[$operation])) { foreach ($this->triggers[$operation] as $name => $ids) { $ids = \is_array($ids) ? $ids : [$ids]; @@ -520,7 +657,7 @@ public function toArray($arrayKey = null): array|null $results = (array)$this->toObject($arrayKey); if ($this->multipleResults && is_array($results)) { - array_walk($results, function(&$value) { + array_walk($results, function (&$value) { $value = (array)$value; }); } @@ -533,9 +670,8 @@ public function toArray($arrayKey = null): array|null * @param null $default * @return mixed */ - public function getValue($fieldName, $default = null) + public function getValue($fieldName, $default = null) { return ($this->toObject())->{$fieldName} ?? $default; } - }