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.''.$rootElementName.'>');
+
+ 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);
+ }
}