Skip to content

Commit 7fe432a

Browse files
authored
Fix #36: Remove serializer usage, rename variable to DataResponseFormatterInterface (#48)
1 parent 19c136e commit 7fe432a

5 files changed

+381
-91
lines changed

composer.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
"psr/http-message": "^1.0",
2424
"psr/http-server-middleware": "^1.0",
2525
"yiisoft/http": "^1.0",
26-
"yiisoft/serializer": "^3.0@dev",
26+
"yiisoft/json": "^1.0",
2727
"yiisoft/strings": "^2.0"
2828
},
2929
"require-dev": {

src/DataResponseFormatterInterface.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,5 @@
88

99
interface DataResponseFormatterInterface
1010
{
11-
public function format(DataResponse $response): ResponseInterface;
11+
public function format(DataResponse $dataResponse): ResponseInterface;
1212
}

src/Formatter/JsonDataResponseFormatter.php

+11-5
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
use Yiisoft\DataResponse\DataResponseFormatterInterface;
1010
use Yiisoft\DataResponse\HasContentTypeTrait;
1111
use Yiisoft\Http\Header;
12-
use Yiisoft\Serializer\JsonSerializer;
12+
use Yiisoft\Json\Json;
1313

1414
final class JsonDataResponseFormatter implements DataResponseFormatterInterface
1515
{
@@ -24,18 +24,24 @@ final class JsonDataResponseFormatter implements DataResponseFormatterInterface
2424

2525
public function format(DataResponse $dataResponse): ResponseInterface
2626
{
27-
$content = '';
28-
$jsonSerializer = new JsonSerializer($this->options);
2927
if ($dataResponse->hasData()) {
30-
$content = $jsonSerializer->serialize($dataResponse->getData());
28+
$content = Json::encode($dataResponse->getData(), $this->options);
3129
}
3230

3331
$response = $dataResponse->getResponse();
34-
$response->getBody()->write($content);
32+
$response->getBody()->write($content ?? '');
3533

3634
return $response->withHeader(Header::CONTENT_TYPE, $this->contentType);
3735
}
3836

37+
/**
38+
* Returns a new instance with the specified encoding options.
39+
*
40+
* @param int $options The encoding options. For more details please refer to
41+
* {@see https://www.php.net/manual/en/function.json-encode.php}.
42+
*
43+
* @return self
44+
*/
3945
public function withOptions(int $options): self
4046
{
4147
$new = clone $this;

src/Formatter/XmlDataResponseFormatter.php

+184-28
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,34 @@
44

55
namespace Yiisoft\DataResponse\Formatter;
66

7-
use function is_array;
8-
use function is_float;
9-
use function is_scalar;
7+
use DOMDocument;
8+
use DOMElement;
9+
use DOMException;
10+
use DOMText;
1011
use Psr\Http\Message\ResponseInterface;
1112
use Traversable;
12-
use Yiisoft\DataResponse\DataResponse;
13-
use Yiisoft\DataResponse\DataResponseFormatterInterface;
1413
use Yiisoft\DataResponse\HasContentTypeTrait;
15-
1614
use Yiisoft\Http\Header;
17-
use Yiisoft\Serializer\XmlSerializer;
1815
use Yiisoft\Strings\NumericHelper;
16+
use Yiisoft\Strings\StringHelper;
17+
use Yiisoft\DataResponse\DataResponse;
18+
use Yiisoft\DataResponse\DataResponseFormatterInterface;
19+
20+
use function get_class;
21+
use function is_array;
22+
use function is_float;
23+
use function is_int;
24+
use function is_object;
25+
use function iterator_to_array;
26+
use function strpos;
1927

2028
final class XmlDataResponseFormatter implements DataResponseFormatterInterface
2129
{
2230
use HasContentTypeTrait;
2331

32+
private const DEFAULT_ITEM_TAG_NAME = 'item';
33+
private const KEY_ATTRIBUTE_NAME = 'key';
34+
2435
/**
2536
* @var string The Content-Type header for the response.
2637
*/
@@ -37,16 +48,31 @@ final class XmlDataResponseFormatter implements DataResponseFormatterInterface
3748
private string $encoding = 'UTF-8';
3849

3950
/**
40-
* @var string The name of the root element. If set to false, null or is empty then no root tag should be added.
51+
* @var string The name of the root element. If an empty value is set, the root tag should not be added.
4152
*/
4253
private string $rootTag = 'response';
4354

55+
/**
56+
* @var bool If true, the object tags will be formed from the class names,
57+
* otherwise the {@see DEFAULT_ITEM_TAG_NAME} value will be used.
58+
*/
59+
private bool $useObjectTags = true;
60+
4461
public function format(DataResponse $dataResponse): ResponseInterface
4562
{
46-
$serializer = new XmlSerializer($this->rootTag, $this->version, $this->encoding);
47-
4863
if ($dataResponse->hasData()) {
49-
$content = $serializer->serialize($this->formatData($dataResponse->getData()));
64+
$dom = new DOMDocument($this->version, $this->encoding);
65+
$data = $dataResponse->getData();
66+
67+
if (!empty($this->rootTag)) {
68+
$root = new DOMElement($this->rootTag);
69+
$dom->appendChild($root);
70+
$this->buildXml($dom, $root, $data);
71+
} else {
72+
$this->buildXml($dom, $dom, $data);
73+
}
74+
75+
$content = $dom->saveXML();
5076
}
5177

5278
$response = $dataResponse->getResponse();
@@ -55,20 +81,42 @@ public function format(DataResponse $dataResponse): ResponseInterface
5581
return $response->withHeader(Header::CONTENT_TYPE, $this->contentType . '; ' . $this->encoding);
5682
}
5783

84+
/**
85+
* Returns a new instance with the specified version.
86+
*
87+
* @param string $version The XML version. Default is "1.0".
88+
*
89+
* @return self
90+
*/
5891
public function withVersion(string $version): self
5992
{
6093
$new = clone $this;
6194
$new->version = $version;
6295
return $new;
6396
}
6497

98+
/**
99+
* Returns a new instance with the specified encoding.
100+
*
101+
* @param string $encoding The XML encoding. Default is "UTF-8".
102+
*
103+
* @return self
104+
*/
65105
public function withEncoding(string $encoding): self
66106
{
67107
$new = clone $this;
68108
$new->encoding = $encoding;
69109
return $new;
70110
}
71111

112+
/**
113+
* Returns a new instance with the specified root tag.
114+
*
115+
* @param string $rootTag The name of the root element. Default is "response".
116+
* If an empty value is set, the root tag should not be added.
117+
*
118+
* @return self
119+
*/
72120
public function withRootTag(string $rootTag): self
73121
{
74122
$new = clone $this;
@@ -77,41 +125,149 @@ public function withRootTag(string $rootTag): self
77125
}
78126

79127
/**
80-
* Pre-formats the data before serialization.
128+
* Returns a new instance with the specified value, whether to use class names as tags or not.
81129
*
82-
* @param mixed $data to format.
130+
* @param bool $useObjectTags If true, the object tags will be formed from the class names,
131+
* otherwise the {@see DEFAULT_ITEM_TAG_NAME} value will be used. Default is true.
83132
*
84-
* @return mixed formatted data.
133+
* @return self
85134
*/
86-
private function formatData($data)
135+
public function withUseObjectTags(bool $useObjectTags): self
87136
{
88-
if (is_scalar($data)) {
89-
return $this->formatScalarValue($data);
90-
}
137+
$new = clone $this;
138+
$new->useObjectTags = $useObjectTags;
139+
return $new;
140+
}
91141

92-
if (!is_array($data) && !($data instanceof Traversable)) {
93-
return $data;
142+
/**
143+
* Builds the data to use in XML.
144+
*
145+
* @param DOMDocument $dom The root DOM document.
146+
* @param DOMDocument|DOMElement $element The current DOM element being processed.
147+
* @param mixed $data Data for building XML.
148+
*/
149+
private function buildXml(DOMDocument $dom, $element, $data): void
150+
{
151+
if (empty($data)) {
152+
return;
94153
}
95154

96-
$formattedData = [];
155+
if (is_array($data) || $data instanceof Traversable) {
156+
$data = $data instanceof Traversable ? iterator_to_array($data) : $data;
157+
$dataSize = count($data);
158+
159+
foreach ($data as $name => $value) {
160+
if (is_int($name) && is_object($value) && !($value instanceof Traversable)) {
161+
$this->buildObject($dom, $element, $value, $dataSize > 1 ? $name : null);
162+
continue;
163+
}
164+
165+
$child = $this->safeCreateDomElement($dom, $name);
97166

98-
foreach ($data as $key => $value) {
99-
if (is_scalar($value)) {
100-
$formattedData[$key] = $this->formatScalarValue($value);
167+
if ($dataSize > 1 && is_int($name)) {
168+
$child->setAttribute(self::KEY_ATTRIBUTE_NAME, (string) $name);
169+
}
170+
171+
$element->appendChild($child);
172+
173+
if (is_array($value) || is_object($value)) {
174+
$this->buildXml($dom, $child, $value);
175+
continue;
176+
}
177+
178+
$this->setScalarValueToDomElement($child, $value);
101179
}
102180

103-
$formattedData[$key] = $this->formatData($value);
181+
return;
182+
}
183+
184+
if (is_object($data)) {
185+
$this->buildObject($dom, $element, $data);
186+
return;
187+
}
188+
189+
$this->setScalarValueToDomElement($element, $data);
190+
}
191+
192+
/**
193+
* Builds the object to use in XML.
194+
*
195+
* @param DOMDocument $dom The root DOM document.
196+
* @param DOMDocument|DOMElement $element The current DOM element being processed.
197+
* @param object $object To build.
198+
* @param int|null $key Key attribute value.
199+
*/
200+
private function buildObject(DOMDocument $dom, $element, object $object, int $key = null): void
201+
{
202+
if ($this->useObjectTags) {
203+
$class = get_class($object);
204+
$class = strpos($class, 'class@anonymous') === false ? StringHelper::baseName($class) : 'AnonymousClass';
205+
}
206+
207+
$child = $this->safeCreateDomElement($dom, $class ?? self::DEFAULT_ITEM_TAG_NAME);
208+
209+
if ($key !== null) {
210+
$child->setAttribute(self::KEY_ATTRIBUTE_NAME, (string) $key);
104211
}
105212

106-
return $formattedData;
213+
$element->appendChild($child);
214+
$array = [];
215+
216+
foreach ($object as $property => $value) {
217+
$array[$property] = $value;
218+
}
219+
220+
$this->buildXml($dom, $child, $array);
221+
}
222+
223+
/**
224+
* Safely creates a DOMElement instance by the specified tag name, if the tag name is not empty,
225+
* not integer, and valid. Otherwise the {@see DEFAULT_ITEM_TAG_NAME} value will be used.
226+
*
227+
* @see http://stackoverflow.com/questions/2519845/how-to-check-if-string-is-a-valid-xml-element-name/2519943#2519943
228+
*
229+
* @param DOMDocument $dom The root DOM document.
230+
* @param int|string|null $tagName The tag name.
231+
*
232+
* @return DOMElement
233+
*/
234+
private function safeCreateDomElement(DOMDocument $dom, $tagName): DOMElement
235+
{
236+
if (empty($tagName) || is_int($tagName)) {
237+
return $dom->createElement(self::DEFAULT_ITEM_TAG_NAME);
238+
}
239+
240+
try {
241+
if (!$element = $dom->createElement($tagName)) {
242+
throw new DOMException();
243+
}
244+
return $element;
245+
} catch (DOMException $e) {
246+
return $dom->createElement(self::DEFAULT_ITEM_TAG_NAME);
247+
}
248+
}
249+
250+
/**
251+
* Sets the scalar value to Dom Element instance if the value is not empty.
252+
*
253+
* @param DOMDocument|DOMElement $element The current DOM element being processed.
254+
* @param bool|float|int|string|null $value
255+
*/
256+
private function setScalarValueToDomElement($element, $value): void
257+
{
258+
$value = $this->formatScalarValue($value);
259+
260+
if ($value !== '') {
261+
$element->appendChild(new DOMText($value));
262+
}
107263
}
108264

109265
/**
110266
* Formats scalar value to use in XML node.
111267
*
112-
* @param bool|float|int|string $value to format.
268+
* @param bool|float|int|string|null $value To format.
113269
*
114-
* @return string string representation of the value.
270+
* @return string The string representation of the value.
115271
*/
116272
private function formatScalarValue($value): string
117273
{

0 commit comments

Comments
 (0)