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 <noreply@anthropic.com>
This commit is contained in:
parent
24cb6d61aa
commit
ad15b36f4d
12 changed files with 282 additions and 36 deletions
|
|
@ -45,7 +45,9 @@ class MicropubController extends Controller
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$handler = $this->handlerRegistry->getHandler($type);
|
$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') {
|
if ($result['response'] === 'updated') {
|
||||||
return response()->json([
|
return response()->json([
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,10 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace App\Providers;
|
namespace App\Providers;
|
||||||
|
|
||||||
use App\Services\Micropub\CardHandler;
|
use App\Services\Micropub\Handlers\CardHandler;
|
||||||
use App\Services\Micropub\EntryHandler;
|
use App\Services\Micropub\Handlers\EntryHandler;
|
||||||
|
use App\Services\Micropub\Handlers\UpdateHandler;
|
||||||
use App\Services\Micropub\MicropubHandlerRegistry;
|
use App\Services\Micropub\MicropubHandlerRegistry;
|
||||||
use App\Services\Micropub\UpdateHandler;
|
|
||||||
use Illuminate\Support\ServiceProvider;
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
|
||||||
class MicropubServiceProvider extends ServiceProvider
|
class MicropubServiceProvider extends ServiceProvider
|
||||||
|
|
|
||||||
44
app/Services/Micropub/Data/CardData.php
Normal file
44
app/Services/Micropub/Data/CardData.php
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Services\Micropub\Data;
|
||||||
|
|
||||||
|
class CardData extends MicropubData
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
public readonly array $tokenData,
|
||||||
|
public readonly ?string $name,
|
||||||
|
public readonly ?string $description,
|
||||||
|
public readonly mixed $geo,
|
||||||
|
public readonly mixed $location,
|
||||||
|
public readonly ?string $latitude,
|
||||||
|
public readonly ?string $longitude,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public static function fromArray(array $data): static
|
||||||
|
{
|
||||||
|
return new static(
|
||||||
|
tokenData: $data['token_data'],
|
||||||
|
name: $data['name'] ?? null,
|
||||||
|
description: $data['description'] ?? null,
|
||||||
|
geo: $data['geo'] ?? null,
|
||||||
|
location: $data['location'] ?? null,
|
||||||
|
latitude: $data['latitude'] ?? null,
|
||||||
|
longitude: $data['longitude'] ?? null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function toArray(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'token_data' => $this->tokenData,
|
||||||
|
'name' => $this->name,
|
||||||
|
'description' => $this->description,
|
||||||
|
'geo' => $this->geo,
|
||||||
|
'location' => $this->location,
|
||||||
|
'latitude' => $this->latitude,
|
||||||
|
'longitude' => $this->longitude,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
65
app/Services/Micropub/Data/EntryData.php
Normal file
65
app/Services/Micropub/Data/EntryData.php
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Services\Micropub\Data;
|
||||||
|
|
||||||
|
class EntryData extends MicropubData
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
public readonly array $tokenData,
|
||||||
|
public readonly ?string $content,
|
||||||
|
public readonly ?string $inReplyTo,
|
||||||
|
public readonly ?string $published,
|
||||||
|
public readonly mixed $location,
|
||||||
|
public readonly ?string $bookmarkOf,
|
||||||
|
public readonly ?string $likeOf,
|
||||||
|
public readonly ?array $mpSyndicateTo,
|
||||||
|
public readonly ?string $name,
|
||||||
|
public readonly ?string $description,
|
||||||
|
public readonly mixed $geo,
|
||||||
|
public readonly mixed $checkin,
|
||||||
|
public readonly mixed $syndication,
|
||||||
|
public readonly ?array $photos,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public static function fromArray(array $data): static
|
||||||
|
{
|
||||||
|
return new static(
|
||||||
|
tokenData: $data['token_data'],
|
||||||
|
content: $data['content'] ?? null,
|
||||||
|
inReplyTo: $data['in-reply-to'] ?? null,
|
||||||
|
published: $data['published'] ?? null,
|
||||||
|
location: $data['location'] ?? null,
|
||||||
|
bookmarkOf: $data['bookmark-of'] ?? null,
|
||||||
|
likeOf: $data['like-of'] ?? null,
|
||||||
|
mpSyndicateTo: $data['mp-syndicate-to'] ?? null,
|
||||||
|
name: $data['name'] ?? null,
|
||||||
|
description: $data['description'] ?? null,
|
||||||
|
geo: $data['geo'] ?? null,
|
||||||
|
checkin: $data['checkin'] ?? null,
|
||||||
|
syndication: $data['syndication'] ?? null,
|
||||||
|
photos: $data['photos'] ?? null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function toArray(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'token_data' => $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,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
12
app/Services/Micropub/Data/MicropubData.php
Normal file
12
app/Services/Micropub/Data/MicropubData.php
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Services\Micropub\Data;
|
||||||
|
|
||||||
|
abstract class MicropubData
|
||||||
|
{
|
||||||
|
abstract public static function fromArray(array $data): static;
|
||||||
|
|
||||||
|
abstract public function toArray(): array;
|
||||||
|
}
|
||||||
38
app/Services/Micropub/Data/UpdateData.php
Normal file
38
app/Services/Micropub/Data/UpdateData.php
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Services\Micropub\Data;
|
||||||
|
|
||||||
|
class UpdateData extends MicropubData
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
public readonly array $tokenData,
|
||||||
|
public readonly ?string $updateUrl,
|
||||||
|
public readonly ?array $updateReplace,
|
||||||
|
public readonly ?array $updateAdd,
|
||||||
|
public readonly ?array $updateDelete,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public static function fromArray(array $data): static
|
||||||
|
{
|
||||||
|
return new static(
|
||||||
|
tokenData: $data['token_data'],
|
||||||
|
updateUrl: $data['update_url'] ?? null,
|
||||||
|
updateReplace: $data['update_replace'] ?? null,
|
||||||
|
updateAdd: $data['update_add'] ?? null,
|
||||||
|
updateDelete: $data['update_delete'] ?? null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function toArray(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'token_data' => $this->tokenData,
|
||||||
|
'update_url' => $this->updateUrl,
|
||||||
|
'update_replace' => $this->updateReplace,
|
||||||
|
'update_add' => $this->updateAdd,
|
||||||
|
'update_delete' => $this->updateDelete,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,20 +2,28 @@
|
||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace App\Services\Micropub;
|
namespace App\Services\Micropub\Handlers;
|
||||||
|
|
||||||
use App\Exceptions\InvalidTokenScopeException;
|
use App\Exceptions\InvalidTokenScopeException;
|
||||||
|
use App\Services\Micropub\Data\CardData;
|
||||||
|
use App\Services\Micropub\Data\MicropubData;
|
||||||
use App\Services\PlaceService;
|
use App\Services\PlaceService;
|
||||||
|
|
||||||
class CardHandler implements MicropubHandlerInterface
|
class CardHandler implements MicropubHandlerInterface
|
||||||
{
|
{
|
||||||
|
public function dataClass(): string
|
||||||
|
{
|
||||||
|
return CardData::class;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @throws InvalidTokenScopeException
|
* @throws InvalidTokenScopeException
|
||||||
*/
|
*/
|
||||||
public function handle(array $data): array
|
public function handle(MicropubData $data): array
|
||||||
{
|
{
|
||||||
// Handle h-card requests
|
assert($data instanceof CardData);
|
||||||
$scopes = $data['token_data']['scope'];
|
|
||||||
|
$scopes = $data->tokenData['scope'];
|
||||||
if (is_string($scopes)) {
|
if (is_string($scopes)) {
|
||||||
$scopes = explode(' ', $scopes);
|
$scopes = explode(' ', $scopes);
|
||||||
}
|
}
|
||||||
|
|
@ -24,7 +32,7 @@ class CardHandler implements MicropubHandlerInterface
|
||||||
throw new InvalidTokenScopeException;
|
throw new InvalidTokenScopeException;
|
||||||
}
|
}
|
||||||
|
|
||||||
$location = resolve(PlaceService::class)->createPlace($data)->uri;
|
$location = resolve(PlaceService::class)->createPlace($data->toArray())->uri;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'response' => 'created',
|
'response' => 'created',
|
||||||
|
|
@ -2,22 +2,31 @@
|
||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace App\Services\Micropub;
|
namespace App\Services\Micropub\Handlers;
|
||||||
|
|
||||||
use App\Exceptions\InvalidTokenScopeException;
|
use App\Exceptions\InvalidTokenScopeException;
|
||||||
use App\Services\ArticleService;
|
use App\Services\ArticleService;
|
||||||
use App\Services\BookmarkService;
|
use App\Services\BookmarkService;
|
||||||
use App\Services\LikeService;
|
use App\Services\LikeService;
|
||||||
|
use App\Services\Micropub\Data\EntryData;
|
||||||
|
use App\Services\Micropub\Data\MicropubData;
|
||||||
use App\Services\NoteService;
|
use App\Services\NoteService;
|
||||||
|
|
||||||
class EntryHandler implements MicropubHandlerInterface
|
class EntryHandler implements MicropubHandlerInterface
|
||||||
{
|
{
|
||||||
|
public function dataClass(): string
|
||||||
|
{
|
||||||
|
return EntryData::class;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @throws InvalidTokenScopeException
|
* @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)) {
|
if (is_string($scopes)) {
|
||||||
$scopes = explode(' ', $scopes);
|
$scopes = explode(' ', $scopes);
|
||||||
}
|
}
|
||||||
|
|
@ -26,11 +35,12 @@ class EntryHandler implements MicropubHandlerInterface
|
||||||
throw new InvalidTokenScopeException;
|
throw new InvalidTokenScopeException;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$dataArray = $data->toArray();
|
||||||
$location = match (true) {
|
$location = match (true) {
|
||||||
isset($data['like-of']) => resolve(LikeService::class)->create($data)->url,
|
isset($dataArray['like-of']) => resolve(LikeService::class)->create($dataArray)->url,
|
||||||
isset($data['bookmark-of']) => resolve(BookmarkService::class)->create($data)->uri,
|
isset($dataArray['bookmark-of']) => resolve(BookmarkService::class)->create($dataArray)->uri,
|
||||||
isset($data['name']) => resolve(ArticleService::class)->create($data)->link,
|
isset($dataArray['name']) => resolve(ArticleService::class)->create($dataArray)->link,
|
||||||
default => resolve(NoteService::class)->create($data)->uri,
|
default => resolve(NoteService::class)->create($dataArray)->uri,
|
||||||
};
|
};
|
||||||
|
|
||||||
return [
|
return [
|
||||||
44
app/Services/Micropub/Handlers/MicropubHandlerInterface.php
Normal file
44
app/Services/Micropub/Handlers/MicropubHandlerInterface.php
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Services\Micropub\Handlers;
|
||||||
|
|
||||||
|
use App\Services\Micropub\Data\MicropubData;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contract for all Micropub request handlers.
|
||||||
|
*
|
||||||
|
* The Micropub endpoint receives different types of request — creating an
|
||||||
|
* h-entry (note, article, bookmark, like), creating an h-card (place), or
|
||||||
|
* updating an existing post. Each type is handled by its own class that
|
||||||
|
* implements this interface.
|
||||||
|
*
|
||||||
|
* Each handler declares the typed data object it needs via dataClass(). The
|
||||||
|
* controller calls dataClass()::fromArray() to build the right object before
|
||||||
|
* passing it to handle(), so the handler always receives a concrete typed
|
||||||
|
* object rather than a raw array.
|
||||||
|
*
|
||||||
|
* Handlers live in App\Services\Micropub\Handlers.
|
||||||
|
* Their corresponding data objects live in App\Services\Micropub\Data.
|
||||||
|
* Handlers are registered in MicropubServiceProvider and looked up by type
|
||||||
|
* string (e.g. "entry", "card", "update") via MicropubHandlerRegistry.
|
||||||
|
*/
|
||||||
|
interface MicropubHandlerInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Return the fully-qualified class name of the data object this handler
|
||||||
|
* expects. The controller will call fromArray() on this class to build the
|
||||||
|
* object from the normalised request data before calling handle().
|
||||||
|
*
|
||||||
|
* @return class-string<MicropubData>
|
||||||
|
*/
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
@ -2,26 +2,34 @@
|
||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace App\Services\Micropub;
|
namespace App\Services\Micropub\Handlers;
|
||||||
|
|
||||||
use App\Exceptions\InvalidTokenScopeException;
|
use App\Exceptions\InvalidTokenScopeException;
|
||||||
use App\Exceptions\MicropubHandlerException;
|
use App\Exceptions\MicropubHandlerException;
|
||||||
use App\Exceptions\MicropubUnsupportedModelException;
|
use App\Exceptions\MicropubUnsupportedModelException;
|
||||||
use App\Models\Media;
|
use App\Models\Media;
|
||||||
use App\Models\Note;
|
use App\Models\Note;
|
||||||
use Illuminate\Support\Arr;
|
use App\Services\Micropub\Data\MicropubData;
|
||||||
|
use App\Services\Micropub\Data\UpdateData;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
class UpdateHandler implements MicropubHandlerInterface
|
class UpdateHandler implements MicropubHandlerInterface
|
||||||
{
|
{
|
||||||
|
public function dataClass(): string
|
||||||
|
{
|
||||||
|
return UpdateData::class;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @throws InvalidTokenScopeException
|
* @throws InvalidTokenScopeException
|
||||||
* @throws MicropubUnsupportedModelException
|
* @throws MicropubUnsupportedModelException
|
||||||
* @throws MicropubHandlerException
|
* @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)) {
|
if (is_string($scopes)) {
|
||||||
$scopes = explode(' ', $scopes);
|
$scopes = explode(' ', $scopes);
|
||||||
}
|
}
|
||||||
|
|
@ -30,7 +38,7 @@ class UpdateHandler implements MicropubHandlerInterface
|
||||||
throw new InvalidTokenScopeException;
|
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') {
|
if (mb_substr($urlPath, 1, 5) !== 'notes') {
|
||||||
throw new MicropubUnsupportedModelException('This implementation currently only supports the updating of 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();
|
$note = Note::nb60(basename($urlPath))->firstOrFail();
|
||||||
|
|
||||||
if (Arr::get($data, 'update_replace')) {
|
if ($data->updateReplace !== null) {
|
||||||
foreach (Arr::get($data, 'update_replace') as $property => $value) {
|
foreach ($data->updateReplace as $property => $value) {
|
||||||
if ($property === 'content') {
|
if ($property === 'content') {
|
||||||
$note->note = $value[0];
|
$note->note = $value[0];
|
||||||
}
|
}
|
||||||
|
|
@ -55,8 +63,8 @@ class UpdateHandler implements MicropubHandlerInterface
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Arr::get($data, 'update_add')) {
|
if ($data->updateAdd !== null) {
|
||||||
foreach (Arr::get($data, 'update_add') as $property => $value) {
|
foreach ($data->updateAdd as $property => $value) {
|
||||||
if ($property === 'syndication') {
|
if ($property === 'syndication') {
|
||||||
$this->applySyndication($note, $value);
|
$this->applySyndication($note, $value);
|
||||||
}
|
}
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Services\Micropub;
|
|
||||||
|
|
||||||
interface MicropubHandlerInterface
|
|
||||||
{
|
|
||||||
public function handle(array $data);
|
|
||||||
}
|
|
||||||
|
|
@ -5,7 +5,27 @@ declare(strict_types=1);
|
||||||
namespace App\Services\Micropub;
|
namespace App\Services\Micropub;
|
||||||
|
|
||||||
use App\Exceptions\MicropubHandlerException;
|
use App\Exceptions\MicropubHandlerException;
|
||||||
|
use App\Services\Micropub\Handlers\MicropubHandlerInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps Micropub post types to their handler instances.
|
||||||
|
*
|
||||||
|
* MicropubRequest normalises every incoming request and resolves it to a type
|
||||||
|
* string ("entry", "card", "update"). The controller asks the registry for the
|
||||||
|
* right handler, then asks the handler which data class to build, and finally
|
||||||
|
* calls handle() with that data object.
|
||||||
|
*
|
||||||
|
* Flow:
|
||||||
|
* MicropubRequest (normalise) → MicropubController
|
||||||
|
* → MicropubHandlerRegistry::getHandler($type)
|
||||||
|
* → $handler->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
|
class MicropubHandlerRegistry
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
|
|
@ -13,6 +33,9 @@ class MicropubHandlerRegistry
|
||||||
*/
|
*/
|
||||||
protected array $handlers = [];
|
protected array $handlers = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a handler for a given Micropub type string.
|
||||||
|
*/
|
||||||
public function register(string $type, MicropubHandlerInterface $handler): self
|
public function register(string $type, MicropubHandlerInterface $handler): self
|
||||||
{
|
{
|
||||||
$this->handlers[$type] = $handler;
|
$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
|
* @throws MicropubHandlerException
|
||||||
*/
|
*/
|
||||||
public function getHandler(string $type): MicropubHandlerInterface
|
public function getHandler(string $type): MicropubHandlerInterface
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue