From ad15b36f4d00550866d13496287e28b224ab0a53 Mon Sep 17 00:00:00 2001 From: Jonny Barnes Date: Sun, 22 Feb 2026 10:41:33 +0000 Subject: [PATCH] Introduce typed DTOs and sub-namespaces for Micropub handlers Each handler now declares the data class it needs via dataClass(). The controller builds the appropriate typed DTO from the raw request array before calling handle(), giving handlers typed property access instead of raw array lookups. Handlers moved to App\Services\Micropub\Handlers, data objects to App\Services\Micropub\Data. MicropubHandlerRegistry and the interface are documented with flow diagrams and guidance for adding new types. Co-Authored-By: Claude Sonnet 4.6 --- app/Http/Controllers/MicropubController.php | 4 +- app/Providers/MicropubServiceProvider.php | 6 +- app/Services/Micropub/Data/CardData.php | 44 +++++++++++++ app/Services/Micropub/Data/EntryData.php | 65 +++++++++++++++++++ app/Services/Micropub/Data/MicropubData.php | 12 ++++ app/Services/Micropub/Data/UpdateData.php | 38 +++++++++++ .../Micropub/{ => Handlers}/CardHandler.php | 18 +++-- .../Micropub/{ => Handlers}/EntryHandler.php | 24 +++++-- .../Handlers/MicropubHandlerInterface.php | 44 +++++++++++++ .../Micropub/{ => Handlers}/UpdateHandler.php | 26 +++++--- .../Micropub/MicropubHandlerInterface.php | 10 --- .../Micropub/MicropubHandlerRegistry.php | 25 +++++++ 12 files changed, 281 insertions(+), 35 deletions(-) create mode 100644 app/Services/Micropub/Data/CardData.php create mode 100644 app/Services/Micropub/Data/EntryData.php create mode 100644 app/Services/Micropub/Data/MicropubData.php create mode 100644 app/Services/Micropub/Data/UpdateData.php rename app/Services/Micropub/{ => Handlers}/CardHandler.php (61%) rename app/Services/Micropub/{ => Handlers}/EntryHandler.php (51%) create mode 100644 app/Services/Micropub/Handlers/MicropubHandlerInterface.php rename app/Services/Micropub/{ => Handlers}/UpdateHandler.php (80%) delete mode 100644 app/Services/Micropub/MicropubHandlerInterface.php diff --git a/app/Http/Controllers/MicropubController.php b/app/Http/Controllers/MicropubController.php index be44b371..a1e81e36 100644 --- a/app/Http/Controllers/MicropubController.php +++ b/app/Http/Controllers/MicropubController.php @@ -45,7 +45,9 @@ class MicropubController extends Controller try { $handler = $this->handlerRegistry->getHandler($type); - $result = $handler->handle($request->getMicropubData()); + $dataClass = $handler->dataClass(); + $data = $dataClass::fromArray($request->getMicropubData()); + $result = $handler->handle($data); if ($result['response'] === 'updated') { return response()->json([ diff --git a/app/Providers/MicropubServiceProvider.php b/app/Providers/MicropubServiceProvider.php index e217827f..b8ff479c 100644 --- a/app/Providers/MicropubServiceProvider.php +++ b/app/Providers/MicropubServiceProvider.php @@ -4,10 +4,10 @@ declare(strict_types=1); namespace App\Providers; -use App\Services\Micropub\CardHandler; -use App\Services\Micropub\EntryHandler; +use App\Services\Micropub\Handlers\CardHandler; +use App\Services\Micropub\Handlers\EntryHandler; +use App\Services\Micropub\Handlers\UpdateHandler; use App\Services\Micropub\MicropubHandlerRegistry; -use App\Services\Micropub\UpdateHandler; use Illuminate\Support\ServiceProvider; class MicropubServiceProvider extends ServiceProvider diff --git a/app/Services/Micropub/Data/CardData.php b/app/Services/Micropub/Data/CardData.php new file mode 100644 index 00000000..cee41109 --- /dev/null +++ b/app/Services/Micropub/Data/CardData.php @@ -0,0 +1,44 @@ + $this->tokenData, + 'name' => $this->name, + 'description' => $this->description, + 'geo' => $this->geo, + 'location' => $this->location, + 'latitude' => $this->latitude, + 'longitude' => $this->longitude, + ]; + } +} diff --git a/app/Services/Micropub/Data/EntryData.php b/app/Services/Micropub/Data/EntryData.php new file mode 100644 index 00000000..cf87fdc5 --- /dev/null +++ b/app/Services/Micropub/Data/EntryData.php @@ -0,0 +1,65 @@ + $this->tokenData, + 'content' => $this->content, + 'in-reply-to' => $this->inReplyTo, + 'published' => $this->published, + 'location' => $this->location, + 'bookmark-of' => $this->bookmarkOf, + 'like-of' => $this->likeOf, + 'mp-syndicate-to' => $this->mpSyndicateTo, + 'name' => $this->name, + 'description' => $this->description, + 'geo' => $this->geo, + 'checkin' => $this->checkin, + 'syndication' => $this->syndication, + 'photos' => $this->photos, + ]; + } +} diff --git a/app/Services/Micropub/Data/MicropubData.php b/app/Services/Micropub/Data/MicropubData.php new file mode 100644 index 00000000..3fabcadc --- /dev/null +++ b/app/Services/Micropub/Data/MicropubData.php @@ -0,0 +1,12 @@ + $this->tokenData, + 'update_url' => $this->updateUrl, + 'update_replace' => $this->updateReplace, + 'update_add' => $this->updateAdd, + 'update_delete' => $this->updateDelete, + ]; + } +} diff --git a/app/Services/Micropub/CardHandler.php b/app/Services/Micropub/Handlers/CardHandler.php similarity index 61% rename from app/Services/Micropub/CardHandler.php rename to app/Services/Micropub/Handlers/CardHandler.php index 12e283be..02e3a066 100644 --- a/app/Services/Micropub/CardHandler.php +++ b/app/Services/Micropub/Handlers/CardHandler.php @@ -2,20 +2,28 @@ declare(strict_types=1); -namespace App\Services\Micropub; +namespace App\Services\Micropub\Handlers; use App\Exceptions\InvalidTokenScopeException; +use App\Services\Micropub\Data\CardData; +use App\Services\Micropub\Data\MicropubData; use App\Services\PlaceService; class CardHandler implements MicropubHandlerInterface { + public function dataClass(): string + { + return CardData::class; + } + /** * @throws InvalidTokenScopeException */ - public function handle(array $data): array + public function handle(MicropubData $data): array { - // Handle h-card requests - $scopes = $data['token_data']['scope']; + assert($data instanceof CardData); + + $scopes = $data->tokenData['scope']; if (is_string($scopes)) { $scopes = explode(' ', $scopes); } @@ -24,7 +32,7 @@ class CardHandler implements MicropubHandlerInterface throw new InvalidTokenScopeException; } - $location = resolve(PlaceService::class)->createPlace($data)->uri; + $location = resolve(PlaceService::class)->createPlace($data->toArray())->uri; return [ 'response' => 'created', diff --git a/app/Services/Micropub/EntryHandler.php b/app/Services/Micropub/Handlers/EntryHandler.php similarity index 51% rename from app/Services/Micropub/EntryHandler.php rename to app/Services/Micropub/Handlers/EntryHandler.php index 9cdbe789..ef9740f2 100644 --- a/app/Services/Micropub/EntryHandler.php +++ b/app/Services/Micropub/Handlers/EntryHandler.php @@ -2,22 +2,31 @@ declare(strict_types=1); -namespace App\Services\Micropub; +namespace App\Services\Micropub\Handlers; use App\Exceptions\InvalidTokenScopeException; use App\Services\ArticleService; use App\Services\BookmarkService; use App\Services\LikeService; +use App\Services\Micropub\Data\EntryData; +use App\Services\Micropub\Data\MicropubData; use App\Services\NoteService; class EntryHandler implements MicropubHandlerInterface { + public function dataClass(): string + { + return EntryData::class; + } + /** * @throws InvalidTokenScopeException */ - public function handle(array $data) + public function handle(MicropubData $data): array { - $scopes = $data['token_data']['scope']; + assert($data instanceof EntryData); + + $scopes = $data->tokenData['scope']; if (is_string($scopes)) { $scopes = explode(' ', $scopes); } @@ -26,11 +35,12 @@ class EntryHandler implements MicropubHandlerInterface throw new InvalidTokenScopeException; } + $dataArray = $data->toArray(); $location = match (true) { - isset($data['like-of']) => resolve(LikeService::class)->create($data)->url, - isset($data['bookmark-of']) => resolve(BookmarkService::class)->create($data)->uri, - isset($data['name']) => resolve(ArticleService::class)->create($data)->link, - default => resolve(NoteService::class)->create($data)->uri, + isset($dataArray['like-of']) => resolve(LikeService::class)->create($dataArray)->url, + isset($dataArray['bookmark-of']) => resolve(BookmarkService::class)->create($dataArray)->uri, + isset($dataArray['name']) => resolve(ArticleService::class)->create($dataArray)->link, + default => resolve(NoteService::class)->create($dataArray)->uri, }; return [ diff --git a/app/Services/Micropub/Handlers/MicropubHandlerInterface.php b/app/Services/Micropub/Handlers/MicropubHandlerInterface.php new file mode 100644 index 00000000..6afddc5f --- /dev/null +++ b/app/Services/Micropub/Handlers/MicropubHandlerInterface.php @@ -0,0 +1,44 @@ + + */ + public function dataClass(): string; + + /** + * Process the request and return a result array with at minimum a + * 'response' key ('created' or 'updated') and a 'url' key pointing to + * the affected resource. + */ + public function handle(MicropubData $data): array; +} diff --git a/app/Services/Micropub/UpdateHandler.php b/app/Services/Micropub/Handlers/UpdateHandler.php similarity index 80% rename from app/Services/Micropub/UpdateHandler.php rename to app/Services/Micropub/Handlers/UpdateHandler.php index d4558547..fb633c8b 100644 --- a/app/Services/Micropub/UpdateHandler.php +++ b/app/Services/Micropub/Handlers/UpdateHandler.php @@ -2,26 +2,34 @@ declare(strict_types=1); -namespace App\Services\Micropub; +namespace App\Services\Micropub\Handlers; use App\Exceptions\InvalidTokenScopeException; use App\Exceptions\MicropubHandlerException; use App\Exceptions\MicropubUnsupportedModelException; use App\Models\Media; use App\Models\Note; -use Illuminate\Support\Arr; +use App\Services\Micropub\Data\MicropubData; +use App\Services\Micropub\Data\UpdateData; use Illuminate\Support\Str; class UpdateHandler implements MicropubHandlerInterface { + public function dataClass(): string + { + return UpdateData::class; + } + /** * @throws InvalidTokenScopeException * @throws MicropubUnsupportedModelException * @throws MicropubHandlerException */ - public function handle(array $data): array + public function handle(MicropubData $data): array { - $scopes = $data['token_data']['scope']; + assert($data instanceof UpdateData); + + $scopes = $data->tokenData['scope']; if (is_string($scopes)) { $scopes = explode(' ', $scopes); } @@ -30,7 +38,7 @@ class UpdateHandler implements MicropubHandlerInterface throw new InvalidTokenScopeException; } - $urlPath = parse_url(Arr::get($data, 'update_url'), PHP_URL_PATH); + $urlPath = parse_url($data->updateUrl, PHP_URL_PATH); if (mb_substr($urlPath, 1, 5) !== 'notes') { throw new MicropubUnsupportedModelException('This implementation currently only supports the updating of notes'); @@ -38,8 +46,8 @@ class UpdateHandler implements MicropubHandlerInterface $note = Note::nb60(basename($urlPath))->firstOrFail(); - if (Arr::get($data, 'update_replace')) { - foreach (Arr::get($data, 'update_replace') as $property => $value) { + if ($data->updateReplace !== null) { + foreach ($data->updateReplace as $property => $value) { if ($property === 'content') { $note->note = $value[0]; } @@ -55,8 +63,8 @@ class UpdateHandler implements MicropubHandlerInterface ]; } - if (Arr::get($data, 'update_add')) { - foreach (Arr::get($data, 'update_add') as $property => $value) { + if ($data->updateAdd !== null) { + foreach ($data->updateAdd as $property => $value) { if ($property === 'syndication') { $this->applySyndication($note, $value); } diff --git a/app/Services/Micropub/MicropubHandlerInterface.php b/app/Services/Micropub/MicropubHandlerInterface.php deleted file mode 100644 index 82040be9..00000000 --- a/app/Services/Micropub/MicropubHandlerInterface.php +++ /dev/null @@ -1,10 +0,0 @@ -dataClass()::fromArray($rawData) + * → $handler->handle($dataObject) + * + * Handlers are registered in MicropubServiceProvider. To support a new + * Micropub post type, create a handler in App\Services\Micropub\Handlers, a + * matching data class in App\Services\Micropub\Data, and register the handler + * here with its type string. + */ class MicropubHandlerRegistry { /** @@ -13,6 +33,9 @@ class MicropubHandlerRegistry */ protected array $handlers = []; + /** + * Register a handler for a given Micropub type string. + */ public function register(string $type, MicropubHandlerInterface $handler): self { $this->handlers[$type] = $handler; @@ -21,6 +44,8 @@ class MicropubHandlerRegistry } /** + * Retrieve the handler for a given type, or throw if none is registered. + * * @throws MicropubHandlerException */ public function getHandler(string $type): MicropubHandlerInterface