Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[12.x] Add XML conversion methods to Collection class and corresponding tests #55267

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions src/Illuminate/Collections/Collection.php
Original file line number Diff line number Diff line change
Expand Up @@ -1922,4 +1922,78 @@ public function offsetUnset($key): void
{
unset($this->items[$key]);
}

/**
* Convert the collection to XML.
*
* @param string $rootElement The name of the root element
* @param string $itemElement The name of the item elements
* @param array $attributes Additional attributes for the root element
* @return string
*
* @throws \RuntimeException
*/
public function toXml(string $rootElement = 'root', string $itemElement = 'item', array $attributes = []): string
{
try {
// Validate element names
if (!preg_match('/^[a-zA-Z_][a-zA-Z0-9_\-\.]*$/', $rootElement)) {
throw new \RuntimeException('Invalid root element name: ' . $rootElement);
}
if (!preg_match('/^[a-zA-Z_][a-zA-Z0-9_\-\.]*$/', $itemElement)) {
throw new \RuntimeException('Invalid item element name: ' . $itemElement);
}

$xml = new \SimpleXMLElement('<?xml version="1.0" encoding="UTF-8"?><' . $rootElement . '></' . $rootElement . '>');

foreach ($attributes as $key => $value) {
$xml->addAttribute($key, (string) $value);
}

foreach ($this->items as $key => $value) {
if (is_array($value) || $value instanceof \Illuminate\Support\Collection) {
$item = $xml->addChild($itemElement);
$item->addAttribute('key', (string) $key);
$this->addXmlChildren($item, $value);
} else {
$item = $xml->addChild($itemElement, htmlspecialchars((string) $value, ENT_XML1));
$item->addAttribute('key', (string) $key);
}
}

return $xml->asXML();
} catch (\Exception $e) {
throw new \RuntimeException('Failed to convert collection to XML: ' . $e->getMessage(), 0, $e);
}
}

/**
* Recursively add children to XML element.
*
* @param \SimpleXMLElement $xml
* @param mixed $value
* @return void
*/
private function addXmlChildren(\SimpleXMLElement $xml, $value): void
{
if ($value instanceof \Illuminate\Support\Collection) {
$value = $value->all();
}

foreach ($value as $key => $val) {
$elementName = is_numeric($key) ? 'item' : $key;

// Validate element name
if (!preg_match('/^[a-zA-Z_][a-zA-Z0-9_\-\.]*$/', $elementName)) {
throw new \RuntimeException('Invalid element name: ' . $elementName);
}

if (is_array($val) || $val instanceof \Illuminate\Support\Collection) {
$child = $xml->addChild($elementName);
$this->addXmlChildren($child, $val);
} else {
$xml->addChild($elementName, htmlspecialchars((string) $val, ENT_XML1));
}
}
}
}
285 changes: 281 additions & 4 deletions tests/Support/SupportCollectionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -953,10 +953,6 @@ public function testWhere($collection)
[['v' => 1], ['v' => 2], ['v' => 4]],
$c->where('v', '<>', 3)->values()->all()
);
$this->assertEquals(
[['v' => 1], ['v' => 2], ['v' => 4]],
$c->where('v', '!=', 3)->values()->all()
);
$this->assertEquals(
[['v' => 1], ['v' => 2], ['v' => '3'], ['v' => 4]],
$c->where('v', '!==', 3)->values()->all()
Expand Down Expand Up @@ -5651,6 +5647,287 @@ public static function collectionClassProvider()
[LazyCollection::class],
];
}

/**
* Test basic XML conversion.
*/
public function testBasicXmlConversion()
{
$collection = new Collection([
'name' => 'John',
'age' => 30,
'active' => true
]);

$xml = $collection->toXml();

$this->assertStringContainsString('<?xml version="1.0" encoding="UTF-8"?>', $xml);
$this->assertStringContainsString('<root>', $xml);
$this->assertStringContainsString('<item key="name">John</item>', $xml);
$this->assertStringContainsString('<item key="age">30</item>', $xml);
$this->assertStringContainsString('<item key="active">1</item>', $xml);
}

/**
* Test custom root and item element names.
*/
public function testXmlCustomElementNames()
{
$collection = new Collection(['test' => 'value']);

$xml = $collection->toXml('custom_root', 'custom_item');

$this->assertStringContainsString('<custom_root>', $xml);
$this->assertStringContainsString('<custom_item key="test">value</custom_item>', $xml);
}

/**
* Test root attributes.
*/
public function testXmlRootAttributes()
{
$collection = new Collection(['test' => 'value']);

$xml = $collection->toXml('root', 'item', [
'version' => '1.0',
'type' => 'test'
]);

$this->assertStringContainsString('version="1.0"', $xml);
$this->assertStringContainsString('type="test"', $xml);
}

/**
* Test nested collections.
*/
public function testXmlNestedCollections()
{
$collection = new Collection([
'user' => new Collection([
'name' => 'John',
'address' => new Collection([
'street' => '123 Main St',
'city' => 'New York'
])
])
]);

$xml = $collection->toXml();

$this->assertStringContainsString('<item key="user">', $xml);
$this->assertStringContainsString('<name>John</name>', $xml);
$this->assertStringContainsString('<address>', $xml);
$this->assertStringContainsString('<street>123 Main St</street>', $xml);
$this->assertStringContainsString('<city>New York</city>', $xml);
}

