diff --git a/README.md b/README.md
index a5660fa..3b963a9 100644
--- a/README.md
+++ b/README.md
@@ -3,7 +3,7 @@
[](https://github.com/usetrmnl/byos_laravel/actions/workflows/test.yml)
TRMNL BYOS Laravel is a self-hostable implementation of a TRMNL server, built with Laravel.
-It allows you to manage TRMNL devices, generate screens using native plugins, recipes (55+ from the [community catalog](https://bnussbau.github.io/trmnl-recipe-catalog/)), or the API, and can also act as a proxy for the native cloud service (Core). With over 20k downloads and 100+ stars, it’s the most popular community-driven BYOS.
+It allows you to manage TRMNL devices, generate screens using native plugins, recipes (55+ from the [community catalog](https://bnussbau.github.io/trmnl-recipe-catalog/)), or the API, and can also act as a proxy for the native cloud service (Core). With over 15k downloads and 100+ stars, it’s the most popular community-driven BYOS.


diff --git a/app/Models/Plugin.php b/app/Models/Plugin.php
index dfeb757..b372cdd 100644
--- a/app/Models/Plugin.php
+++ b/app/Models/Plugin.php
@@ -14,7 +14,6 @@ use App\Liquid\Tags\TemplateTag;
use Exception;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
-use Illuminate\Http\Client\Response;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\Facades\Http;
@@ -23,7 +22,6 @@ use Illuminate\Support\Str;
use Keepsuit\LaravelLiquid\LaravelLiquidExtension;
use Keepsuit\Liquid\Exceptions\LiquidException;
use Keepsuit\Liquid\Extensions\StandardExtension;
-use SimpleXMLElement;
class Plugin extends Model
{
@@ -85,7 +83,7 @@ class Plugin extends Model
$currentValue = $this->configuration[$fieldKey] ?? null;
// If the field has a default value and no current value is set, it's not missing
- if ((in_array($currentValue, [null, '', []], true)) && ! isset($field['default'])) {
+ if (($currentValue === null || $currentValue === '' || ($currentValue === [])) && ! isset($field['default'])) {
return true; // Found a required field that is not set and has no default
}
}
@@ -147,9 +145,11 @@ class Plugin extends Model
try {
// Make the request based on the verb
- $httpResponse = $this->polling_verb === 'post' ? $httpRequest->post($resolvedUrl) : $httpRequest->get($resolvedUrl);
-
- $response = $this->parseResponse($httpResponse);
+ if ($this->polling_verb === 'post') {
+ $response = $httpRequest->post($resolvedUrl)->json();
+ } else {
+ $response = $httpRequest->get($resolvedUrl)->json();
+ }
$this->update([
'data_payload' => $response,
@@ -183,12 +183,14 @@ class Plugin extends Model
try {
// Make the request based on the verb
- $httpResponse = $this->polling_verb === 'post' ? $httpRequest->post($resolvedUrl) : $httpRequest->get($resolvedUrl);
-
- $response = $this->parseResponse($httpResponse);
+ if ($this->polling_verb === 'post') {
+ $response = $httpRequest->post($resolvedUrl)->json();
+ } else {
+ $response = $httpRequest->get($resolvedUrl)->json();
+ }
// Check if response is an array at root level
- if (array_keys($response) === range(0, count($response) - 1)) {
+ if (is_array($response) && array_keys($response) === range(0, count($response) - 1)) {
// Response is a sequential array, nest under .data
$combinedResponse["IDX_{$index}"] = ['data' => $response];
} else {
@@ -209,56 +211,6 @@ class Plugin extends Model
}
}
- /**
- * Parse HTTP response, handling both JSON and XML content types
- */
- private function parseResponse(Response $httpResponse): array
- {
- if ($httpResponse->header('Content-Type') && str_contains($httpResponse->header('Content-Type'), 'xml')) {
- try {
- // Convert XML to array and wrap under 'rss' key
- $xml = simplexml_load_string($httpResponse->body());
- if ($xml === false) {
- throw new Exception('Invalid XML content');
- }
-
- // Convert SimpleXML directly to array
- $xmlArray = $this->xmlToArray($xml);
-
- return ['rss' => $xmlArray];
- } catch (Exception $e) {
- Log::warning('Failed to parse XML response: '.$e->getMessage());
-
- return ['error' => 'Failed to parse XML response'];
- }
- }
-
- // Default to JSON parsing
- try {
- return $httpResponse->json() ?? [];
- } catch (Exception $e) {
- Log::warning('Failed to parse JSON response: '.$e->getMessage());
-
- return ['error' => 'Failed to parse JSON response'];
- }
- }
-
- /**
- * Convert SimpleXML object to array recursively
- */
- private function xmlToArray(SimpleXMLElement $xml): array
- {
- $array = (array) $xml;
-
- foreach ($array as $key => $value) {
- if ($value instanceof SimpleXMLElement) {
- $array[$key] = $this->xmlToArray($value);
- }
- }
-
- return $array;
- }
-
/**
* Apply Liquid template replacements (converts 'with' syntax to comma syntax)
*/
diff --git a/composer.json b/composer.json
index 0d3fc42..8f3079d 100644
--- a/composer.json
+++ b/composer.json
@@ -12,7 +12,6 @@
"require": {
"php": "^8.2",
"ext-imagick": "*",
- "ext-simplexml": "*",
"ext-zip": "*",
"bnussbau/laravel-trmnl-blade": "2.0.*",
"bnussbau/trmnl-pipeline-php": "^0.3.0",
diff --git a/composer.lock b/composer.lock
index 44c00c7..97a6c15 100644
--- a/composer.lock
+++ b/composer.lock
@@ -62,16 +62,16 @@
},
{
"name": "aws/aws-sdk-php",
- "version": "3.357.0",
+ "version": "3.356.43",
"source": {
"type": "git",
"url": "https://github.com/aws/aws-sdk-php.git",
- "reference": "0325c653b022aedb38f397cf559ab4d0f7967901"
+ "reference": "5722f6ea95240ef28f1ded35448bedcccb0b0883"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/0325c653b022aedb38f397cf559ab4d0f7967901",
- "reference": "0325c653b022aedb38f397cf559ab4d0f7967901",
+ "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/5722f6ea95240ef28f1ded35448bedcccb0b0883",
+ "reference": "5722f6ea95240ef28f1ded35448bedcccb0b0883",
"shasum": ""
},
"require": {
@@ -153,9 +153,9 @@
"support": {
"forum": "https://github.com/aws/aws-sdk-php/discussions",
"issues": "https://github.com/aws/aws-sdk-php/issues",
- "source": "https://github.com/aws/aws-sdk-php/tree/3.357.0"
+ "source": "https://github.com/aws/aws-sdk-php/tree/3.356.43"
},
- "time": "2025-10-22T19:43:07+00:00"
+ "time": "2025-10-21T19:13:44+00:00"
},
{
"name": "bnussbau/laravel-trmnl-blade",
@@ -1618,16 +1618,16 @@
},
{
"name": "laravel/framework",
- "version": "v12.35.1",
+ "version": "v12.35.0",
"source": {
"type": "git",
"url": "https://github.com/laravel/framework.git",
- "reference": "d6d6e3cb68238e2fb25b440f222442adef5a8a15"
+ "reference": "9583ef9e405a71d5b8c04ff6efd05a7ef9a5baef"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/framework/zipball/d6d6e3cb68238e2fb25b440f222442adef5a8a15",
- "reference": "d6d6e3cb68238e2fb25b440f222442adef5a8a15",
+ "url": "https://api.github.com/repos/laravel/framework/zipball/9583ef9e405a71d5b8c04ff6efd05a7ef9a5baef",
+ "reference": "9583ef9e405a71d5b8c04ff6efd05a7ef9a5baef",
"shasum": ""
},
"require": {
@@ -1833,7 +1833,7 @@
"issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework"
},
- "time": "2025-10-23T15:25:03+00:00"
+ "time": "2025-10-21T15:15:41+00:00"
},
{
"name": "laravel/prompts",
@@ -10471,16 +10471,16 @@
},
{
"name": "rector/rector",
- "version": "2.2.5",
+ "version": "2.2.4",
"source": {
"type": "git",
"url": "https://github.com/rectorphp/rector.git",
- "reference": "fb9418af7777dfb1c87a536dc58398b5b07c74b9"
+ "reference": "904f12f23858ef54ec5782b05cb2979b703cb185"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/rectorphp/rector/zipball/fb9418af7777dfb1c87a536dc58398b5b07c74b9",
- "reference": "fb9418af7777dfb1c87a536dc58398b5b07c74b9",
+ "url": "https://api.github.com/repos/rectorphp/rector/zipball/904f12f23858ef54ec5782b05cb2979b703cb185",
+ "reference": "904f12f23858ef54ec5782b05cb2979b703cb185",
"shasum": ""
},
"require": {
@@ -10519,7 +10519,7 @@
],
"support": {
"issues": "https://github.com/rectorphp/rector/issues",
- "source": "https://github.com/rectorphp/rector/tree/2.2.5"
+ "source": "https://github.com/rectorphp/rector/tree/2.2.4"
},
"funding": [
{
@@ -10527,7 +10527,7 @@
"type": "github"
}
],
- "time": "2025-10-23T11:22:37+00:00"
+ "time": "2025-10-22T07:50:23+00:00"
},
{
"name": "sebastian/cli-parser",
diff --git a/tests/Feature/PluginXmlResponseTest.php b/tests/Feature/PluginXmlResponseTest.php
deleted file mode 100644
index 308d914..0000000
--- a/tests/Feature/PluginXmlResponseTest.php
+++ /dev/null
@@ -1,171 +0,0 @@
- Http::response([
- 'title' => 'Test Data',
- 'items' => [
- ['id' => 1, 'name' => 'Item 1'],
- ['id' => 2, 'name' => 'Item 2'],
- ],
- ], 200, ['Content-Type' => 'application/json']),
- ]);
-
- $plugin = Plugin::factory()->create([
- 'data_strategy' => 'polling',
- 'polling_url' => 'https://example.com/api/data',
- 'polling_verb' => 'get',
- ]);
-
- $plugin->updateDataPayload();
-
- $plugin->refresh();
-
- expect($plugin->data_payload)->toBe([
- 'title' => 'Test Data',
- 'items' => [
- ['id' => 1, 'name' => 'Item 1'],
- ['id' => 2, 'name' => 'Item 2'],
- ],
- ]);
-});
-
-test('plugin parses XML responses and wraps under rss key', function (): void {
- $xmlContent = '
-
-
- Test RSS Feed
- -
- Test Item 1
- Description 1
-
- -
- Test Item 2
- Description 2
-
-
- ';
-
- Http::fake([
- 'example.com/feed.xml' => Http::response($xmlContent, 200, ['Content-Type' => 'application/xml']),
- ]);
-
- $plugin = Plugin::factory()->create([
- 'data_strategy' => 'polling',
- 'polling_url' => 'https://example.com/feed.xml',
- 'polling_verb' => 'get',
- ]);
-
- $plugin->updateDataPayload();
-
- $plugin->refresh();
-
- expect($plugin->data_payload)->toHaveKey('rss');
- expect($plugin->data_payload['rss'])->toHaveKey('@attributes');
- expect($plugin->data_payload['rss'])->toHaveKey('channel');
- expect($plugin->data_payload['rss']['channel']['title'])->toBe('Test RSS Feed');
- expect($plugin->data_payload['rss']['channel']['item'])->toHaveCount(2);
-});
-
-test('plugin handles non-XML content-type as JSON', function (): void {
- $jsonContent = '{"title": "Test Data", "items": [1, 2, 3]}';
-
- Http::fake([
- 'example.com/data' => Http::response($jsonContent, 200, ['Content-Type' => 'text/plain']),
- ]);
-
- $plugin = Plugin::factory()->create([
- 'data_strategy' => 'polling',
- 'polling_url' => 'https://example.com/data',
- 'polling_verb' => 'get',
- ]);
-
- $plugin->updateDataPayload();
-
- $plugin->refresh();
-
- expect($plugin->data_payload)->toBe([
- 'title' => 'Test Data',
- 'items' => [1, 2, 3],
- ]);
-});
-
-test('plugin handles invalid XML gracefully', function (): void {
- $invalidXml = '- unclosed tag';
-
- Http::fake([
- 'example.com/invalid.xml' => Http::response($invalidXml, 200, ['Content-Type' => 'application/xml']),
- ]);
-
- $plugin = Plugin::factory()->create([
- 'data_strategy' => 'polling',
- 'polling_url' => 'https://example.com/invalid.xml',
- 'polling_verb' => 'get',
- ]);
-
- $plugin->updateDataPayload();
-
- $plugin->refresh();
-
- expect($plugin->data_payload)->toBe(['error' => 'Failed to parse XML response']);
-});
-
-test('plugin handles multiple URLs with mixed content types', function (): void {
- $jsonResponse = ['title' => 'JSON Data', 'items' => [1, 2, 3]];
- $xmlContent = '
- XML Data
';
-
- Http::fake([
- 'example.com/json' => Http::response($jsonResponse, 200, ['Content-Type' => 'application/json']),
- 'example.com/xml' => Http::response($xmlContent, 200, ['Content-Type' => 'application/xml']),
- ]);
-
- $plugin = Plugin::factory()->create([
- 'data_strategy' => 'polling',
- 'polling_url' => "https://example.com/json\nhttps://example.com/xml",
- 'polling_verb' => 'get',
- ]);
-
- $plugin->updateDataPayload();
-
- $plugin->refresh();
-
- expect($plugin->data_payload)->toHaveKey('IDX_0');
- expect($plugin->data_payload)->toHaveKey('IDX_1');
-
- // First URL should be JSON
- expect($plugin->data_payload['IDX_0'])->toBe($jsonResponse);
-
- // Second URL should be XML wrapped under rss
- expect($plugin->data_payload['IDX_1'])->toHaveKey('rss');
- expect($plugin->data_payload['IDX_1']['rss']['item'])->toBe('XML Data');
-});
-
-test('plugin handles POST requests with XML responses', function (): void {
- $xmlContent = 'successtest';
-
- Http::fake([
- 'example.com/api' => Http::response($xmlContent, 200, ['Content-Type' => 'application/xml']),
- ]);
-
- $plugin = Plugin::factory()->create([
- 'data_strategy' => 'polling',
- 'polling_url' => 'https://example.com/api',
- 'polling_verb' => 'post',
- 'polling_body' => '{"query": "test"}',
- ]);
-
- $plugin->updateDataPayload();
-
- $plugin->refresh();
-
- expect($plugin->data_payload)->toHaveKey('rss');
- expect($plugin->data_payload['rss'])->toHaveKey('status');
- expect($plugin->data_payload['rss'])->toHaveKey('data');
- expect($plugin->data_payload['rss']['status'])->toBe('success');
- expect($plugin->data_payload['rss']['data'])->toBe('test');
-});