diff --git a/composer.json b/composer.json
index ac6661e..e59f33c 100644
--- a/composer.json
+++ b/composer.json
@@ -18,7 +18,8 @@
"require": {
"php": "^8.2",
"ext-bcmath": "*",
- "ext-ctype": "*"
+ "ext-ctype": "*",
+ "ext-zlib": "*"
},
"require-dev": {
"phpunit/phpunit": "^11.4.2"
diff --git a/composer.lock b/composer.lock
index 7182f09..54fbc5f 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "271ccda664646033d9a3393b06caecfc",
+ "content-hash": "1f565fe082028eca22f4d68bd2ec44d0",
"packages": [],
"packages-dev": [
{
@@ -568,16 +568,16 @@
},
{
"name": "phpunit/phpunit",
- "version": "11.4.2",
+ "version": "11.4.3",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git",
- "reference": "1863643c3f04ad03dcb9c6996c294784cdda4805"
+ "reference": "e8e8ed1854de5d36c088ec1833beae40d2dedd76"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/1863643c3f04ad03dcb9c6996c294784cdda4805",
- "reference": "1863643c3f04ad03dcb9c6996c294784cdda4805",
+ "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/e8e8ed1854de5d36c088ec1833beae40d2dedd76",
+ "reference": "e8e8ed1854de5d36c088ec1833beae40d2dedd76",
"shasum": ""
},
"require": {
@@ -648,7 +648,7 @@
"support": {
"issues": "https://github.com/sebastianbergmann/phpunit/issues",
"security": "https://github.com/sebastianbergmann/phpunit/security/policy",
- "source": "https://github.com/sebastianbergmann/phpunit/tree/11.4.2"
+ "source": "https://github.com/sebastianbergmann/phpunit/tree/11.4.3"
},
"funding": [
{
@@ -664,7 +664,7 @@
"type": "tidelift"
}
],
- "time": "2024-10-19T13:05:19+00:00"
+ "time": "2024-10-28T13:07:50+00:00"
},
{
"name": "sebastian/cli-parser",
@@ -1648,7 +1648,8 @@
"platform": {
"php": "^8.2",
"ext-bcmath": "*",
- "ext-ctype": "*"
+ "ext-ctype": "*",
+ "ext-zlib": "*"
},
"platform-dev": [],
"plugin-api-version": "2.6.0"
diff --git a/examples/json.php b/examples/json.php
new file mode 100644
index 0000000..b867136
--- /dev/null
+++ b/examples/json.php
@@ -0,0 +1,165 @@
+ 'Create Json Instances',
+ 'description' => 'We create two Json
objects with different user data.',
+ 'code' => "\$json1 = new Json(\$jsonData1);\n\$json2 = new Json(\$jsonData2);",
+ 'output' => "Json1: " . $json1->getJson() . "\nJson2: " . $json2->getJson(),
+ ];
+
+// // 2. Compare Json instances
+// $areEqual = $json1->compareWith($json2) ? 'Yes' : 'No';
+// $examples[] = [
+// 'title' => 'Compare Json Instances',
+// 'description' => 'We compare json1
and json2
to check if they are identical.',
+// 'code' => "\$areEqual = \$json1->compareWith(\$json2) ? 'Yes' : 'No';",
+// 'output' => "Are Json1 and Json2 identical? " . $areEqual,
+// ];
+
+ // 3. Serialize Json to Array
+ $array1 = $json1->toArray();
+ $examples[] = [
+ 'title' => 'Serialize Json1 to Array',
+ 'description' => 'We convert json1
to a PHP array.',
+ 'code' => "\$array1 = \$json1->toArray();",
+ 'output' => print_r($array1, true),
+ ];
+
+ // 4. Deserialize Array to Json
+ $jsonFromArray = Json::fromArray($array1);
+ $examples[] = [
+ 'title' => 'Deserialize Array to Json',
+ 'description' => 'We create a new Json
object from array1
.',
+ 'code' => "\$jsonFromArray = Json::fromArray(\$array1);",
+ 'output' => "Json from Array: " . $jsonFromArray->getJson(),
+ ];
+
+// // 5. Compress Json1 using HuffmanEncoding
+ $huffmanEncoder = new HuffmanEncoding();
+ $compressed = $json1->compress($huffmanEncoder);
+ $examples[] = [
+ 'title' => 'Compress Json1 using HuffmanEncoding',
+ 'description' => 'We compress json1
using HuffmanEncoding
.',
+ 'code' => "\$huffmanEncoder = new HuffmanEncoding();\n\$compressed = \$json1->compress(\$huffmanEncoder);",
+ 'output' => "Compressed Json1 (hex): " . bin2hex($compressed),
+ ];
+
+// // 6. Decompress the previously compressed data
+ $decompressedJson = $json1->decompress($huffmanEncoder, $compressed);
+ $examples[] = [
+ 'title' => 'Decompress the Compressed Data',
+ 'description' => 'We decompress the previously compressed data to retrieve the original JSON.',
+ 'code' => "\$decompressedJson = \$json1->decompress(\$huffmanEncoder, \$compressed);",
+ 'output' => "Decompressed Json: " . $decompressedJson->getJson(),
+ ];
+
+ // 7. Verify decompressed data matches original
+ $isMatch = ($json1->toArray() === $decompressedJson->toArray()) ? 'Yes' : 'No';
+ $examples[] = [
+ 'title' => 'Verify Decompressed Data',
+ 'description' => 'We check if the decompressed JSON matches the original json1
data.',
+ 'code' => "\$isMatch = (\$json1->toArray() === \$decompressedJson->toArray()) ? 'Yes' : 'No';",
+ 'output' => "Does decompressed Json match original Json1? " . $isMatch,
+ ];
+//
+ // 8. Update Json1 by adding a new user
+ $updatedJson1 = $json1->update('users', array_merge($json1->toArray()['users'], [['id' => 5, 'name' => 'Eve']]));
+ $examples[] = [
+ 'title' => 'Update Json1 by Adding a New User',
+ 'description' => 'We add a new user to json1
.',
+ 'code' => "\$updatedJson1 = \$json1->update('users', array_merge(\$json1->toArray()['users'], [['id' => 5, 'name' => 'Eve']]));",
+ 'output' => "Updated Json1: " . $updatedJson1->getJson(),
+ ];
+//
+ // 9. Remove a user from updated Json1
+ $modifiedJson1 = $updatedJson1->remove('users', 2); // Assuming remove method removes by 'id' or index
+ $examples[] = [
+ 'title' => 'Remove a User from Updated Json1',
+ 'description' => 'We remove the user with ID 2 from updatedJson1
.',
+ 'code' => "\$modifiedJson1 = \$updatedJson1->remove('users', 2);",
+ 'output' => "Modified Json1: " . $modifiedJson1->getJson(),
+ ];
+
+} catch (InvalidArgumentException|JsonException $e) {
+ $errorMessage = $e->getMessage();
+}
+
+// Capture all outputs
+$content = ob_get_clean();
+
+?>
+
+
+
+
+
+
+ Json Class Test Example
+
+
+
+
+
+
+
+
+
+
+
+
Json Class Test Example
+
+
+
+ Error:
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Composite/Json.php b/src/Composite/Json.php
new file mode 100644
index 0000000..a88db75
--- /dev/null
+++ b/src/Composite/Json.php
@@ -0,0 +1,200 @@
+validateJson($json);
+ $this->schema = $schema;
+ $this->json = $json;
+ }
+
+ /**
+ * Validates if a string is valid JSON.
+ *
+ * @param string $json
+ * @throws InvalidArgumentException
+ */
+ private function validateJson(string $json): void
+ {
+ try {
+ json_decode($json, true, 512, JSON_THROW_ON_ERROR);
+ } catch (JsonException $e) {
+ throw new InvalidArgumentException('Invalid JSON provided: ' . $e->getMessage());
+ }
+ }
+
+
+ /**
+ * Serializes the JSON data to an array.
+ *
+ * @return array
+ * @throws JsonException
+ */
+ public function toArray(): array
+ {
+ if ($this->data === null) {
+ $this->data = json_decode($this->json, true, 512, JSON_THROW_ON_ERROR);
+ }
+
+ return $this->data;
+ }
+
+ /**
+ * Serializes the JSON data to an object.
+ *
+ * @return object
+ * @throws JsonException
+ */
+ public function toObject(): object
+ {
+ return json_decode($this->json, false, 512, JSON_THROW_ON_ERROR);
+ }
+
+ /**
+ * Deserializes an array to a Json instance.
+ *
+ * @param array $data
+ * @param string|null $schema
+ * @return self
+ * @throws InvalidArgumentException
+ * @throws JsonException
+ */
+ public static function fromArray(array $data, ?string $schema = null): self
+ {
+ $json = json_encode($data, JSON_THROW_ON_ERROR);
+ return new self($json, $schema);
+ }
+
+ /**
+ * Deserializes an object to a Json instance.
+ *
+ * @param object $object
+ * @param string|null $schema
+ * @return self
+ * @throws InvalidArgumentException
+ * @throws JsonException
+ */
+ public static function fromObject(object $object, ?string $schema = null): self
+ {
+ $json = json_encode($object, JSON_THROW_ON_ERROR);
+ return new self($json, $schema);
+ }
+
+ /**
+ * Gets the JSON string.
+ *
+ * @return string
+ */
+ public function getJson(): string
+ {
+ return $this->json;
+ }
+
+ /**
+ * Compresses the JSON string using the provided encoder.
+ *
+ * @param EncoderInterface $encoder
+ * @return string The compressed string.
+ */
+ public function compress(EncoderInterface $encoder): string
+ {
+ return $encoder->encode($this->json);
+ }
+
+ /**
+ * Decompresses the string using the provided decoder.
+ *
+ * @param DecoderInterface $decoder
+ * @param string $compressed
+ * @return self
+ * @throws InvalidArgumentException
+ */
+ public static function decompress(DecoderInterface $decoder, string $compressed): self
+ {
+ $json = $decoder->decode($compressed);
+ return new self($json);
+ }
+
+ /**
+ * Merges this JSON with another Json instance.
+ * In case of conflicting keys, values from the other Json take precedence.
+ *
+ * @param Json $other
+ * @return self
+ * @throws JsonException
+ */
+ public function merge(Json $other): self
+ {
+ $mergedData = array_merge_recursive($this->toArray(), $other->toArray());
+ $mergedJson = json_encode($mergedData, JSON_THROW_ON_ERROR);
+ return new self($mergedJson, $this->schema);
+ }
+
+ /**
+ * Updates the JSON data with a given key-value pair.
+ *
+ * @param string $key
+ * @param mixed $value
+ * @return self
+ * @throws JsonException
+ */
+ public function update(string $key, mixed $value): self
+ {
+ $data = $this->toArray();
+ $data[$key] = $value;
+ $updatedJson = json_encode($data, JSON_THROW_ON_ERROR);
+ return new self($updatedJson, $this->schema);
+ }
+
+ /**
+ * Removes a key from the JSON data.
+ *
+ * @param string $key
+ * @return self
+ * @throws JsonException
+ */
+ public function remove(string $key): self
+ {
+ $data = $this->toArray();
+ unset($data[$key]);
+ $updatedJson = json_encode($data, JSON_THROW_ON_ERROR);
+ return new self($updatedJson, $this->schema);
+ }
+}
diff --git a/src/Encoding/Base64Encoding.php b/src/Encoding/Base64Encoding.php
new file mode 100644
index 0000000..30e1c51
--- /dev/null
+++ b/src/Encoding/Base64Encoding.php
@@ -0,0 +1,41 @@
+buildFrequencyTable($data);
+
+ // Step 2: Build Huffman Tree
+ $huffmanTree = $this->buildHuffmanTree($frequency);
+
+ // Step 3: Generate Huffman Codes
+ $codes = [];
+ $this->generateCodes($huffmanTree, '', $codes);
+
+ // Step 4: Encode data
+ $encodedData = '';
+ for ($i = 0, $len = strlen($data); $i < $len; $i++) {
+ $char = $data[$i];
+ $encodedData .= $codes[$char];
+ }
+
+ // Step 5: Serialize frequency table and encoded data
+ // Prefix the encoded data with the JSON-encoded frequency table and a separator
+ $serializedFrequency = json_encode($frequency);
+ if ($serializedFrequency === false) {
+ throw new InvalidArgumentException('Failed to serialize frequency table.');
+ }
+
+ // Use a unique separator (null byte) to split frequency table and encoded data
+ $separator = "\0";
+
+ // Convert bit string to byte string
+ $byteString = $this->bitsToBytes($encodedData);
+
+ return $serializedFrequency . $separator . $byteString;
+ }
+
+ /**
+ * Decodes the data using Huffman decoding.
+ *
+ * @param string $data The encoded data with serialized frequency table.
+ * @return string The original decoded data.
+ */
+ public function decode(string $data): string
+ {
+ if ($data === '') {
+ throw new InvalidArgumentException('Cannot decode empty string.');
+ }
+
+ // Step 1: Split the frequency table and the encoded data
+ $separatorPos = strpos($data, "\0");
+ if ($separatorPos === false) {
+ throw new InvalidArgumentException('Invalid encoded data format.');
+ }
+
+ $serializedFrequency = substr($data, 0, $separatorPos);
+ $encodedDataBytes = substr($data, $separatorPos + 1);
+
+ // Step 2: Deserialize frequency table
+ $frequency = json_decode($serializedFrequency, true);
+ if (!is_array($frequency)) {
+ throw new InvalidArgumentException('Failed to deserialize frequency table.');
+ }
+
+ // Step 3: Rebuild Huffman Tree
+ $huffmanTree = $this->buildHuffmanTree($frequency);
+
+ // Step 4: Convert bytes back to bit string
+ $encodedDataBits = $this->bytesToBits($encodedDataBytes);
+
+ // Step 5: Decode bit string using Huffman Tree
+ $decodedData = '';
+ $currentNode = $huffmanTree;
+ $totalBits = strlen($encodedDataBits);
+ for ($i = 0; $i < $totalBits; $i++) {
+ $bit = $encodedDataBits[$i];
+ if ($bit === '0') {
+ $currentNode = $currentNode->left;
+ } else {
+ $currentNode = $currentNode->right;
+ }
+
+ if ($currentNode->isLeaf()) {
+ $decodedData .= $currentNode->character;
+ $currentNode = $huffmanTree;
+ }
+ }
+
+ return $decodedData;
+ }
+
+ /**
+ * Builds a frequency table for the given data.
+ *
+ * @param string $data
+ * @return array Associative array with characters as keys and frequencies as values.
+ */
+ private function buildFrequencyTable(string $data): array
+ {
+ $frequency = [];
+ for ($i = 0, $len = strlen($data); $i < $len; $i++) {
+ $char = $data[$i];
+ if (isset($frequency[$char])) {
+ $frequency[$char]++;
+ } else {
+ $frequency[$char] = 1;
+ }
+ }
+ return $frequency;
+ }
+
+ /**
+ * Builds the Huffman tree from the frequency table.
+ *
+ * @param array $frequency
+ * @return Node The root of the Huffman tree.
+ */
+ private function buildHuffmanTree(array $frequency): Node
+ {
+ // Create a priority queue (min-heap) based on frequency
+ $pq = new \SplPriorityQueue();
+ $pq->setExtractFlags(\SplPriorityQueue::EXTR_DATA);
+
+ foreach ($frequency as $char => $freq) {
+ // Ensure $char is a string
+ $char = (string)$char;
+ // Since SplPriorityQueue is a max-heap, use negative frequency for min-heap behavior
+ $pq->insert(new Node($char, $freq), -$freq);
+ }
+
+ // Edge case: Only one unique character
+ if ($pq->count() === 1) {
+ $onlyNode = $pq->extract();
+ return new Node((string)$onlyNode->character, $onlyNode->frequency, $onlyNode, null);
+ }
+
+ // Build the Huffman tree
+ while ($pq->count() > 1) {
+ $left = $pq->extract();
+ $right = $pq->extract();
+ $merged = new Node('', $left->frequency + $right->frequency, $left, $right);
+ $pq->insert($merged, -$merged->frequency);
+ }
+
+ return $pq->extract();
+ }
+
+ /**
+ * Generates Huffman codes by traversing the tree.
+ *
+ * @param Node $node
+ * @param string $prefix
+ * @param array &$codes
+ * @return void
+ */
+ private function generateCodes(Node $node, string $prefix, array &$codes): void
+ {
+ if ($node->isLeaf()) {
+ // Edge case: If there's only one unique character, assign '0' as its code
+ $codes[$node->character] = $prefix === '' ? '0' : $prefix;
+ return;
+ }
+
+ if ($node->left !== null) {
+ $this->generateCodes($node->left, $prefix . '0', $codes);
+ }
+
+ if ($node->right !== null) {
+ $this->generateCodes($node->right, $prefix . '1', $codes);
+ }
+ }
+
+ /**
+ * Converts a bit string to a byte string.
+ *
+ * @param string $bits
+ * @return string
+ */
+ private function bitsToBytes(string $bits): string
+ {
+ $bytes = '';
+ $length = strlen($bits);
+ for ($i = 0; $i < $length; $i += 8) {
+ $byte = substr($bits, $i, 8);
+ if (strlen($byte) < 8) {
+ $byte = str_pad($byte, 8, '0'); // Pad with zeros if not enough bits
+ }
+ $bytes .= chr(bindec($byte));
+ }
+ return $bytes;
+ }
+
+ /**
+ * Converts a byte string back to a bit string.
+ *
+ * @param string $bytes
+ * @return string
+ */
+ private function bytesToBits(string $bytes): string
+ {
+ $bits = '';
+ $length = strlen($bytes);
+ for ($i = 0; $i < $length; $i++) {
+ $bits .= str_pad(decbin(ord($bytes[$i])), 8, '0', STR_PAD_LEFT);
+ }
+ return $bits;
+ }
+}
diff --git a/src/Encoding/Node.php b/src/Encoding/Node.php
new file mode 100644
index 0000000..48a7cc4
--- /dev/null
+++ b/src/Encoding/Node.php
@@ -0,0 +1,35 @@
+character = $character;
+ $this->frequency = $frequency;
+ $this->left = $left;
+ $this->right = $right;
+ }
+
+ /**
+ * Check if the node is a leaf node.
+ *
+ * @return bool
+ */
+ public function isLeaf(): bool
+ {
+ return is_null($this->left) && is_null($this->right);
+ }
+}
diff --git a/src/Interfaces/DecoderInterface.php b/src/Interfaces/DecoderInterface.php
new file mode 100644
index 0000000..ac8f8c5
--- /dev/null
+++ b/src/Interfaces/DecoderInterface.php
@@ -0,0 +1,21 @@
+