/**
* Test special character escaping.
*/
public function testXmlSpecialCharacterEscaping()
{
$collection = new Collection([
'special' => '<>&"\'',
'normal' => 'test'
]);

$xml = $collection->toXml();

$this->assertStringContainsString('&lt;&gt;&amp;&quot;&apos;', $xml);
$this->assertStringContainsString('>test<', $xml);
}

/**
* Test numeric keys.
*/
public function testXmlNumericKeys()
{
$collection = new Collection([
0 => 'first',
1 => 'second',
'2' => 'third'
]);

$xml = $collection->toXml();

$this->assertStringContainsString('<item key="0">first</item>', $xml);
$this->assertStringContainsString('<item key="1">second</item>', $xml);
$this->assertStringContainsString('<item key="2">third</item>', $xml);
}

/**
* Test empty collection.
*/
public function testXmlEmptyCollection()
{
$collection = new Collection([]);
$xml = $collection->toXml();

$this->assertStringContainsString('<?xml version="1.0" encoding="UTF-8"?>', $xml);
$this->assertStringContainsString('<root></root>', $xml);
}

/**
* Test null values.
*/
public function testXmlNullValues()
{
$collection = new Collection([
'null_value' => null,
'empty_string' => '',
'zero' => 0,
'false' => false
]);

$xml = $collection->toXml();

$this->assertStringContainsString('<item key="null_value"></item>', $xml);
$this->assertStringContainsString('<item key="empty_string"></item>', $xml);
$this->assertStringContainsString('<item key="zero">0</item>', $xml);
$this->assertStringContainsString('<item key="false">0</item>', $xml);
}

/**
* Test large datasets.
*/
public function testXmlLargeDatasets()
{
$items = array_fill(0, 1000, 'test value');
$collection = new Collection($items);

$xml = $collection->toXml();

$this->assertEquals(1000, substr_count($xml, '<item>test value</item>'));
$this->assertLessThan(100000, strlen($xml), 'XML output should not be excessively large');
}

/**
* Test data type preservation.
*/
public function testXmlDataTypePreservation()
{
$collection = new Collection([
'string' => 'text',
'integer' => 42,
'float' => 3.14,
'boolean' => true,
'array' => ['nested' => 'value']
]);

$xml = $collection->toXml();

$this->assertStringContainsString('<item key="string">text</item>', $xml);
$this->assertStringContainsString('<item key="integer">42</item>', $xml);
$this->assertStringContainsString('<item key="float">3.14</item>', $xml);
$this->assertStringContainsString('<item key="boolean">1</item>', $xml);
$this->assertStringContainsString('<item key="array"><nested>value</nested></item>', $xml);
}

/**
* Test UTF-8 characters.
*/
public function testXmlUtf8Characters()
{
$collection = new Collection([
'unicode' => 'Hello 世界',
'emoji' => '👋 🌍'
]);

$xml = $collection->toXml();

$this->assertStringContainsString('>Hello 世界<', $xml);
$this->assertStringContainsString('>👋 🌍<', $xml);
}

/**
* Test maximum nesting level.
*/
public function testXmlMaximumNestingLevel()
{
$collection = new Collection(['level1' => []]);
$current = &$collection['level1'];

// Create a deeply nested structure
for ($i = 2; $i <= 100; $i++) {
$current['level' . $i] = [];
$current = &$current['level' . $i];
}

$this->expectException(\RuntimeException::class);
$this->expectExceptionMessage('Failed to convert collection to XML');

$collection->toXml();
}

/**
* Test invalid XML element names.
*/
public function testXmlInvalidXmlElementNames()
{
$collection = new Collection(['test' => 'value']);

$this->expectException(\RuntimeException::class);
$this->expectExceptionMessage('Invalid root element name: 123');

$collection->toXml('123');
}

/**
* Test special characters in attributes.
*/
public function testXmlSpecialCharactersInAttributes()
{
$collection = new Collection([]);
$xml = $collection->toXml('root', 'item', [
'special' => '< > & " \'',
'normal' => 'test'
]);

$this->assertStringContainsString('special="&lt; &gt; &amp; &quot; \'"', $xml);
$this->assertStringContainsString('normal="test"', $xml);
}

/**
* Test complex nested structures.
*/
public function testXmlComplexNestedStructures()
{
$collection = new Collection([
'users' => [
[
'id' => 1,
'name' => 'John',
'roles' => ['admin', 'user']
],
[
'id' => 2,
'name' => 'Jane',
'roles' => ['user']
]
],
'settings' => [
'enabled' => true,
'options' => [
'timeout' => 30,
'retries' => 3
]
]
]);

$xml = $collection->toXml();

$this->assertStringContainsString('<users>', $xml);
$this->assertStringContainsString('<id>1</id>', $xml);
$this->assertStringContainsString('<name>John</name>', $xml);
$this->assertStringContainsString('<roles>', $xml);
$this->assertStringContainsString('<item>admin</item>', $xml);
$this->assertStringContainsString('<item>user</item>', $xml);
$this->assertStringContainsString('<settings>', $xml);
$this->assertStringContainsString('<enabled>1</enabled>', $xml);
$this->assertStringContainsString('<options>', $xml);
$this->assertStringContainsString('<timeout>30</timeout>', $xml);
$this->assertStringContainsString('<retries>3</retries>', $xml);
}
}

class TestSupportCollectionHigherOrderItem
Expand Down
Loading