diff --git a/CHANGELOG.md b/CHANGELOG.md index 6eef549b..0f598152 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://github.com/kbsali/php-redmine-api/compare/v2.2.0...v2.x) +### Deprecated + +- `Redmine\Api\AbstractApi::attachCustomFieldXML()` is deprecated +- `Redmine\Api\Project::prepareParamsXml()` is deprecated + ## [v2.2.0](https://github.com/kbsali/php-redmine-api/compare/v2.1.1...v2.2.0) - 2022-03-01 ### Added diff --git a/src/Redmine/Api/AbstractApi.php b/src/Redmine/Api/AbstractApi.php index e34fa81d..fc9cdde3 100644 --- a/src/Redmine/Api/AbstractApi.php +++ b/src/Redmine/Api/AbstractApi.php @@ -185,7 +185,7 @@ protected function retrieveAll($endpoint, array $params = []) } /** - * Retrieves all the elements of a given endpoint (even if the + * Retrieves as many elements as you want of a given endpoint (even if the * total number of elements is greater than 100). * * @param string $endpoint API end point @@ -253,6 +253,8 @@ protected function retrieveData(string $endpoint, array $params = []): array /** * Attaches Custom Fields to a create/update query. * + * @deprecated the `attachCustomFieldXML()` method is deprecated. + * * @param SimpleXMLElement $xml XML Element the custom fields are attached to * @param array $fields array of fields to attach, each field needs name, id and value set * @@ -262,6 +264,8 @@ protected function retrieveData(string $endpoint, array $params = []): array */ protected function attachCustomFieldXML(SimpleXMLElement $xml, array $fields) { + @trigger_error('The '.__METHOD__.' method is deprecated.', E_USER_DEPRECATED); + $_fields = $xml->addChild('custom_fields'); $_fields->addAttribute('type', 'array'); foreach ($fields as $field) { diff --git a/src/Redmine/Api/Group.php b/src/Redmine/Api/Group.php index ef8169c4..4457ae16 100644 --- a/src/Redmine/Api/Group.php +++ b/src/Redmine/Api/Group.php @@ -5,6 +5,7 @@ use Exception; use Redmine\Exception\MissingParameterException; use Redmine\Serializer\PathSerializer; +use Redmine\Serializer\XmlSerializer; /** * Handling of groups. @@ -78,9 +79,10 @@ public function create(array $params = []) throw new MissingParameterException('Theses parameters are mandatory: `name`'); } - $xml = $this->buildXML($params); - - return $this->post('/groups.xml', $xml->asXML()); + return $this->post( + '/groups.xml', + XmlSerializer::createFromArray(['group' => $params])->getEncoded() + ); } /** @@ -142,9 +144,10 @@ public function remove($id) */ public function addUser($id, $userId) { - $xml = new \SimpleXMLElement(''.$userId.''); - - return $this->post('/groups/'.$id.'/users.xml', $xml->asXML()); + return $this->post( + '/groups/'.$id.'/users.xml', + XmlSerializer::createFromArray(['user_id' => $userId])->getEncoded() + ); } /** @@ -161,29 +164,4 @@ public function removeUser($id, $userId) { return $this->delete('/groups/'.$id.'/users/'.$userId.'.xml'); } - - /** - * Build the XML for a group. - * - * @param array $params for the new/updated group data - * - * @return \SimpleXMLElement - */ - private function buildXML(array $params = []) - { - $xml = new \SimpleXMLElement(''); - - foreach ($params as $k => $v) { - if ('user_ids' === $k && is_array($v)) { - $item = $xml->addChild($k); - foreach ($v as $role) { - $item->addChild('user_id', $role); - } - } else { - $xml->addChild($k, $v); - } - } - - return $xml; - } } diff --git a/src/Redmine/Api/Issue.php b/src/Redmine/Api/Issue.php index 312ea14a..895c612e 100644 --- a/src/Redmine/Api/Issue.php +++ b/src/Redmine/Api/Issue.php @@ -2,7 +2,9 @@ namespace Redmine\Api; +use Redmine\Serializer\JsonSerializer; use Redmine\Serializer\PathSerializer; +use Redmine\Serializer\XmlSerializer; /** * Listing issues, searching, editing and closing your projects issues. @@ -66,45 +68,6 @@ public function show($id, array $params = []) ); } - /** - * Build the XML for an issue. - * - * @param array $params for the new/updated issue data - * - * @return \SimpleXMLElement - */ - private function buildXML(array $params = []) - { - $xml = new \SimpleXMLElement(''); - - foreach ($params as $k => $v) { - if ('custom_fields' === $k && is_array($v)) { - $this->attachCustomFieldXML($xml, $v); - } elseif ('watcher_user_ids' === $k && is_array($v)) { - $watcherUserIds = $xml->addChild('watcher_user_ids', ''); - $watcherUserIds->addAttribute('type', 'array'); - foreach ($v as $watcher) { - $watcherUserIds->addChild('watcher_user_id', (int) $watcher); - } - } elseif ('uploads' === $k && is_array($v)) { - $uploadsItem = $xml->addChild('uploads', ''); - $uploadsItem->addAttribute('type', 'array'); - foreach ($v as $upload) { - $upload_item = $uploadsItem->addChild('upload', ''); - foreach ($upload as $upload_k => $upload_v) { - $upload_item->addChild($upload_k, $upload_v); - } - } - } else { - // "addChild" does not escape text for XML value, but the setter does. - // http://stackoverflow.com/a/555039/99904 - $xml->$k = $v; - } - } - - return $xml; - } - /** * Create a new issue given an array of $params * The issue is assigned to the authenticated user. @@ -135,9 +98,10 @@ public function create(array $params = []) $params = $this->cleanParams($params); $params = $this->sanitizeParams($defaults, $params); - $xml = $this->buildXML($params); - - return $this->post('/issues.xml', $xml->asXML()); + return $this->post( + '/issues.xml', + XmlSerializer::createFromArray(['issue' => $params])->getEncoded() + ); } /** @@ -171,9 +135,10 @@ public function update($id, array $params) $sanitizedParams['assigned_to_id'] = ''; } - $xml = $this->buildXML($sanitizedParams); - - return $this->put('/issues/'.$id.'.xml', $xml->asXML()); + return $this->put( + '/issues/'.$id.'.xml', + XmlSerializer::createFromArray(['issue' => $sanitizedParams])->getEncoded() + ); } /** @@ -184,7 +149,10 @@ public function update($id, array $params) */ public function addWatcher($id, $watcherUserId) { - return $this->post('/issues/'.$id.'/watchers.xml', ''.$watcherUserId.''); + return $this->post( + '/issues/'.$id.'/watchers.xml', + XmlSerializer::createFromArray(['user_id' => $watcherUserId])->getEncoded() + ); } /** @@ -207,10 +175,9 @@ public function removeWatcher($id, $watcherUserId) public function setIssueStatus($id, $status) { $api = $this->client->getApi('issue_status'); - $statusId = $api->getIdByName($status); return $this->update($id, [ - 'status_id' => $statusId, + 'status_id' => $api->getIdByName($status), ]); } @@ -300,13 +267,15 @@ public function attach($id, array $attachment) */ public function attachMany($id, array $attachments) { - $request = []; - $request['issue'] = [ + $params = [ 'id' => $id, 'uploads' => $attachments, ]; - return $this->put('/issues/'.$id.'.json', json_encode($request)); + return $this->put( + '/issues/'.$id.'.json', + JsonSerializer::createFromArray(['issue' => $params])->getEncoded() + ); } /** diff --git a/src/Redmine/Api/IssueCategory.php b/src/Redmine/Api/IssueCategory.php index 2a2a8698..cd375c3c 100644 --- a/src/Redmine/Api/IssueCategory.php +++ b/src/Redmine/Api/IssueCategory.php @@ -4,6 +4,7 @@ use Redmine\Exception\MissingParameterException; use Redmine\Serializer\PathSerializer; +use Redmine\Serializer\XmlSerializer; /** * Listing issue categories, creating, editing. @@ -112,12 +113,10 @@ public function create($project, array $params = []) throw new MissingParameterException('Theses parameters are mandatory: `name`'); } - $xml = new \SimpleXMLElement(''); - foreach ($params as $k => $v) { - $xml->addChild($k, $v); - } - - return $this->post('/projects/'.$project.'/issue_categories.xml', $xml->asXML()); + return $this->post( + '/projects/'.$project.'/issue_categories.xml', + XmlSerializer::createFromArray(['issue_category' => $params])->getEncoded() + ); } /** @@ -137,12 +136,10 @@ public function update($id, array $params) ]; $params = $this->sanitizeParams($defaults, $params); - $xml = new \SimpleXMLElement(''); - foreach ($params as $k => $v) { - $xml->addChild($k, $v); - } - - return $this->put('/issue_categories/'.$id.'.xml', $xml->asXML()); + return $this->put( + '/issue_categories/'.$id.'.xml', + XmlSerializer::createFromArray(['issue_category' => $params])->getEncoded() + ); } /** diff --git a/src/Redmine/Api/IssueRelation.php b/src/Redmine/Api/IssueRelation.php index 1574b9ba..31b319a5 100644 --- a/src/Redmine/Api/IssueRelation.php +++ b/src/Redmine/Api/IssueRelation.php @@ -85,9 +85,10 @@ public function create($issueId, array $params = []) $params = $this->sanitizeParams($defaults, $params); - $params = json_encode(['relation' => $params]); - - $response = $this->post('/issues/'.urlencode($issueId).'/relations.json', $params); + $response = $this->post( + '/issues/'.urlencode($issueId).'/relations.json', + JsonSerializer::createFromArray(['relation' => $params])->getEncoded() + ); return JsonSerializer::createFromString($response)->getNormalized(); } diff --git a/src/Redmine/Api/Membership.php b/src/Redmine/Api/Membership.php index ed5cbd33..7b792d04 100644 --- a/src/Redmine/Api/Membership.php +++ b/src/Redmine/Api/Membership.php @@ -3,6 +3,7 @@ namespace Redmine\Api; use Redmine\Exception\MissingParameterException; +use Redmine\Serializer\XmlSerializer; /** * Handling project memberships. @@ -56,9 +57,10 @@ public function create($project, array $params = []) throw new MissingParameterException('Theses parameters are mandatory: `user_id`, `role_ids`'); } - $xml = $this->buildXML($params); - - return $this->post('/projects/'.$project.'/memberships.xml', $xml->asXML()); + return $this->post( + '/projects/'.$project.'/memberships.xml', + XmlSerializer::createFromArray(['membership' => $params])->getEncoded() + ); } /** @@ -84,9 +86,10 @@ public function update($id, array $params = []) throw new MissingParameterException('Missing mandatory parameters'); } - $xml = $this->buildXML($params); - - return $this->put('/memberships/'.$id.'.xml', $xml->asXML()); + return $this->put( + '/memberships/'.$id.'.xml', + XmlSerializer::createFromArray(['membership' => $params])->getEncoded() + ); } /** @@ -129,30 +132,4 @@ public function removeMember($projectId, $userId, array $params = []) return $removed; } - - /** - * Build the XML for a membership. - * - * @param array $params for the new/updated membership data - * - * @return \SimpleXMLElement - */ - private function buildXML(array $params = []) - { - $xml = new \SimpleXMLElement(''); - - foreach ($params as $k => $v) { - if ('role_ids' === $k && is_array($v)) { - $item = $xml->addChild($k); - $item->addAttribute('type', 'array'); - foreach ($v as $role) { - $item->addChild('role_id', $role); - } - } else { - $xml->addChild($k, $v); - } - } - - return $xml; - } } diff --git a/src/Redmine/Api/Project.php b/src/Redmine/Api/Project.php index 90fb25d2..c62cbf6e 100755 --- a/src/Redmine/Api/Project.php +++ b/src/Redmine/Api/Project.php @@ -4,6 +4,7 @@ use Redmine\Exception\MissingParameterException; use Redmine\Serializer\PathSerializer; +use Redmine\Serializer\XmlSerializer; /** * Listing projects, creating, editing. @@ -123,9 +124,10 @@ public function create(array $params = []) throw new MissingParameterException('Theses parameters are mandatory: `name`, `identifier`'); } - $xml = $this->prepareParamsXml($params); - - return $this->post('/projects.xml', $xml->asXML()); + return $this->post( + '/projects.xml', + XmlSerializer::createFromArray(['project' => $params])->getEncoded() + ); } /** @@ -147,40 +149,26 @@ public function update($id, array $params) ]; $params = $this->sanitizeParams($defaults, $params); - $xml = $this->prepareParamsXml($params); - - return $this->put('/projects/'.$id.'.xml', $xml->asXML()); + return $this->put( + '/projects/'.$id.'.xml', + XmlSerializer::createFromArray(['project' => $params])->getEncoded() + ); } /** + * @deprecated the `prepareParamsXml()` method is deprecated, use `\Redmine\Serializer\XmlSerializer::createFromArray()` instead. + * * @param array $params * * @return \SimpleXMLElement */ protected function prepareParamsXml($params) { - $_params = [ - 'tracker_ids' => 'tracker', - 'issue_custom_field_ids' => 'issue_custom_field', - 'enabled_module_names' => 'enabled_module_names', - ]; - - $xml = new \SimpleXMLElement(''); - foreach ($params as $k => $v) { - if ('custom_fields' === $k && is_array($v)) { - $this->attachCustomFieldXML($xml, $v); - } elseif (isset($_params[$k]) && is_array($v)) { - $array = $xml->addChild($k, ''); - $array->addAttribute('type', 'array'); - foreach ($v as $id) { - $array->addChild($_params[$k], $id); - } - } else { - $xml->addChild($k, htmlspecialchars($v)); - } - } + @trigger_error('The '.__METHOD__.' method is deprecated, use `\Redmine\Serializer\XmlSerializer::createFromArray()` instead.', E_USER_DEPRECATED); - return $xml; + return new \SimpleXMLElement( + XmlSerializer::createFromArray(['project' => $params])->getEncoded() + ); } /** diff --git a/src/Redmine/Api/TimeEntry.php b/src/Redmine/Api/TimeEntry.php index 7f8304b5..343633eb 100644 --- a/src/Redmine/Api/TimeEntry.php +++ b/src/Redmine/Api/TimeEntry.php @@ -3,6 +3,7 @@ namespace Redmine\Api; use Redmine\Exception\MissingParameterException; +use Redmine\Serializer\XmlSerializer; /** * Listing time entries, creating, editing. @@ -75,16 +76,10 @@ public function create(array $params = []) throw new MissingParameterException('Theses parameters are mandatory: `issue_id` or `project_id`, `hours`'); } - $xml = new \SimpleXMLElement(''); - foreach ($params as $k => $v) { - if ('custom_fields' === $k && is_array($v)) { - $this->attachCustomFieldXML($xml, $v); - } else { - $xml->addChild($k, htmlspecialchars($v)); - } - } - - return $this->post('/time_entries.xml', $xml->asXML()); + return $this->post( + '/time_entries.xml', + XmlSerializer::createFromArray(['time_entry' => $params])->getEncoded() + ); } /** @@ -109,16 +104,10 @@ public function update($id, array $params) ]; $params = $this->sanitizeParams($defaults, $params); - $xml = new \SimpleXMLElement(''); - foreach ($params as $k => $v) { - if ('custom_fields' === $k && is_array($v)) { - $this->attachCustomFieldXML($xml, $v); - } else { - $xml->addChild($k, htmlspecialchars($v)); - } - } - - return $this->put('/time_entries/'.$id.'.xml', $xml->asXML()); + return $this->put( + '/time_entries/'.$id.'.xml', + XmlSerializer::createFromArray(['time_entry' => $params])->getEncoded() + ); } /** diff --git a/src/Redmine/Api/User.php b/src/Redmine/Api/User.php index b813fdc8..05c4fc09 100644 --- a/src/Redmine/Api/User.php +++ b/src/Redmine/Api/User.php @@ -4,6 +4,7 @@ use Redmine\Exception\MissingParameterException; use Redmine\Serializer\PathSerializer; +use Redmine\Serializer\XmlSerializer; /** * Listing users, creating, editing. @@ -153,16 +154,11 @@ public function create(array $params = []) ) { throw new MissingParameterException('Theses parameters are mandatory: `login`, `lastname`, `firstname`, `mail`'); } - $xml = new \SimpleXMLElement(''); - foreach ($params as $k => $v) { - if ('custom_fields' === $k) { - $this->attachCustomFieldXML($xml, $v); - } else { - $xml->addChild($k, $v); - } - } - return $this->post('/users.xml', $xml->asXML()); + return $this->post( + '/users.xml', + XmlSerializer::createFromArray(['user' => $params])->getEncoded() + ); } /** @@ -186,16 +182,10 @@ public function update($id, array $params) ]; $params = $this->sanitizeParams($defaults, $params); - $xml = new \SimpleXMLElement(''); - foreach ($params as $k => $v) { - if ('custom_fields' === $k) { - $this->attachCustomFieldXML($xml, $v); - } else { - $xml->addChild($k, $v); - } - } - - return $this->put('/users/'.$id.'.xml', $xml->asXML()); + return $this->put( + '/users/'.$id.'.xml', + XmlSerializer::createFromArray(['user' => $params])->getEncoded() + ); } /** diff --git a/src/Redmine/Api/Version.php b/src/Redmine/Api/Version.php index 9c90fcd3..fda33443 100644 --- a/src/Redmine/Api/Version.php +++ b/src/Redmine/Api/Version.php @@ -4,6 +4,7 @@ use Redmine\Exception\InvalidParameterException; use Redmine\Exception\MissingParameterException; +use Redmine\Serializer\XmlSerializer; /** * Listing versions, creating, editing. @@ -120,16 +121,10 @@ public function create($project, array $params = []) $this->validateStatus($params); $this->validateSharing($params); - $xml = new \SimpleXMLElement(''); - foreach ($params as $k => $v) { - if ('custom_fields' === $k && is_array($v)) { - $this->attachCustomFieldXML($xml, $v); - } else { - $xml->addChild($k, $v); - } - } - - return $this->post('/projects/'.$project.'/versions.xml', $xml->asXML()); + return $this->post( + '/projects/'.$project.'/versions.xml', + XmlSerializer::createFromArray(['version' => $params])->getEncoded() + ); } /** @@ -154,16 +149,10 @@ public function update($id, array $params) $this->validateStatus($params); $this->validateSharing($params); - $xml = new \SimpleXMLElement(''); - foreach ($params as $k => $v) { - if ('custom_fields' === $k && is_array($v)) { - $this->attachCustomFieldXML($xml, $v); - } else { - $xml->addChild($k, $v); - } - } - - return $this->put('/versions/'.$id.'.xml', $xml->asXML()); + return $this->put( + '/versions/'.$id.'.xml', + XmlSerializer::createFromArray(['version' => $params])->getEncoded() + ); } private function validateStatus(array $params = []) diff --git a/src/Redmine/Api/Wiki.php b/src/Redmine/Api/Wiki.php index 1edb7d2d..cc459c3f 100644 --- a/src/Redmine/Api/Wiki.php +++ b/src/Redmine/Api/Wiki.php @@ -3,6 +3,7 @@ namespace Redmine\Api; use Redmine\Serializer\PathSerializer; +use Redmine\Serializer\XmlSerializer; /** * Listing Wiki pages. @@ -79,23 +80,10 @@ public function create($project, $page, array $params = []) ]; $params = $this->sanitizeParams($defaults, $params); - $xml = new \SimpleXMLElement(''); - foreach ($params as $k => $v) { - if ('uploads' === $k && is_array($v)) { - $item = $xml->addChild('uploads', ''); - $item->addAttribute('type', 'array'); - foreach ($v as $upload) { - $uploadItem = $item->addChild('upload', ''); - foreach ($upload as $uploadK => $uploadV) { - $uploadItem->addChild($uploadK, $uploadV); - } - } - } else { - $xml->addChild($k, htmlspecialchars($v)); - } - } - - return $this->put('/projects/'.$project.'/wiki/'.$page.'.xml', $xml->asXML()); + return $this->put( + '/projects/'.$project.'/wiki/'.$page.'.xml', + XmlSerializer::createFromArray(['wiki_page' => $params])->getEncoded() + ); } /** diff --git a/src/Redmine/Serializer/JsonSerializer.php b/src/Redmine/Serializer/JsonSerializer.php index 83ad0f97..e8310997 100644 --- a/src/Redmine/Serializer/JsonSerializer.php +++ b/src/Redmine/Serializer/JsonSerializer.php @@ -23,6 +23,17 @@ public static function createFromString(string $data): self return $serializer; } + /** + * @throws SerializerException if $data could not be serialized to JSON + */ + public static function createFromArray(array $data): self + { + $serializer = new self(); + $serializer->encode($data); + + return $serializer; + } + private string $encoded; /** @var mixed */ @@ -41,6 +52,11 @@ public function getNormalized() return $this->normalized; } + public function getEncoded(): string + { + return $this->encoded; + } + private function decode(string $encoded): void { $this->encoded = $encoded; @@ -56,4 +72,23 @@ private function decode(string $encoded): void throw new SerializerException('Catched error "'.$e->getMessage().'" while decoding JSON: '.$encoded, $e->getCode(), $e); } } + + private function encode(array $normalized): void + { + $this->normalized = $normalized; + + try { + $this->encoded = json_encode( + $normalized, + \JSON_THROW_ON_ERROR, + 512 + ); + } catch (JsonException $e) { + throw new SerializerException( + 'Could not encode JSON from array: ' . $e->getMessage(), + $e->getCode(), + $e + ); + } + } } diff --git a/src/Redmine/Serializer/XmlSerializer.php b/src/Redmine/Serializer/XmlSerializer.php index d7632dbb..682c6392 100644 --- a/src/Redmine/Serializer/XmlSerializer.php +++ b/src/Redmine/Serializer/XmlSerializer.php @@ -2,9 +2,9 @@ namespace Redmine\Serializer; -use Exception; use Redmine\Exception\SerializerException; use SimpleXMLElement; +use Throwable; /** * XmlSerializer. @@ -24,6 +24,17 @@ public static function createFromString(string $data): self return $serializer; } + /** + * @throws SerializerException if $data could not be serialized to XML + */ + public static function createFromArray(array $data): self + { + $serializer = new self(); + $serializer->denormalize($data); + + return $serializer; + } + private string $encoded; /** @var mixed */ @@ -44,14 +55,23 @@ public function getNormalized() return $this->normalized; } + public function getEncoded(): string + { + return $this->encoded; + } + private function deserialize(string $encoded): void { $this->encoded = $encoded; try { $this->deserialized = new SimpleXMLElement($encoded); - } catch (Exception $e) { - throw new SerializerException('Catched error "'.$e->getMessage().'" while decoding XML: '.$encoded, $e->getCode(), $e); + } catch (Throwable $e) { + throw new SerializerException( + 'Catched error "' . $e->getMessage() . '" while decoding XML: ' . $encoded, + $e->getCode(), + $e + ); } $this->normalize($this->deserialized); @@ -67,4 +87,117 @@ private function normalize(SimpleXMLElement $deserialized): void $this->normalized = JsonSerializer::createFromString($serialized)->getNormalized(); } + + private function denormalize(array $normalized): void + { + $this->normalized = $normalized; + + $rootElementName = array_key_first($this->normalized); + + try { + $this->deserialized = $this->createXmlElement($rootElementName, $this->normalized[$rootElementName]); + } catch (Throwable $e) { + throw new SerializerException( + 'Could not create XML from array: ' . $e->getMessage(), + $e->getCode(), + $e + ); + } + + $this->encoded = $this->deserialized->asXml(); + } + + private function createXmlElement(string $rootElementName, $params): SimpleXMLElement + { + $value = ''; + if (! is_array($params)) { + $value = $params; + } + + $xml = new SimpleXMLElement('<'.$rootElementName.'>'.$value.''); + + if (is_array($params)) { + foreach ($params as $k => $v) { + $this->addChildToXmlElement($xml, $k, $v); + } + } + + return $xml; + } + + private function addChildToXmlElement(SimpleXMLElement $xml, $k, $v): void + { + $specialParams = [ + 'enabled_module_names' => 'enabled_module_names', + 'issue_custom_field_ids' => 'issue_custom_field', + 'role_ids' => 'role_id', + 'tracker_ids' => 'tracker', + 'user_ids' => 'user_id', + 'watcher_user_ids' => 'watcher_user_id', + ]; + + if ('custom_fields' === $k && is_array($v)) { + $this->attachCustomFieldXML($xml, $v, 'custom_fields', 'custom_field'); + } elseif ('uploads' === $k && is_array($v)) { + $uploadsItem = $xml->addChild('uploads', ''); + $uploadsItem->addAttribute('type', 'array'); + foreach ($v as $upload) { + $upload_item = $uploadsItem->addChild('upload', ''); + foreach ($upload as $upload_k => $upload_v) { + $upload_item->addChild($upload_k, $upload_v); + } + } + } elseif (isset($specialParams[$k]) && is_array($v)) { + $array = $xml->addChild($k, ''); + $array->addAttribute('type', 'array'); + foreach ($v as $id) { + $array->addChild($specialParams[$k], $id); + } + } else { + $xml->$k = $v; + } + } + + /** + * Attaches Custom Fields to XML element. + * + * @param SimpleXMLElement $xml XML Element the custom fields are attached to + * @param array $fields array of fields to attach, each field needs name, id and value set + * + * @see http://www.redmine.org/projects/redmine/wiki/Rest_api#Working-with-custom-fields + */ + private function attachCustomFieldXML(SimpleXMLElement $xml, array $fields, string $fieldsName, string $fieldName): void + { + $_fields = $xml->addChild($fieldsName); + $_fields->addAttribute('type', 'array'); + foreach ($fields as $field) { + $_field = $_fields->addChild($fieldName); + + if (isset($field['name'])) { + $_field->addAttribute('name', $field['name']); + } + if (isset($field['field_format'])) { + $_field->addAttribute('field_format', $field['field_format']); + } + if (isset($field['id'])) { + $_field->addAttribute('id', $field['id']); + } + if (array_key_exists('value', $field) && is_array($field['value'])) { + $_field->addAttribute('multiple', 'true'); + $_values = $_field->addChild('value'); + if (array_key_exists('token', $field['value'])) { + foreach ($field['value'] as $key => $val) { + $_values->addChild($key, $val); + } + } else { + $_values->addAttribute('type', 'array'); + foreach ($field['value'] as $val) { + $_values->addChild('value', $val); + } + } + } else { + $_field->value = $field['value']; + } + } + } } diff --git a/tests/Integration/GroupXmlTest.php b/tests/Integration/GroupXmlTest.php index 7070050a..bf85f5e7 100644 --- a/tests/Integration/GroupXmlTest.php +++ b/tests/Integration/GroupXmlTest.php @@ -43,7 +43,7 @@ public function testCreateComplex() $xml = ' Developers - + 3 5 diff --git a/tests/Unit/Api/IssueTest.php b/tests/Unit/Api/IssueTest.php index c35465b1..aa94b1c3 100644 --- a/tests/Unit/Api/IssueTest.php +++ b/tests/Unit/Api/IssueTest.php @@ -320,7 +320,7 @@ public function testAddWatcherCallsPost() ->method('requestPost') ->with( $this->stringStartsWith('/issues/5/watchers.xml'), - $this->stringEndsWith('10') + $this->stringEndsWith('10'."\n") ) ->willReturn(true); $client->expects($this->exactly(1)) diff --git a/tests/Unit/Serializer/JsonSerializerTest.php b/tests/Unit/Serializer/JsonSerializerTest.php index 529a5352..54b25d3a 100644 --- a/tests/Unit/Serializer/JsonSerializerTest.php +++ b/tests/Unit/Serializer/JsonSerializerTest.php @@ -10,7 +10,7 @@ class JsonSerializerTest extends TestCase { - public function getNormalizedAndEncodedData() + public function getEncodedToNormalizedData() { return [ [ @@ -59,7 +59,7 @@ public function getNormalizedAndEncodedData() /** * @test * - * @dataProvider getNormalizedAndEncodedData + * @dataProvider getEncodedToNormalizedData */ public function createFromStringDecodesToExpectedNormalizedData(string $data, $expected) { @@ -71,8 +71,14 @@ public function createFromStringDecodesToExpectedNormalizedData(string $data, $e public function getInvalidEncodedData() { return [ - [''], - ['["foo":"bar"]'], + [ + 'Catched error "Syntax error" while decoding JSON: ', + '', + ], + [ + 'Catched error "Syntax error" while decoding JSON: ["foo":"bar"]', + '["foo":"bar"]', + ], ]; } @@ -81,10 +87,134 @@ public function getInvalidEncodedData() * * @dataProvider getInvalidEncodedData */ - public function createFromStringWithInvalidStringThrowsException(string $data) + public function createFromStringWithInvalidStringThrowsException(string $message, string $data) { $this->expectException(SerializerException::class); + $this->expectExceptionMessage($message); $serializer = JsonSerializer::createFromString($data); } + + public function getNormalizedToEncodedData() + { + return [ + [ + [ + 'issue' => [ + 'project_id' => 1, + 'subject' => 'Example', + 'priority_id' => 4, + ], + ], + <<< JSON + { + "issue": { + "project_id": 1, + "subject": "Example", + "priority_id": 4 + } + } + JSON, + ], + [ + [ + 'issue' => [ + 'project_id' => 1, + 'subject' => 'Example', + 'priority_id' => 4, + ], + 'ignored' => [ + 'only the first element of the array will be used', + ], + ], + <<< JSON + { + "issue": { + "project_id": 1, + "subject": "Example", + "priority_id": 4 + }, + "ignored": [ + "only the first element of the array will be used" + ] + } + JSON, + ], + [ + [ + 'project' => [ + 'name' => 'some name', + 'identifier' => 'the_identifier', + 'custom_fields' => [ + [ + 'id' => 123, + 'name' => 'cf_name', + 'field_format' => 'string', + 'value' => [1, 2, 3], + ], + ], + ], + ], + <<< JSON + { + "project": { + "name": "some name", + "identifier": "the_identifier", + "custom_fields": [ + { + "id": 123, + "name": "cf_name", + "field_format": "string", + "value": [ + 1, + 2, + 3 + ] + } + ] + } + } + JSON, + ], + ]; + } + + /** + * @test + * + * @dataProvider getNormalizedToEncodedData + */ + public function createFromArrayDecodesToExpectedString(array $data, $expected) + { + $serializer = JsonSerializer::createFromArray($data); + + // decode the result, so we encode again with JSON_PRETTY_PRINT to compare the formated output + $encoded = json_encode( + json_decode($serializer->getEncoded(), true, 512, \JSON_THROW_ON_ERROR), + \JSON_THROW_ON_ERROR | \JSON_PRETTY_PRINT + ); + + $this->assertSame($expected, $encoded); + } + + public function getInvalidSerializedData() + { + yield [ + 'Could not encode JSON from array: Type is not supported', + [fopen('php://temp', 'r+')], + ]; + } + + /** + * @test + * + * @dataProvider getInvalidSerializedData + */ + public function createFromArrayWithInvalidDataThrowsException(string $message, array $data) + { + $this->expectException(SerializerException::class); + $this->expectExceptionMessage($message); + + $serializer = JsonSerializer::createFromArray($data); + } } diff --git a/tests/Unit/Serializer/XmlSerializerTest.php b/tests/Unit/Serializer/XmlSerializerTest.php index 944d2a09..e26fbd54 100644 --- a/tests/Unit/Serializer/XmlSerializerTest.php +++ b/tests/Unit/Serializer/XmlSerializerTest.php @@ -10,7 +10,7 @@ class XmlSerializerTest extends TestCase { - public function getNormalizedAndEncodedData() + public function getEncodedToNormalizedData() { return [ [ @@ -30,7 +30,7 @@ public function getNormalizedAndEncodedData() ['1'], ], [ - <<< END + <<< XML @@ -40,7 +40,7 @@ public function getNormalizedAndEncodedData() 4325 - END, + XML, [ '@attributes' => [ 'type' => 'array', @@ -62,7 +62,7 @@ public function getNormalizedAndEncodedData() /** * @test * - * @dataProvider getNormalizedAndEncodedData + * @dataProvider getEncodedToNormalizedData */ public function createFromStringDecodesToExpectedNormalizedData(string $data, $expected) { @@ -74,11 +74,26 @@ public function createFromStringDecodesToExpectedNormalizedData(string $data, $e public function getInvalidEncodedData() { return [ - [''], - [''], - ['<>'], - [''], - [''], + [ + 'Catched error "String could not be parsed as XML" while decoding XML: ', + '', + ], + [ + 'Catched error "String could not be parsed as XML" while decoding XML: ', + '', + ], + [ + 'Catched error "String could not be parsed as XML" while decoding XML: <>', + '<>', + ], + [ + 'Catched error "String could not be parsed as XML" while decoding XML: ', + '', + ], + [ + 'Catched error "String could not be parsed as XML" while decoding XML: ', + '', + ], ]; } @@ -87,10 +102,136 @@ public function getInvalidEncodedData() * * @dataProvider getInvalidEncodedData */ - public function createFromStringWithInvalidStringThrowsException(string $data) + public function createFromStringWithInvalidStringThrowsException(string $message, string $data) { $this->expectException(SerializerException::class); + $this->expectExceptionMessage($message); $serializer = XmlSerializer::createFromString($data); } + + public function getNormalizedToEncodedData() + { + return [ + [ + [ + 'issue' => [ + 'project_id' => 1, + 'subject' => 'Example', + 'priority_id' => 4, + ], + ], + <<< XML + + + 1 + Example + 4 + + XML, + ], + [ + [ + 'issue' => [ + 'project_id' => 1, + 'subject' => 'Example', + 'priority_id' => 4, + ], + 'ignored' => [ + 'only the first element of the array will be used', + ], + ], + <<< XML + + + 1 + Example + 4 + + XML, + ], + [ + [ + 'project' => [ + 'name' => 'some name', + 'identifier' => 'the_identifier', + 'custom_fields' => [ + [ + 'id' => 123, + 'name' => 'cf_name', + 'field_format' => 'string', + 'value' => [1, 2, 3], + ], + ], + ], + ], + <<< XML + + + some name + the_identifier + + + + 1 + 2 + 3 + + + + + XML, + ], + ]; + } + + /** + * @test + * + * @dataProvider getNormalizedToEncodedData + */ + public function createFromArrayDecodesToExpectedString(array $data, $expected) + { + $serializer = XmlSerializer::createFromArray($data); + + // Load the encoded string into a DOMDocument, so we can compare the formated output + $dom = dom_import_simplexml(new \SimpleXMLElement($serializer->getEncoded()))->ownerDocument; + $dom->formatOutput = true; + + $this->assertSame($expected, trim($dom->saveXML())); + } + + public function getInvalidSerializedData() + { + if (version_compare(\PHP_VERSION, '8.0.0', '<')) { + // old Exception message for PHP 7.4 + yield [ + 'Could not create XML from array: Undefined index: ', + [], + ]; + } else { + // new Exeption message for PHP 8.0 + yield [ + 'Could not create XML from array: Undefined array key ""', + [], + ]; + } + yield [ + 'Could not create XML from array: String could not be parsed as XML', + ['0' => ['foobar']], + ]; + } + + /** + * @test + * + * @dataProvider getInvalidSerializedData + */ + public function createFromArrayWithInvalidDataThrowsException(string $message, array $data) + { + $this->expectException(SerializerException::class); + $this->expectExceptionMessage($message); + + $serializer = XmlSerializer::createFromArray($data); + } }