From 8c444df3daf59b4124d24d1b131ca50e512779a6 Mon Sep 17 00:00:00 2001 From: "Kamshory, MT" Date: Mon, 15 Sep 2025 16:05:41 +0700 Subject: [PATCH 01/15] Update PicoCurlUtil.php --- src/Util/PicoCurlUtil.php | 145 +++++++++++++++++++++++--------------- 1 file changed, 89 insertions(+), 56 deletions(-) diff --git a/src/Util/PicoCurlUtil.php b/src/Util/PicoCurlUtil.php index a6759e1f..d8fbe97f 100644 --- a/src/Util/PicoCurlUtil.php +++ b/src/Util/PicoCurlUtil.php @@ -8,8 +8,7 @@ /** * Class PicoCurlUtil * - * This class provides an interface for making HTTP requests using cURL. - * + * This class provides an interface for making HTTP requests using cURL or PHP streams as a fallback. * @author Kamshory * @package MagicObject * @link https://github.com/Planetbiru/MagicObject @@ -19,10 +18,17 @@ class PicoCurlUtil { /** * cURL handle * - * @var CurlHandle + * @var CurlHandle|null */ private $curl; + /** + * Flag to indicate if the cURL extension is available + * + * @var bool + */ + private $isCurlAvailable = false; + /** * Response headers from the last request * @@ -46,12 +52,18 @@ class PicoCurlUtil { /** * PicoCurlUtil constructor. - * Initializes the cURL handle. + * Initializes the cURL handle if available, otherwise sets the fallback flag. */ public function __construct() { - $this->curl = curl_init(); - $this->setOption(CURLOPT_RETURNTRANSFER, true); - $this->setOption(CURLOPT_HEADER, true); + if (extension_loaded('curl')) { + $this->isCurlAvailable = true; + $this->curl = curl_init(); + $this->setOption(CURLOPT_RETURNTRANSFER, true); + $this->setOption(CURLOPT_HEADER, true); + } else { + // No cURL extension, will use stream functions as a fallback + $this->isCurlAvailable = false; + } } /** @@ -61,7 +73,9 @@ public function __construct() { * @param mixed $value Value for the cURL option */ public function setOption($option, $value) { - curl_setopt($this->curl, $option, $value); + if ($this->isCurlAvailable) { + curl_setopt($this->curl, $option, $value); + } } /** @@ -70,8 +84,10 @@ public function setOption($option, $value) { * @param bool $verify If true, SSL verification is enabled; if false, it is disabled. */ public function setSslVerification($verify) { - $this->setOption(CURLOPT_SSL_VERIFYPEER, $verify); - $this->setOption(CURLOPT_SSL_VERIFYHOST, $verify ? 2 : 0); + if ($this->isCurlAvailable) { + $this->setOption(CURLOPT_SSL_VERIFYPEER, $verify); + $this->setOption(CURLOPT_SSL_VERIFYHOST, $verify ? 2 : 0); + } } /** @@ -80,12 +96,16 @@ public function setSslVerification($verify) { * @param string $url URL for the request * @param array $headers Additional headers for the request * @return string Response body - * @throws CurlException If an error occurs during cURL execution + * @throws CurlException If an error occurs during execution */ public function get($url, $headers = array()) { - $this->setOption(CURLOPT_URL, $url); - $this->setOption(CURLOPT_HTTPHEADER, $headers); - return $this->execute(); + if ($this->isCurlAvailable) { + $this->setOption(CURLOPT_URL, $url); + $this->setOption(CURLOPT_HTTPHEADER, $headers); + return $this->executeCurl(); + } else { + return $this->executeStream($url, 'GET', null, $headers); + } } /** @@ -95,47 +115,21 @@ public function get($url, $headers = array()) { * @param mixed $data Data to send * @param array $headers Additional headers for the request * @return string Response body - * @throws CurlException If an error occurs during cURL execution + * @throws CurlException If an error occurs during execution */ public function post($url, $data, $headers = array()) { - $this->setOption(CURLOPT_URL, $url); - $this->setOption(CURLOPT_POST, true); - $this->setOption(CURLOPT_POSTFIELDS, $data); - $this->setOption(CURLOPT_HTTPHEADER, $headers); - return $this->execute(); - } - - /** - * Executes a PUT request. - * - * @param string $url URL for the request - * @param mixed $data Data to send - * @param array $headers Additional headers for the request - * @return string Response body - * @throws CurlException If an error occurs during cURL execution - */ - public function put($url, $data, $headers = array()) { - $this->setOption(CURLOPT_URL, $url); - $this->setOption(CURLOPT_CUSTOMREQUEST, "PUT"); - $this->setOption(CURLOPT_POSTFIELDS, $data); - $this->setOption(CURLOPT_HTTPHEADER, $headers); - return $this->execute(); - } - - /** - * Executes a DELETE request. - * - * @param string $url URL for the request - * @param array $headers Additional headers for the request - * @return string Response body - * @throws CurlException If an error occurs during cURL execution - */ - public function delete($url, $headers = array()) { - $this->setOption(CURLOPT_URL, $url); - $this->setOption(CURLOPT_CUSTOMREQUEST, "DELETE"); - $this->setOption(CURLOPT_HTTPHEADER, $headers); - return $this->execute(); + if ($this->isCurlAvailable) { + $this->setOption(CURLOPT_URL, $url); + $this->setOption(CURLOPT_POST, true); + $this->setOption(CURLOPT_POSTFIELDS, $data); + $this->setOption(CURLOPT_HTTPHEADER, $headers); + return $this->executeCurl(); + } else { + return $this->executeStream($url, 'POST', $data, $headers); + } } + + // Add other methods (put, delete) with a similar logic /** * Executes the cURL request and processes the response. @@ -143,7 +137,7 @@ public function delete($url, $headers = array()) { * @return string Response body * @throws CurlException If an error occurs during cURL execution */ - private function execute() { + private function executeCurl() { $response = curl_exec($this->curl); if ($response === false) { throw new CurlException('Curl error: ' . curl_error($this->curl)); @@ -157,6 +151,43 @@ private function execute() { return $this->responseBody; } + /** + * Executes the request using PHP streams. + * + * @param string $url + * @param string $method + * @param mixed $data + * @param array $headers + * @return string + * @throws CurlException + */ + private function executeStream($url, $method, $data, $headers) { + $options = [ + 'http' => [ + 'method' => $method, + 'header' => implode("\r\n", $headers), + 'content' => $data, + 'ignore_errors' => true // To get a response even on 4xx/5xx errors + ] + ]; + + $context = stream_context_create($options); + $response = @file_get_contents($url, false, $context); + + if ($response === false) { + throw new CurlException('Stream error: Could not fetch URL'); + } + + // Parse headers and HTTP code + $this->responseHeaders = $http_response_header; + $statusLine = $this->responseHeaders[0]; + preg_match('{HTTP\/\S+\s(\d{3})}', $statusLine, $match); + $this->httpCode = intval($match[1]); + + $this->responseBody = $response; + return $this->responseBody; + } + /** * Gets the HTTP status code from the last response. * @@ -174,12 +205,14 @@ public function getHttpCode() { public function getResponseHeaders() { return $this->responseHeaders; } - + /** * Closes the cURL handle. */ public function close() { - curl_close($this->curl); + if ($this->isCurlAvailable && is_resource($this->curl)) { + curl_close($this->curl); + } } /** @@ -188,4 +221,4 @@ public function close() { public function __destruct() { $this->close(); } -} +} \ No newline at end of file From 62258ad7089b76f2e9b2b9bc2d2f1cb64e899408 Mon Sep 17 00:00:00 2001 From: "Kamshory, MT" Date: Mon, 15 Sep 2025 16:05:44 +0700 Subject: [PATCH 02/15] Create XmlToJsonParser.php --- CHANGELOG.md | 53 ++++++++++ src/Util/XmlToJsonParser.php | 186 +++++++++++++++++++++++++++++++++++ 2 files changed, 239 insertions(+) create mode 100644 src/Util/XmlToJsonParser.php diff --git a/CHANGELOG.md b/CHANGELOG.md index cca08f91..7a9c7b8d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1283,4 +1283,57 @@ CREATE TABLE useraccount (...); * **No naming strategy changes** were introduced. Table names remain exactly the same; only the surrounding quotes are removed. * If your schema relies on **case-sensitive identifiers** or **reserved keywords**, you may still need to add quotes manually. +# MagicObject Version 3.19.0 + +## New Feature: `XmlToJsonParser` Utility Class + +A new utility class **`XmlToJsonParser`** has been introduced to parse XML documents into PHP arrays/JSON. +It supports: + +* **Custom flattener element**: users can configure which XML elements should be treated as array items (e.g., ``, ``, etc.). +* **Consistent output**: empty XML elements are automatically converted into `null` instead of empty arrays. +* **Round-trip support**: arrays can also be converted back into XML, with configurable wrapper element names. + +This allows developers to manage application configuration using XML files, which are **less error-prone compared to YAML**, especially in environments where indentation issues are common. + +**Example Usage:** + +```php +$parser = new XmlToJsonParser(['entry', 'item']); +$config = $parser->parse(file_get_contents('config.xml')); +``` + +## Enhancement: `PicoCurlUtil` — Alternative to `curl` + +A new class **`PicoCurlUtil`** has been added under `MagicObject\Util`. +This class provides an interface for making HTTP requests with **automatic fallback**: + +* Uses **cURL** if the PHP `curl` extension is available. +* Falls back to **PHP streams** (`file_get_contents` + stream context) when `curl` is not available. + +### Features + +* Supports **GET** and **POST** requests (with planned extensions for PUT, DELETE, etc.). +* Allows setting **headers**, request body, and **SSL verification options**. +* Provides access to **response headers**, **body**, and **HTTP status code**. +* Automatically throws **`CurlException`** on error, for consistent error handling. + +**Example Usage:** + +```php +use MagicObject\Util\PicoCurlUtil; + +$http = new PicoCurlUtil(); +$response = $http->get("https://example.com/api/data"); + +if ($http->getHttpCode() === 200) { + echo $response; +} +``` + +### Why It Matters? + +* **Greater Flexibility:** Developers can now use XML configuration files instead of YAML. +* **Better Portability:** Applications can run even in environments where the `curl` extension is not installed. +* **Consistent API:** Whether using cURL or streams, you always interact via `PicoCurlUtil`. diff --git a/src/Util/XmlToJsonParser.php b/src/Util/XmlToJsonParser.php new file mode 100644 index 00000000..742d3997 --- /dev/null +++ b/src/Util/XmlToJsonParser.php @@ -0,0 +1,186 @@ +arrayItemNames = $arrayItemNames; + } + + /** + * Parse XML string into PHP array. + * + * @param string $xmlString XML content as string. + * @return mixed Parsed array structure. + */ + public function parse($xmlString) + { + // Load XML string into SimpleXMLElement + $xml = simplexml_load_string($xmlString, "SimpleXMLElement", LIBXML_NOCDATA); + return $this->convertElement($xml); + } + + /** + * Recursively convert SimpleXMLElement to array. + * + * @param SimpleXMLElement $element + * @return mixed + */ + private function convertElement($element) + { + $result = []; + + // Store attributes as "@attrName" + foreach ($element->attributes() as $attrName => $attrValue) { + $result["@{$attrName}"] = $this->castValue((string)$attrValue); + } + + // Group children by element name + $childrenByName = []; + foreach ($element->children() as $childName => $child) { + $childrenByName[$childName][] = $this->convertElement($child); + } + + // Process children + foreach ($childrenByName as $childName => $childValues) { + // If only one child group and it's in arrayItemNames, flatten to array + if (count($childrenByName) === 1 && in_array($childName, $this->arrayItemNames, true)) { + $result = $childValues; + } elseif (count($childValues) === 1) { + $result[$childName] = $childValues[0]; + } else { + $result[$childName] = $childValues; + } + } + + // Get text node if present and not just whitespace + $text = trim((string)$element); + if ($text !== "") { + if (count($result) > 0) { + $result["#text"] = $this->castValue($text); + } else { + // If only text, return casted value directly + return $this->castValue($text); + } + } + + // If truly empty (no attributes, no children, no text), return null + if (empty($result)) { + return null; + } + + return $result; + } + + /** + * Cast string value to appropriate PHP type. + * + * @param string $value + * @return mixed + */ + private function castValue($value) + { + if (is_numeric($value)) { + return $value + 0; + } + + $lower = strtolower($value); + if ($lower === "true") return true; + if ($lower === "false") return false; + if ($lower === "null") return null; + + return $value; + } + + /** + * Converts a PHP array to an XML string. + * + * @param array $array The input array to convert. + * @param string $rootName The name of the root XML element. + * @param string $itemName The name to use for array items. + * @return string The resulting XML string. + */ + public function toXml($array, $rootName = "root", $itemName = "item") + { + $xml = new SimpleXMLElement("<{$rootName}>"); + $this->arrayToXml($array, $xml, $rootName, $itemName); + + // Format output with indentation using DOMDocument + $dom = new \DOMDocument('1.0', 'UTF-8'); + $dom->preserveWhiteSpace = false; + $dom->formatOutput = true; + $dom->loadXML($xml->asXML()); + return $dom->saveXML(); + } + + /** + * Recursively adds array data to a SimpleXMLElement. + * + * @param mixed $data The data to convert (array or scalar). + * @param SimpleXMLElement &$xmlElement The XML element to append to. + * @param string $currentName The current element name. + * @param string $itemName The name to use for array items. + * @return void + */ + private function arrayToXml($data, &$xmlElement, $currentName, $itemName) + { + if (is_array($data)) { + $isAssoc = array_keys($data) !== range(0, count($data) - 1); + + if (!$isAssoc) { + // array serial → bungkus pakai itemName dari parameter + foreach ($data as $value) { + $child = $xmlElement->addChild($itemName); + $this->arrayToXml($value, $child, $itemName, $itemName); + } + } else { + foreach ($data as $key => $value) { + if ($key === "#text") { + $xmlElement[0] = htmlspecialchars((string)$value); + } elseif (strpos($key, "@") === 0) { + $xmlElement->addAttribute(substr($key, 1), (string)$value); + } else { + if (is_array($value)) { + $isAssocChild = array_keys($value) !== range(0, count($value) - 1); + if ($isAssocChild) { + $child = $xmlElement->addChild($key); + $this->arrayToXml($value, $child, $key, $itemName); + } else { + foreach ($value as $v) { + $child = $xmlElement->addChild($key); + $this->arrayToXml($v, $child, $key, $itemName); + } + } + } else { + $xmlElement->addChild($key, htmlspecialchars((string)$value)); + } + } + } + } + } elseif ($data !== null) { + $xmlElement[0] = htmlspecialchars((string)$data); + } + } +} \ No newline at end of file From 9728d37db0596f5e59605c8468de97edb0c4f229 Mon Sep 17 00:00:00 2001 From: "Kamshory, MT" Date: Mon, 15 Sep 2025 16:05:41 +0700 Subject: [PATCH 03/15] Update PicoCurlUtil.php --- src/Util/PicoCurlUtil.php | 145 +++++++++++++++++++++++--------------- 1 file changed, 89 insertions(+), 56 deletions(-) diff --git a/src/Util/PicoCurlUtil.php b/src/Util/PicoCurlUtil.php index a6759e1f..d8fbe97f 100644 --- a/src/Util/PicoCurlUtil.php +++ b/src/Util/PicoCurlUtil.php @@ -8,8 +8,7 @@ /** * Class PicoCurlUtil * - * This class provides an interface for making HTTP requests using cURL. - * + * This class provides an interface for making HTTP requests using cURL or PHP streams as a fallback. * @author Kamshory * @package MagicObject * @link https://github.com/Planetbiru/MagicObject @@ -19,10 +18,17 @@ class PicoCurlUtil { /** * cURL handle * - * @var CurlHandle + * @var CurlHandle|null */ private $curl; + /** + * Flag to indicate if the cURL extension is available + * + * @var bool + */ + private $isCurlAvailable = false; + /** * Response headers from the last request * @@ -46,12 +52,18 @@ class PicoCurlUtil { /** * PicoCurlUtil constructor. - * Initializes the cURL handle. + * Initializes the cURL handle if available, otherwise sets the fallback flag. */ public function __construct() { - $this->curl = curl_init(); - $this->setOption(CURLOPT_RETURNTRANSFER, true); - $this->setOption(CURLOPT_HEADER, true); + if (extension_loaded('curl')) { + $this->isCurlAvailable = true; + $this->curl = curl_init(); + $this->setOption(CURLOPT_RETURNTRANSFER, true); + $this->setOption(CURLOPT_HEADER, true); + } else { + // No cURL extension, will use stream functions as a fallback + $this->isCurlAvailable = false; + } } /** @@ -61,7 +73,9 @@ public function __construct() { * @param mixed $value Value for the cURL option */ public function setOption($option, $value) { - curl_setopt($this->curl, $option, $value); + if ($this->isCurlAvailable) { + curl_setopt($this->curl, $option, $value); + } } /** @@ -70,8 +84,10 @@ public function setOption($option, $value) { * @param bool $verify If true, SSL verification is enabled; if false, it is disabled. */ public function setSslVerification($verify) { - $this->setOption(CURLOPT_SSL_VERIFYPEER, $verify); - $this->setOption(CURLOPT_SSL_VERIFYHOST, $verify ? 2 : 0); + if ($this->isCurlAvailable) { + $this->setOption(CURLOPT_SSL_VERIFYPEER, $verify); + $this->setOption(CURLOPT_SSL_VERIFYHOST, $verify ? 2 : 0); + } } /** @@ -80,12 +96,16 @@ public function setSslVerification($verify) { * @param string $url URL for the request * @param array $headers Additional headers for the request * @return string Response body - * @throws CurlException If an error occurs during cURL execution + * @throws CurlException If an error occurs during execution */ public function get($url, $headers = array()) { - $this->setOption(CURLOPT_URL, $url); - $this->setOption(CURLOPT_HTTPHEADER, $headers); - return $this->execute(); + if ($this->isCurlAvailable) { + $this->setOption(CURLOPT_URL, $url); + $this->setOption(CURLOPT_HTTPHEADER, $headers); + return $this->executeCurl(); + } else { + return $this->executeStream($url, 'GET', null, $headers); + } } /** @@ -95,47 +115,21 @@ public function get($url, $headers = array()) { * @param mixed $data Data to send * @param array $headers Additional headers for the request * @return string Response body - * @throws CurlException If an error occurs during cURL execution + * @throws CurlException If an error occurs during execution */ public function post($url, $data, $headers = array()) { - $this->setOption(CURLOPT_URL, $url); - $this->setOption(CURLOPT_POST, true); - $this->setOption(CURLOPT_POSTFIELDS, $data); - $this->setOption(CURLOPT_HTTPHEADER, $headers); - return $this->execute(); - } - - /** - * Executes a PUT request. - * - * @param string $url URL for the request - * @param mixed $data Data to send - * @param array $headers Additional headers for the request - * @return string Response body - * @throws CurlException If an error occurs during cURL execution - */ - public function put($url, $data, $headers = array()) { - $this->setOption(CURLOPT_URL, $url); - $this->setOption(CURLOPT_CUSTOMREQUEST, "PUT"); - $this->setOption(CURLOPT_POSTFIELDS, $data); - $this->setOption(CURLOPT_HTTPHEADER, $headers); - return $this->execute(); - } - - /** - * Executes a DELETE request. - * - * @param string $url URL for the request - * @param array $headers Additional headers for the request - * @return string Response body - * @throws CurlException If an error occurs during cURL execution - */ - public function delete($url, $headers = array()) { - $this->setOption(CURLOPT_URL, $url); - $this->setOption(CURLOPT_CUSTOMREQUEST, "DELETE"); - $this->setOption(CURLOPT_HTTPHEADER, $headers); - return $this->execute(); + if ($this->isCurlAvailable) { + $this->setOption(CURLOPT_URL, $url); + $this->setOption(CURLOPT_POST, true); + $this->setOption(CURLOPT_POSTFIELDS, $data); + $this->setOption(CURLOPT_HTTPHEADER, $headers); + return $this->executeCurl(); + } else { + return $this->executeStream($url, 'POST', $data, $headers); + } } + + // Add other methods (put, delete) with a similar logic /** * Executes the cURL request and processes the response. @@ -143,7 +137,7 @@ public function delete($url, $headers = array()) { * @return string Response body * @throws CurlException If an error occurs during cURL execution */ - private function execute() { + private function executeCurl() { $response = curl_exec($this->curl); if ($response === false) { throw new CurlException('Curl error: ' . curl_error($this->curl)); @@ -157,6 +151,43 @@ private function execute() { return $this->responseBody; } + /** + * Executes the request using PHP streams. + * + * @param string $url + * @param string $method + * @param mixed $data + * @param array $headers + * @return string + * @throws CurlException + */ + private function executeStream($url, $method, $data, $headers) { + $options = [ + 'http' => [ + 'method' => $method, + 'header' => implode("\r\n", $headers), + 'content' => $data, + 'ignore_errors' => true // To get a response even on 4xx/5xx errors + ] + ]; + + $context = stream_context_create($options); + $response = @file_get_contents($url, false, $context); + + if ($response === false) { + throw new CurlException('Stream error: Could not fetch URL'); + } + + // Parse headers and HTTP code + $this->responseHeaders = $http_response_header; + $statusLine = $this->responseHeaders[0]; + preg_match('{HTTP\/\S+\s(\d{3})}', $statusLine, $match); + $this->httpCode = intval($match[1]); + + $this->responseBody = $response; + return $this->responseBody; + } + /** * Gets the HTTP status code from the last response. * @@ -174,12 +205,14 @@ public function getHttpCode() { public function getResponseHeaders() { return $this->responseHeaders; } - + /** * Closes the cURL handle. */ public function close() { - curl_close($this->curl); + if ($this->isCurlAvailable && is_resource($this->curl)) { + curl_close($this->curl); + } } /** @@ -188,4 +221,4 @@ public function close() { public function __destruct() { $this->close(); } -} +} \ No newline at end of file From 74ba14b9b305e925e9f001576e495608e3f6c0b2 Mon Sep 17 00:00:00 2001 From: "Kamshory, MT" Date: Mon, 15 Sep 2025 16:05:44 +0700 Subject: [PATCH 04/15] Create XmlToJsonParser.php --- CHANGELOG.md | 53 ++++++++++ src/Util/XmlToJsonParser.php | 186 +++++++++++++++++++++++++++++++++++ 2 files changed, 239 insertions(+) create mode 100644 src/Util/XmlToJsonParser.php diff --git a/CHANGELOG.md b/CHANGELOG.md index cca08f91..7a9c7b8d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1283,4 +1283,57 @@ CREATE TABLE useraccount (...); * **No naming strategy changes** were introduced. Table names remain exactly the same; only the surrounding quotes are removed. * If your schema relies on **case-sensitive identifiers** or **reserved keywords**, you may still need to add quotes manually. +# MagicObject Version 3.19.0 + +## New Feature: `XmlToJsonParser` Utility Class + +A new utility class **`XmlToJsonParser`** has been introduced to parse XML documents into PHP arrays/JSON. +It supports: + +* **Custom flattener element**: users can configure which XML elements should be treated as array items (e.g., ``, ``, etc.). +* **Consistent output**: empty XML elements are automatically converted into `null` instead of empty arrays. +* **Round-trip support**: arrays can also be converted back into XML, with configurable wrapper element names. + +This allows developers to manage application configuration using XML files, which are **less error-prone compared to YAML**, especially in environments where indentation issues are common. + +**Example Usage:** + +```php +$parser = new XmlToJsonParser(['entry', 'item']); +$config = $parser->parse(file_get_contents('config.xml')); +``` + +## Enhancement: `PicoCurlUtil` — Alternative to `curl` + +A new class **`PicoCurlUtil`** has been added under `MagicObject\Util`. +This class provides an interface for making HTTP requests with **automatic fallback**: + +* Uses **cURL** if the PHP `curl` extension is available. +* Falls back to **PHP streams** (`file_get_contents` + stream context) when `curl` is not available. + +### Features + +* Supports **GET** and **POST** requests (with planned extensions for PUT, DELETE, etc.). +* Allows setting **headers**, request body, and **SSL verification options**. +* Provides access to **response headers**, **body**, and **HTTP status code**. +* Automatically throws **`CurlException`** on error, for consistent error handling. + +**Example Usage:** + +```php +use MagicObject\Util\PicoCurlUtil; + +$http = new PicoCurlUtil(); +$response = $http->get("https://example.com/api/data"); + +if ($http->getHttpCode() === 200) { + echo $response; +} +``` + +### Why It Matters? + +* **Greater Flexibility:** Developers can now use XML configuration files instead of YAML. +* **Better Portability:** Applications can run even in environments where the `curl` extension is not installed. +* **Consistent API:** Whether using cURL or streams, you always interact via `PicoCurlUtil`. diff --git a/src/Util/XmlToJsonParser.php b/src/Util/XmlToJsonParser.php new file mode 100644 index 00000000..742d3997 --- /dev/null +++ b/src/Util/XmlToJsonParser.php @@ -0,0 +1,186 @@ +arrayItemNames = $arrayItemNames; + } + + /** + * Parse XML string into PHP array. + * + * @param string $xmlString XML content as string. + * @return mixed Parsed array structure. + */ + public function parse($xmlString) + { + // Load XML string into SimpleXMLElement + $xml = simplexml_load_string($xmlString, "SimpleXMLElement", LIBXML_NOCDATA); + return $this->convertElement($xml); + } + + /** + * Recursively convert SimpleXMLElement to array. + * + * @param SimpleXMLElement $element + * @return mixed + */ + private function convertElement($element) + { + $result = []; + + // Store attributes as "@attrName" + foreach ($element->attributes() as $attrName => $attrValue) { + $result["@{$attrName}"] = $this->castValue((string)$attrValue); + } + + // Group children by element name + $childrenByName = []; + foreach ($element->children() as $childName => $child) { + $childrenByName[$childName][] = $this->convertElement($child); + } + + // Process children + foreach ($childrenByName as $childName => $childValues) { + // If only one child group and it's in arrayItemNames, flatten to array + if (count($childrenByName) === 1 && in_array($childName, $this->arrayItemNames, true)) { + $result = $childValues; + } elseif (count($childValues) === 1) { + $result[$childName] = $childValues[0]; + } else { + $result[$childName] = $childValues; + } + } + + // Get text node if present and not just whitespace + $text = trim((string)$element); + if ($text !== "") { + if (count($result) > 0) { + $result["#text"] = $this->castValue($text); + } else { + // If only text, return casted value directly + return $this->castValue($text); + } + } + + // If truly empty (no attributes, no children, no text), return null + if (empty($result)) { + return null; + } + + return $result; + } + + /** + * Cast string value to appropriate PHP type. + * + * @param string $value + * @return mixed + */ + private function castValue($value) + { + if (is_numeric($value)) { + return $value + 0; + } + + $lower = strtolower($value); + if ($lower === "true") return true; + if ($lower === "false") return false; + if ($lower === "null") return null; + + return $value; + } + + /** + * Converts a PHP array to an XML string. + * + * @param array $array The input array to convert. + * @param string $rootName The name of the root XML element. + * @param string $itemName The name to use for array items. + * @return string The resulting XML string. + */ + public function toXml($array, $rootName = "root", $itemName = "item") + { + $xml = new SimpleXMLElement("<{$rootName}>"); + $this->arrayToXml($array, $xml, $rootName, $itemName); + + // Format output with indentation using DOMDocument + $dom = new \DOMDocument('1.0', 'UTF-8'); + $dom->preserveWhiteSpace = false; + $dom->formatOutput = true; + $dom->loadXML($xml->asXML()); + return $dom->saveXML(); + } + + /** + * Recursively adds array data to a SimpleXMLElement. + * + * @param mixed $data The data to convert (array or scalar). + * @param SimpleXMLElement &$xmlElement The XML element to append to. + * @param string $currentName The current element name. + * @param string $itemName The name to use for array items. + * @return void + */ + private function arrayToXml($data, &$xmlElement, $currentName, $itemName) + { + if (is_array($data)) { + $isAssoc = array_keys($data) !== range(0, count($data) - 1); + + if (!$isAssoc) { + // array serial → bungkus pakai itemName dari parameter + foreach ($data as $value) { + $child = $xmlElement->addChild($itemName); + $this->arrayToXml($value, $child, $itemName, $itemName); + } + } else { + foreach ($data as $key => $value) { + if ($key === "#text") { + $xmlElement[0] = htmlspecialchars((string)$value); + } elseif (strpos($key, "@") === 0) { + $xmlElement->addAttribute(substr($key, 1), (string)$value); + } else { + if (is_array($value)) { + $isAssocChild = array_keys($value) !== range(0, count($value) - 1); + if ($isAssocChild) { + $child = $xmlElement->addChild($key); + $this->arrayToXml($value, $child, $key, $itemName); + } else { + foreach ($value as $v) { + $child = $xmlElement->addChild($key); + $this->arrayToXml($v, $child, $key, $itemName); + } + } + } else { + $xmlElement->addChild($key, htmlspecialchars((string)$value)); + } + } + } + } + } elseif ($data !== null) { + $xmlElement[0] = htmlspecialchars((string)$data); + } + } +} \ No newline at end of file From 626091ac2a9a7da4c366442ca659b6647d732015 Mon Sep 17 00:00:00 2001 From: "Kamshory, MT" Date: Mon, 15 Sep 2025 16:30:19 +0700 Subject: [PATCH 05/15] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a9c7b8d..2bf34ada 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1283,6 +1283,7 @@ CREATE TABLE useraccount (...); * **No naming strategy changes** were introduced. Table names remain exactly the same; only the surrounding quotes are removed. * If your schema relies on **case-sensitive identifiers** or **reserved keywords**, you may still need to add quotes manually. + # MagicObject Version 3.19.0 ## New Feature: `XmlToJsonParser` Utility Class From 41b456d1529df30b25eb55fde1383e3742f3b134 Mon Sep 17 00:00:00 2001 From: "Kamshory, MT" Date: Mon, 15 Sep 2025 16:36:46 +0700 Subject: [PATCH 06/15] Add scalar value, convert false to string "false" --- src/Util/XmlToJsonParser.php | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/src/Util/XmlToJsonParser.php b/src/Util/XmlToJsonParser.php index 742d3997..53b4a9b6 100644 --- a/src/Util/XmlToJsonParser.php +++ b/src/Util/XmlToJsonParser.php @@ -143,6 +143,13 @@ public function toXml($array, $rootName = "root", $itemName = "item") * @param string $currentName The current element name. * @param string $itemName The name to use for array items. * @return void + * + * This function traverses the input array or scalar value and appends it to the given XML element. + * - If the value is an array, it checks if it is associative or sequential. + * - Sequential arrays are wrapped using the provided itemName. + * - Associative arrays are processed by key, handling text nodes ("#text") and attributes ("@attr"). + * - Scalar values are added as text nodes. + * - Boolean false is converted to string "false" instead of null. */ private function arrayToXml($data, &$xmlElement, $currentName, $itemName) { @@ -150,7 +157,7 @@ private function arrayToXml($data, &$xmlElement, $currentName, $itemName) $isAssoc = array_keys($data) !== range(0, count($data) - 1); if (!$isAssoc) { - // array serial → bungkus pakai itemName dari parameter + // Sequential array → wrap using itemName parameter foreach ($data as $value) { $child = $xmlElement->addChild($itemName); $this->arrayToXml($value, $child, $itemName, $itemName); @@ -158,9 +165,11 @@ private function arrayToXml($data, &$xmlElement, $currentName, $itemName) } else { foreach ($data as $key => $value) { if ($key === "#text") { - $xmlElement[0] = htmlspecialchars((string)$value); + // Add text node + $xmlElement[0] = htmlspecialchars($value === false ? "false" : (string)$value); } elseif (strpos($key, "@") === 0) { - $xmlElement->addAttribute(substr($key, 1), (string)$value); + // Add attribute + $xmlElement->addAttribute(substr($key, 1), $value === false ? "false" : (string)$value); } else { if (is_array($value)) { $isAssocChild = array_keys($value) !== range(0, count($value) - 1); @@ -174,13 +183,15 @@ private function arrayToXml($data, &$xmlElement, $currentName, $itemName) } } } else { - $xmlElement->addChild($key, htmlspecialchars((string)$value)); + // Add child element with value, convert false to string "false" + $xmlElement->addChild($key, htmlspecialchars($value === false ? "false" : (string)$value)); } } } } } elseif ($data !== null) { - $xmlElement[0] = htmlspecialchars((string)$data); + // Add scalar value, convert false to string "false" + $xmlElement[0] = htmlspecialchars($data === false ? "false" : (string)$data); } } } \ No newline at end of file From 088b08ef444a3a4afb8612440596379fe0fac761 Mon Sep 17 00:00:00 2001 From: "Kamshory, MT" Date: Sat, 13 Sep 2025 22:10:32 +0700 Subject: [PATCH 07/15] Update README.md --- README.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 77fa89bf..f1b84923 100644 --- a/README.md +++ b/README.md @@ -602,4 +602,12 @@ A tutorial is available here: https://github.com/Planetbiru/MagicObject/blob/mai # Contributors -1. Kamshory - https://github.com/kamshory/ \ No newline at end of file +1. Kamshory - https://github.com/kamshory/ + +## Support MagicObject Development + +MagicObject is actively developed to help the developer community build applications faster and more efficiently. If you find this project useful or would like to support further development, you can make a donation via PayPal. Your contribution will be used to add new features, fix bugs, and improve documentation. + +Thank you for your support! + +[![paypal](https://www.paypalobjects.com/en_US/i/btn/btn_donateCC_LG.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=DMHFJ6LR7FGQS) \ No newline at end of file From e5859f37586ad4078f3adb2e483e6b8764e0de5c Mon Sep 17 00:00:00 2001 From: "Kamshory, MT" Date: Sat, 13 Sep 2025 22:10:32 +0700 Subject: [PATCH 08/15] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f1b84923..4fc5d82f 100644 --- a/README.md +++ b/README.md @@ -604,7 +604,7 @@ A tutorial is available here: https://github.com/Planetbiru/MagicObject/blob/mai 1. Kamshory - https://github.com/kamshory/ -## Support MagicObject Development +# Support MagicObject Development MagicObject is actively developed to help the developer community build applications faster and more efficiently. If you find this project useful or would like to support further development, you can make a donation via PayPal. Your contribution will be used to add new features, fix bugs, and improve documentation. From 1515df00019be9012586b188db0cdfaaa7be2879 Mon Sep 17 00:00:00 2001 From: "Kamshory, MT" Date: Mon, 15 Sep 2025 16:05:41 +0700 Subject: [PATCH 09/15] Update PicoCurlUtil.php --- src/Util/PicoCurlUtil.php | 145 +++++++++++++++++++++++--------------- 1 file changed, 89 insertions(+), 56 deletions(-) diff --git a/src/Util/PicoCurlUtil.php b/src/Util/PicoCurlUtil.php index a6759e1f..d8fbe97f 100644 --- a/src/Util/PicoCurlUtil.php +++ b/src/Util/PicoCurlUtil.php @@ -8,8 +8,7 @@ /** * Class PicoCurlUtil * - * This class provides an interface for making HTTP requests using cURL. - * + * This class provides an interface for making HTTP requests using cURL or PHP streams as a fallback. * @author Kamshory * @package MagicObject * @link https://github.com/Planetbiru/MagicObject @@ -19,10 +18,17 @@ class PicoCurlUtil { /** * cURL handle * - * @var CurlHandle + * @var CurlHandle|null */ private $curl; + /** + * Flag to indicate if the cURL extension is available + * + * @var bool + */ + private $isCurlAvailable = false; + /** * Response headers from the last request * @@ -46,12 +52,18 @@ class PicoCurlUtil { /** * PicoCurlUtil constructor. - * Initializes the cURL handle. + * Initializes the cURL handle if available, otherwise sets the fallback flag. */ public function __construct() { - $this->curl = curl_init(); - $this->setOption(CURLOPT_RETURNTRANSFER, true); - $this->setOption(CURLOPT_HEADER, true); + if (extension_loaded('curl')) { + $this->isCurlAvailable = true; + $this->curl = curl_init(); + $this->setOption(CURLOPT_RETURNTRANSFER, true); + $this->setOption(CURLOPT_HEADER, true); + } else { + // No cURL extension, will use stream functions as a fallback + $this->isCurlAvailable = false; + } } /** @@ -61,7 +73,9 @@ public function __construct() { * @param mixed $value Value for the cURL option */ public function setOption($option, $value) { - curl_setopt($this->curl, $option, $value); + if ($this->isCurlAvailable) { + curl_setopt($this->curl, $option, $value); + } } /** @@ -70,8 +84,10 @@ public function setOption($option, $value) { * @param bool $verify If true, SSL verification is enabled; if false, it is disabled. */ public function setSslVerification($verify) { - $this->setOption(CURLOPT_SSL_VERIFYPEER, $verify); - $this->setOption(CURLOPT_SSL_VERIFYHOST, $verify ? 2 : 0); + if ($this->isCurlAvailable) { + $this->setOption(CURLOPT_SSL_VERIFYPEER, $verify); + $this->setOption(CURLOPT_SSL_VERIFYHOST, $verify ? 2 : 0); + } } /** @@ -80,12 +96,16 @@ public function setSslVerification($verify) { * @param string $url URL for the request * @param array $headers Additional headers for the request * @return string Response body - * @throws CurlException If an error occurs during cURL execution + * @throws CurlException If an error occurs during execution */ public function get($url, $headers = array()) { - $this->setOption(CURLOPT_URL, $url); - $this->setOption(CURLOPT_HTTPHEADER, $headers); - return $this->execute(); + if ($this->isCurlAvailable) { + $this->setOption(CURLOPT_URL, $url); + $this->setOption(CURLOPT_HTTPHEADER, $headers); + return $this->executeCurl(); + } else { + return $this->executeStream($url, 'GET', null, $headers); + } } /** @@ -95,47 +115,21 @@ public function get($url, $headers = array()) { * @param mixed $data Data to send * @param array $headers Additional headers for the request * @return string Response body - * @throws CurlException If an error occurs during cURL execution + * @throws CurlException If an error occurs during execution */ public function post($url, $data, $headers = array()) { - $this->setOption(CURLOPT_URL, $url); - $this->setOption(CURLOPT_POST, true); - $this->setOption(CURLOPT_POSTFIELDS, $data); - $this->setOption(CURLOPT_HTTPHEADER, $headers); - return $this->execute(); - } - - /** - * Executes a PUT request. - * - * @param string $url URL for the request - * @param mixed $data Data to send - * @param array $headers Additional headers for the request - * @return string Response body - * @throws CurlException If an error occurs during cURL execution - */ - public function put($url, $data, $headers = array()) { - $this->setOption(CURLOPT_URL, $url); - $this->setOption(CURLOPT_CUSTOMREQUEST, "PUT"); - $this->setOption(CURLOPT_POSTFIELDS, $data); - $this->setOption(CURLOPT_HTTPHEADER, $headers); - return $this->execute(); - } - - /** - * Executes a DELETE request. - * - * @param string $url URL for the request - * @param array $headers Additional headers for the request - * @return string Response body - * @throws CurlException If an error occurs during cURL execution - */ - public function delete($url, $headers = array()) { - $this->setOption(CURLOPT_URL, $url); - $this->setOption(CURLOPT_CUSTOMREQUEST, "DELETE"); - $this->setOption(CURLOPT_HTTPHEADER, $headers); - return $this->execute(); + if ($this->isCurlAvailable) { + $this->setOption(CURLOPT_URL, $url); + $this->setOption(CURLOPT_POST, true); + $this->setOption(CURLOPT_POSTFIELDS, $data); + $this->setOption(CURLOPT_HTTPHEADER, $headers); + return $this->executeCurl(); + } else { + return $this->executeStream($url, 'POST', $data, $headers); + } } + + // Add other methods (put, delete) with a similar logic /** * Executes the cURL request and processes the response. @@ -143,7 +137,7 @@ public function delete($url, $headers = array()) { * @return string Response body * @throws CurlException If an error occurs during cURL execution */ - private function execute() { + private function executeCurl() { $response = curl_exec($this->curl); if ($response === false) { throw new CurlException('Curl error: ' . curl_error($this->curl)); @@ -157,6 +151,43 @@ private function execute() { return $this->responseBody; } + /** + * Executes the request using PHP streams. + * + * @param string $url + * @param string $method + * @param mixed $data + * @param array $headers + * @return string + * @throws CurlException + */ + private function executeStream($url, $method, $data, $headers) { + $options = [ + 'http' => [ + 'method' => $method, + 'header' => implode("\r\n", $headers), + 'content' => $data, + 'ignore_errors' => true // To get a response even on 4xx/5xx errors + ] + ]; + + $context = stream_context_create($options); + $response = @file_get_contents($url, false, $context); + + if ($response === false) { + throw new CurlException('Stream error: Could not fetch URL'); + } + + // Parse headers and HTTP code + $this->responseHeaders = $http_response_header; + $statusLine = $this->responseHeaders[0]; + preg_match('{HTTP\/\S+\s(\d{3})}', $statusLine, $match); + $this->httpCode = intval($match[1]); + + $this->responseBody = $response; + return $this->responseBody; + } + /** * Gets the HTTP status code from the last response. * @@ -174,12 +205,14 @@ public function getHttpCode() { public function getResponseHeaders() { return $this->responseHeaders; } - + /** * Closes the cURL handle. */ public function close() { - curl_close($this->curl); + if ($this->isCurlAvailable && is_resource($this->curl)) { + curl_close($this->curl); + } } /** @@ -188,4 +221,4 @@ public function close() { public function __destruct() { $this->close(); } -} +} \ No newline at end of file From fc36a945ab5703bce4f04dd102cd9f20d107397d Mon Sep 17 00:00:00 2001 From: "Kamshory, MT" Date: Mon, 15 Sep 2025 16:05:44 +0700 Subject: [PATCH 10/15] Create XmlToJsonParser.php --- CHANGELOG.md | 53 ++++++++++ src/Util/XmlToJsonParser.php | 186 +++++++++++++++++++++++++++++++++++ 2 files changed, 239 insertions(+) create mode 100644 src/Util/XmlToJsonParser.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 09fa3c47..1bd045a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1283,6 +1283,59 @@ CREATE TABLE useraccount (...); * **No naming strategy changes** were introduced. Table names remain exactly the same; only the surrounding quotes are removed. * If your schema relies on **case-sensitive identifiers** or **reserved keywords**, you may still need to add quotes manually. +# MagicObject Version 3.19.0 + +## New Feature: `XmlToJsonParser` Utility Class + +A new utility class **`XmlToJsonParser`** has been introduced to parse XML documents into PHP arrays/JSON. +It supports: + +* **Custom flattener element**: users can configure which XML elements should be treated as array items (e.g., ``, ``, etc.). +* **Consistent output**: empty XML elements are automatically converted into `null` instead of empty arrays. +* **Round-trip support**: arrays can also be converted back into XML, with configurable wrapper element names. + +This allows developers to manage application configuration using XML files, which are **less error-prone compared to YAML**, especially in environments where indentation issues are common. + +**Example Usage:** + +```php +$parser = new XmlToJsonParser(['entry', 'item']); +$config = $parser->parse(file_get_contents('config.xml')); +``` + +## Enhancement: `PicoCurlUtil` — Alternative to `curl` + +A new class **`PicoCurlUtil`** has been added under `MagicObject\Util`. +This class provides an interface for making HTTP requests with **automatic fallback**: + +* Uses **cURL** if the PHP `curl` extension is available. +* Falls back to **PHP streams** (`file_get_contents` + stream context) when `curl` is not available. + +### Features + +* Supports **GET** and **POST** requests (with planned extensions for PUT, DELETE, etc.). +* Allows setting **headers**, request body, and **SSL verification options**. +* Provides access to **response headers**, **body**, and **HTTP status code**. +* Automatically throws **`CurlException`** on error, for consistent error handling. + +**Example Usage:** + +```php +use MagicObject\Util\PicoCurlUtil; + +$http = new PicoCurlUtil(); +$response = $http->get("https://example.com/api/data"); + +if ($http->getHttpCode() === 200) { + echo $response; +} +``` + +### Why It Matters? + +* **Greater Flexibility:** Developers can now use XML configuration files instead of YAML. +* **Better Portability:** Applications can run even in environments where the `curl` extension is not installed. +* **Consistent API:** Whether using cURL or streams, you always interact via `PicoCurlUtil`. # MagicObject Version 3.18.0 diff --git a/src/Util/XmlToJsonParser.php b/src/Util/XmlToJsonParser.php new file mode 100644 index 00000000..742d3997 --- /dev/null +++ b/src/Util/XmlToJsonParser.php @@ -0,0 +1,186 @@ +arrayItemNames = $arrayItemNames; + } + + /** + * Parse XML string into PHP array. + * + * @param string $xmlString XML content as string. + * @return mixed Parsed array structure. + */ + public function parse($xmlString) + { + // Load XML string into SimpleXMLElement + $xml = simplexml_load_string($xmlString, "SimpleXMLElement", LIBXML_NOCDATA); + return $this->convertElement($xml); + } + + /** + * Recursively convert SimpleXMLElement to array. + * + * @param SimpleXMLElement $element + * @return mixed + */ + private function convertElement($element) + { + $result = []; + + // Store attributes as "@attrName" + foreach ($element->attributes() as $attrName => $attrValue) { + $result["@{$attrName}"] = $this->castValue((string)$attrValue); + } + + // Group children by element name + $childrenByName = []; + foreach ($element->children() as $childName => $child) { + $childrenByName[$childName][] = $this->convertElement($child); + } + + // Process children + foreach ($childrenByName as $childName => $childValues) { + // If only one child group and it's in arrayItemNames, flatten to array + if (count($childrenByName) === 1 && in_array($childName, $this->arrayItemNames, true)) { + $result = $childValues; + } elseif (count($childValues) === 1) { + $result[$childName] = $childValues[0]; + } else { + $result[$childName] = $childValues; + } + } + + // Get text node if present and not just whitespace + $text = trim((string)$element); + if ($text !== "") { + if (count($result) > 0) { + $result["#text"] = $this->castValue($text); + } else { + // If only text, return casted value directly + return $this->castValue($text); + } + } + + // If truly empty (no attributes, no children, no text), return null + if (empty($result)) { + return null; + } + + return $result; + } + + /** + * Cast string value to appropriate PHP type. + * + * @param string $value + * @return mixed + */ + private function castValue($value) + { + if (is_numeric($value)) { + return $value + 0; + } + + $lower = strtolower($value); + if ($lower === "true") return true; + if ($lower === "false") return false; + if ($lower === "null") return null; + + return $value; + } + + /** + * Converts a PHP array to an XML string. + * + * @param array $array The input array to convert. + * @param string $rootName The name of the root XML element. + * @param string $itemName The name to use for array items. + * @return string The resulting XML string. + */ + public function toXml($array, $rootName = "root", $itemName = "item") + { + $xml = new SimpleXMLElement("<{$rootName}>"); + $this->arrayToXml($array, $xml, $rootName, $itemName); + + // Format output with indentation using DOMDocument + $dom = new \DOMDocument('1.0', 'UTF-8'); + $dom->preserveWhiteSpace = false; + $dom->formatOutput = true; + $dom->loadXML($xml->asXML()); + return $dom->saveXML(); + } + + /** + * Recursively adds array data to a SimpleXMLElement. + * + * @param mixed $data The data to convert (array or scalar). + * @param SimpleXMLElement &$xmlElement The XML element to append to. + * @param string $currentName The current element name. + * @param string $itemName The name to use for array items. + * @return void + */ + private function arrayToXml($data, &$xmlElement, $currentName, $itemName) + { + if (is_array($data)) { + $isAssoc = array_keys($data) !== range(0, count($data) - 1); + + if (!$isAssoc) { + // array serial → bungkus pakai itemName dari parameter + foreach ($data as $value) { + $child = $xmlElement->addChild($itemName); + $this->arrayToXml($value, $child, $itemName, $itemName); + } + } else { + foreach ($data as $key => $value) { + if ($key === "#text") { + $xmlElement[0] = htmlspecialchars((string)$value); + } elseif (strpos($key, "@") === 0) { + $xmlElement->addAttribute(substr($key, 1), (string)$value); + } else { + if (is_array($value)) { + $isAssocChild = array_keys($value) !== range(0, count($value) - 1); + if ($isAssocChild) { + $child = $xmlElement->addChild($key); + $this->arrayToXml($value, $child, $key, $itemName); + } else { + foreach ($value as $v) { + $child = $xmlElement->addChild($key); + $this->arrayToXml($v, $child, $key, $itemName); + } + } + } else { + $xmlElement->addChild($key, htmlspecialchars((string)$value)); + } + } + } + } + } elseif ($data !== null) { + $xmlElement[0] = htmlspecialchars((string)$data); + } + } +} \ No newline at end of file From 6c195bed76c4d01aa33ad67b8cdd5a3e2e94410e Mon Sep 17 00:00:00 2001 From: "Kamshory, MT" Date: Mon, 15 Sep 2025 16:30:19 +0700 Subject: [PATCH 11/15] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1bd045a4..b75e9845 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1283,6 +1283,7 @@ CREATE TABLE useraccount (...); * **No naming strategy changes** were introduced. Table names remain exactly the same; only the surrounding quotes are removed. * If your schema relies on **case-sensitive identifiers** or **reserved keywords**, you may still need to add quotes manually. + # MagicObject Version 3.19.0 ## New Feature: `XmlToJsonParser` Utility Class From fe8b58a1678dc7425005f48060ba19b0104ae70d Mon Sep 17 00:00:00 2001 From: "Kamshory, MT" Date: Mon, 15 Sep 2025 16:05:44 +0700 Subject: [PATCH 12/15] Create XmlToJsonParser.php --- CHANGELOG.md | 53 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b75e9845..98e0582f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1283,6 +1283,59 @@ CREATE TABLE useraccount (...); * **No naming strategy changes** were introduced. Table names remain exactly the same; only the surrounding quotes are removed. * If your schema relies on **case-sensitive identifiers** or **reserved keywords**, you may still need to add quotes manually. +# MagicObject Version 3.19.0 + +## New Feature: `XmlToJsonParser` Utility Class + +A new utility class **`XmlToJsonParser`** has been introduced to parse XML documents into PHP arrays/JSON. +It supports: + +* **Custom flattener element**: users can configure which XML elements should be treated as array items (e.g., ``, ``, etc.). +* **Consistent output**: empty XML elements are automatically converted into `null` instead of empty arrays. +* **Round-trip support**: arrays can also be converted back into XML, with configurable wrapper element names. + +This allows developers to manage application configuration using XML files, which are **less error-prone compared to YAML**, especially in environments where indentation issues are common. + +**Example Usage:** + +```php +$parser = new XmlToJsonParser(['entry', 'item']); +$config = $parser->parse(file_get_contents('config.xml')); +``` + +## Enhancement: `PicoCurlUtil` — Alternative to `curl` + +A new class **`PicoCurlUtil`** has been added under `MagicObject\Util`. +This class provides an interface for making HTTP requests with **automatic fallback**: + +* Uses **cURL** if the PHP `curl` extension is available. +* Falls back to **PHP streams** (`file_get_contents` + stream context) when `curl` is not available. + +### Features + +* Supports **GET** and **POST** requests (with planned extensions for PUT, DELETE, etc.). +* Allows setting **headers**, request body, and **SSL verification options**. +* Provides access to **response headers**, **body**, and **HTTP status code**. +* Automatically throws **`CurlException`** on error, for consistent error handling. + +**Example Usage:** + +```php +use MagicObject\Util\PicoCurlUtil; + +$http = new PicoCurlUtil(); +$response = $http->get("https://example.com/api/data"); + +if ($http->getHttpCode() === 200) { + echo $response; +} +``` + +### Why It Matters? + +* **Greater Flexibility:** Developers can now use XML configuration files instead of YAML. +* **Better Portability:** Applications can run even in environments where the `curl` extension is not installed. +* **Consistent API:** Whether using cURL or streams, you always interact via `PicoCurlUtil`. # MagicObject Version 3.19.0 From 848a63aad6af1c3f801d613d5933e86df17ce69d Mon Sep 17 00:00:00 2001 From: "Kamshory, MT" Date: Mon, 15 Sep 2025 16:36:46 +0700 Subject: [PATCH 13/15] Add scalar value, convert false to string "false" --- src/Util/XmlToJsonParser.php | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/src/Util/XmlToJsonParser.php b/src/Util/XmlToJsonParser.php index 742d3997..53b4a9b6 100644 --- a/src/Util/XmlToJsonParser.php +++ b/src/Util/XmlToJsonParser.php @@ -143,6 +143,13 @@ public function toXml($array, $rootName = "root", $itemName = "item") * @param string $currentName The current element name. * @param string $itemName The name to use for array items. * @return void + * + * This function traverses the input array or scalar value and appends it to the given XML element. + * - If the value is an array, it checks if it is associative or sequential. + * - Sequential arrays are wrapped using the provided itemName. + * - Associative arrays are processed by key, handling text nodes ("#text") and attributes ("@attr"). + * - Scalar values are added as text nodes. + * - Boolean false is converted to string "false" instead of null. */ private function arrayToXml($data, &$xmlElement, $currentName, $itemName) { @@ -150,7 +157,7 @@ private function arrayToXml($data, &$xmlElement, $currentName, $itemName) $isAssoc = array_keys($data) !== range(0, count($data) - 1); if (!$isAssoc) { - // array serial → bungkus pakai itemName dari parameter + // Sequential array → wrap using itemName parameter foreach ($data as $value) { $child = $xmlElement->addChild($itemName); $this->arrayToXml($value, $child, $itemName, $itemName); @@ -158,9 +165,11 @@ private function arrayToXml($data, &$xmlElement, $currentName, $itemName) } else { foreach ($data as $key => $value) { if ($key === "#text") { - $xmlElement[0] = htmlspecialchars((string)$value); + // Add text node + $xmlElement[0] = htmlspecialchars($value === false ? "false" : (string)$value); } elseif (strpos($key, "@") === 0) { - $xmlElement->addAttribute(substr($key, 1), (string)$value); + // Add attribute + $xmlElement->addAttribute(substr($key, 1), $value === false ? "false" : (string)$value); } else { if (is_array($value)) { $isAssocChild = array_keys($value) !== range(0, count($value) - 1); @@ -174,13 +183,15 @@ private function arrayToXml($data, &$xmlElement, $currentName, $itemName) } } } else { - $xmlElement->addChild($key, htmlspecialchars((string)$value)); + // Add child element with value, convert false to string "false" + $xmlElement->addChild($key, htmlspecialchars($value === false ? "false" : (string)$value)); } } } } } elseif ($data !== null) { - $xmlElement[0] = htmlspecialchars((string)$data); + // Add scalar value, convert false to string "false" + $xmlElement[0] = htmlspecialchars($data === false ? "false" : (string)$data); } } } \ No newline at end of file From 162962b82f61d8641e9bc43af00a19e353d0f6e1 Mon Sep 17 00:00:00 2001 From: "Kamshory, MT" Date: Mon, 15 Sep 2025 16:46:13 +0700 Subject: [PATCH 14/15] Update CHANGELOG.md --- CHANGELOG.md | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 98e0582f..2818d696 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1337,6 +1337,23 @@ if ($http->getHttpCode() === 200) { * **Better Portability:** Applications can run even in environments where the `curl` extension is not installed. * **Consistent API:** Whether using cURL or streams, you always interact via `PicoCurlUtil`. + +# MagicObject Version 3.18.0 + +## Enhancement: Database Migration + +A new parameter has been added to **Database Migration** to provide greater flexibility. +Previously, all migration queries had to be dumped into a SQL file before execution. +With this update, developers can now choose to **run queries directly on the target database**, reducing steps and improving efficiency in deployment workflows. + +This makes migrations faster, easier to automate, and less error-prone—especially useful for CI/CD pipelines. + +## Bug Fixes: Undefined Array Index in `PicoPageData::applySubqueryResult()` + +Fixed an issue where an **undefined array index** error could occur when the provided data structure did not match the expected format. +This patch ensures more robust handling of unexpected input, improving the **stability and reliability** of query result processing. + + # MagicObject Version 3.19.0 ## New Feature: `XmlToJsonParser` Utility Class @@ -1392,18 +1409,3 @@ if ($http->getHttpCode() === 200) { * **Consistent API:** Whether using cURL or streams, you always interact via `PicoCurlUtil`. -# MagicObject Version 3.18.0 - -## Enhancement: Database Migration - -A new parameter has been added to **Database Migration** to provide greater flexibility. -Previously, all migration queries had to be dumped into a SQL file before execution. -With this update, developers can now choose to **run queries directly on the target database**, reducing steps and improving efficiency in deployment workflows. - -This makes migrations faster, easier to automate, and less error-prone—especially useful for CI/CD pipelines. - -## Bug Fixes: Undefined Array Index in `PicoPageData::applySubqueryResult()` - -Fixed an issue where an **undefined array index** error could occur when the provided data structure did not match the expected format. -This patch ensures more robust handling of unexpected input, improving the **stability and reliability** of query result processing. - From 4c0b29af7a43c3f6ded137f43ed12b056f962777 Mon Sep 17 00:00:00 2001 From: "Kamshory, MT" Date: Wed, 17 Sep 2025 01:34:21 +0700 Subject: [PATCH 15/15] Update CHANGELOG.md --- CHANGELOG.md | 59 ---------------------------------------------------- 1 file changed, 59 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d68855da..f5d6af74 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1283,59 +1283,7 @@ CREATE TABLE useraccount (...); * **No naming strategy changes** were introduced. Table names remain exactly the same; only the surrounding quotes are removed. * If your schema relies on **case-sensitive identifiers** or **reserved keywords**, you may still need to add quotes manually. -# MagicObject Version 3.19.0 - -## New Feature: `XmlToJsonParser` Utility Class - -A new utility class **`XmlToJsonParser`** has been introduced to parse XML documents into PHP arrays/JSON. -It supports: - -* **Custom flattener element**: users can configure which XML elements should be treated as array items (e.g., ``, ``, etc.). -* **Consistent output**: empty XML elements are automatically converted into `null` instead of empty arrays. -* **Round-trip support**: arrays can also be converted back into XML, with configurable wrapper element names. - -This allows developers to manage application configuration using XML files, which are **less error-prone compared to YAML**, especially in environments where indentation issues are common. - -**Example Usage:** -```php -$parser = new XmlToJsonParser(['entry', 'item']); -$config = $parser->parse(file_get_contents('config.xml')); -``` - -## Enhancement: `PicoCurlUtil` — Alternative to `curl` - -A new class **`PicoCurlUtil`** has been added under `MagicObject\Util`. -This class provides an interface for making HTTP requests with **automatic fallback**: - -* Uses **cURL** if the PHP `curl` extension is available. -* Falls back to **PHP streams** (`file_get_contents` + stream context) when `curl` is not available. - -### Features - -* Supports **GET** and **POST** requests (with planned extensions for PUT, DELETE, etc.). -* Allows setting **headers**, request body, and **SSL verification options**. -* Provides access to **response headers**, **body**, and **HTTP status code**. -* Automatically throws **`CurlException`** on error, for consistent error handling. - -**Example Usage:** - -```php -use MagicObject\Util\PicoCurlUtil; - -$http = new PicoCurlUtil(); -$response = $http->get("https://example.com/api/data"); - -if ($http->getHttpCode() === 200) { - echo $response; -} -``` - -### Why It Matters? - -* **Greater Flexibility:** Developers can now use XML configuration files instead of YAML. -* **Better Portability:** Applications can run even in environments where the `curl` extension is not installed. -* **Consistent API:** Whether using cURL or streams, you always interact via `PicoCurlUtil`. # MagicObject Version 3.18.0 @@ -1419,10 +1367,3 @@ Example: tcp://localhost:6379?db=3 ``` -## Enhancement: HTTP Request Fallback without cURL - -MagicObject now includes a **fallback mechanism** for HTTP requests. -If the **cURL extension** is not available in the PHP environment, the library will automatically fall back to using **`stream_context_create`** with **`file_get_contents`**. - -This ensures better **portability** and compatibility across different hosting environments where cURL may be disabled by default. -