chore: drop old package

This commit is contained in:
Sami Mazouz 2024-03-05 13:49:42 +01:00
parent fdf7b9a082
commit 3493dc80d0
No known key found for this signature in database
60 changed files with 313 additions and 2098 deletions

View File

@ -150,7 +150,6 @@
"pusher/pusher-php-server": "^7.2",
"s9e/text-formatter": "^2.13",
"staudenmeir/eloquent-eager-limit": "^1.8.2",
"sycho/json-api": "^0.5.0",
"sycho/sourcemap": "^2.0.0",
"symfony/config": "^6.3",
"symfony/console": "^6.3",

View File

@ -45,13 +45,13 @@ return [
->fields(PostResourceFields::class)
->endpoint(
[Endpoint\Index::class, Endpoint\Show::class, Endpoint\Create::class, Endpoint\Update::class],
function (Endpoint\Index|Endpoint\Show|Endpoint\Create|Endpoint\Update $endpoint): Endpoint\Endpoint {
function (Endpoint\Index|Endpoint\Show|Endpoint\Create|Endpoint\Update $endpoint): Endpoint\EndpointInterface {
return $endpoint->addDefaultInclude(['likes']);
}
),
(new Extend\ApiResource(Resource\DiscussionResource::class))
->endpoint(Endpoint\Show::class, function (Endpoint\Show $endpoint): Endpoint\Endpoint {
->endpoint(Endpoint\Show::class, function (Endpoint\Show $endpoint): Endpoint\EndpointInterface {
return $endpoint->addDefaultInclude(['posts.likes']);
}),

View File

@ -63,7 +63,7 @@ return [
(new Extend\ApiResource(Resource\PostResource::class))
->fields(PostResourceFields::class)
->endpoint([Endpoint\Index::class, Endpoint\Show::class], function (Endpoint\Index|Endpoint\Show $endpoint): Endpoint\Endpoint {
->endpoint([Endpoint\Index::class, Endpoint\Show::class], function (Endpoint\Index|Endpoint\Show $endpoint): Endpoint\EndpointInterface {
return $endpoint->addDefaultInclude(['mentionedBy', 'mentionedBy.user', 'mentionedBy.discussion']);
})
->endpoint(Endpoint\Index::class, function (Endpoint\Index $endpoint): Endpoint\Index {
@ -131,7 +131,7 @@ return [
}),
(new Extend\ApiResource(Resource\PostResource::class))
->endpoint([Endpoint\Index::class, Endpoint\Show::class], function (Endpoint\Index|Endpoint\Show $endpoint): Endpoint\Endpoint {
->endpoint([Endpoint\Index::class, Endpoint\Show::class], function (Endpoint\Index|Endpoint\Show $endpoint): Endpoint\EndpointInterface {
return $endpoint->eagerLoad(['mentionsTags']);
}),
]),

View File

@ -12,6 +12,7 @@ namespace Flarum\Statistics\Api\Controller;
use Carbon\Carbon;
use DateTime;
use Flarum\Discussion\Discussion;
use Flarum\Http\Exception\InvalidParameterException;
use Flarum\Http\RequestUtil;
use Flarum\Post\Post;
use Flarum\Post\RegisteredTypesScope;
@ -24,7 +25,6 @@ use Laminas\Diactoros\Response\JsonResponse;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Tobscure\JsonApi\Exception\InvalidParameterException;
class ShowStatisticsData implements RequestHandlerInterface
{

View File

@ -40,7 +40,7 @@ class TagResource extends AbstractDatabaseResource
}
}
public function find(string $id, \Tobyz\JsonApiServer\Context $context): ?object
public function find(string $id, Context $context): ?object
{
$actor = $context->getActor();

View File

@ -78,7 +78,6 @@
"psr/http-server-middleware": "^1.0.2",
"s9e/text-formatter": "^2.13",
"staudenmeir/eloquent-eager-limit": "^1.8.2",
"sycho/json-api": "^0.5.0",
"sycho/sourcemap": "^2.0.0",
"symfony/config": "^6.3",
"symfony/console": "^6.3",

View File

@ -10,6 +10,7 @@
namespace Flarum\Api;
use Flarum\Api\Controller\AbstractSerializeController;
use Flarum\Api\Endpoint\EndpointInterface;
use Flarum\Api\Serializer\AbstractSerializer;
use Flarum\Api\Serializer\BasicDiscussionSerializer;
use Flarum\Api\Serializer\NotificationSerializer;
@ -24,6 +25,7 @@ use Flarum\Http\UrlGenerator;
use Illuminate\Contracts\Container\Container;
use Laminas\Stratigility\MiddlewarePipe;
use ReflectionClass;
use Tobyz\JsonApiServer\Endpoint\Endpoint;
class ApiServiceProvider extends AbstractServiceProvider
{
@ -42,6 +44,8 @@ class ApiServiceProvider extends AbstractServiceProvider
Resource\DiscussionResource::class,
Resource\NotificationResource::class,
Resource\AccessTokenResource::class,
Resource\MailSettingResource::class,
Resource\ExtensionReadmeResource::class,
];
});
@ -155,8 +159,7 @@ class ApiServiceProvider extends AbstractServiceProvider
public function boot(Container $container): void
{
AbstractSerializeController::setContainer($container);
AbstractSerializer::setContainer($container);
//
}
protected function populateRoutes(RouteCollection $routes, Container $container): void
@ -186,14 +189,16 @@ class ApiServiceProvider extends AbstractServiceProvider
* None of the injected dependencies should be directly used within
* the `endpoints` method. Encourage using callbacks.
*
* @var \Flarum\Api\Endpoint\Endpoint[] $endpoints
* @var array<Endpoint&EndpointInterface> $endpoints
*/
$endpoints = $resource->resolveEndpoints(true);
foreach ($endpoints as $endpoint) {
$route = $endpoint->route();
$method = $endpoint->method;
$path = rtrim("/$type$endpoint->path", '/');
$name = "$type.$endpoint->name";
$routes->addRoute($route->method, rtrim("/$type$route->path", '/'), "$type.$route->name", $factory->toApiResource($resource::class, $endpoint::class));
$routes->addRoute($method, $path, $name, $factory->toApiResource($resource::class, $endpoint->name));
}
}
}

View File

@ -13,7 +13,6 @@ use Tobyz\JsonApiServer\Resource\Resource;
class Context extends BaseContext
{
protected ?SearchResults $search = null;
protected int|string|null $modelId = null;
/**
* Data passed internally when reusing resource endpoint logic.
@ -26,13 +25,6 @@ class Context extends BaseContext
*/
protected array $parameters = [];
public function withModelId(int|string|null $id): static
{
$new = clone $this;
$new->modelId = $id;
return $new;
}
public function withSearchResults(SearchResults $search): static
{
$new = clone $this;
@ -47,11 +39,6 @@ class Context extends BaseContext
return $new;
}
public function getModelId(): int|string|null
{
return $this->modelId;
}
public function getSearchResults(): ?SearchResults
{
return $this->search;

View File

@ -1,21 +0,0 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\Api\Controller;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
abstract class AbstractCreateController extends AbstractShowController
{
public function handle(ServerRequestInterface $request): ResponseInterface
{
return parent::handle($request)->withStatus(201);
}
}

View File

@ -1,46 +0,0 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\Api\Controller;
use Psr\Http\Message\ServerRequestInterface;
use Tobscure\JsonApi\Collection;
use Tobscure\JsonApi\Document;
use Tobscure\JsonApi\ElementInterface;
use Tobscure\JsonApi\SerializerInterface;
abstract class AbstractListController extends AbstractSerializeController
{
protected function createElement(mixed $data, SerializerInterface $serializer): ElementInterface
{
return new Collection($data, $serializer);
}
abstract protected function data(ServerRequestInterface $request, Document $document): iterable;
protected function addPaginationData(Document $document, ServerRequestInterface $request, string $url, ?int $total): void
{
$limit = $this->extractLimit($request);
$offset = $this->extractOffset($request);
$document->addPaginationLinks(
$url,
$request->getQueryParams(),
$offset,
$limit,
$total,
);
$document->setMeta([
'total' => $total,
'perPage' => $limit,
'page' => $offset / $limit + 1,
]);
}
}

View File

@ -1,432 +0,0 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\Api\Controller;
use Flarum\Api\JsonApiResponse;
use Flarum\Api\Serializer\AbstractSerializer;
use Illuminate\Contracts\Container\Container;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
use InvalidArgumentException;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Tobscure\JsonApi\Document;
use Tobscure\JsonApi\ElementInterface;
use Tobscure\JsonApi\Parameters;
use Tobscure\JsonApi\SerializerInterface;
abstract class AbstractSerializeController implements RequestHandlerInterface
{
/**
* The name of the serializer class to output results with.
*
* @var class-string<AbstractSerializer>|null
*/
public ?string $serializer;
/**
* The relationships that are included by default.
*
* @var string[]
*/
public array $include = [];
/**
* The relationships that are available to be included.
*
* @var string[]
*/
public array $optionalInclude = [];
/**
* The maximum number of records that can be requested.
*/
public int $maxLimit = 50;
/**
* The number of records included by default.
*/
public int $limit = 20;
/**
* The fields that are available to be sorted by.
*
* @var string[]
*/
public array $sortFields = [];
/**
* The default sort field and order to use.
*
* @var array<string, string>|null
*/
public ?array $sort = null;
protected static Container $container;
/**
* @var array<class-string<self>, callable[]>
*/
protected static array $beforeDataCallbacks = [];
/**
* @var array<class-string<self>, callable[]>
*/
protected static array $beforeSerializationCallbacks = [];
/**
* @var string[][]
*/
protected static array $loadRelations = [];
/**
* @var array<string, callable>
*/
protected static array $loadRelationCallables = [];
public function handle(ServerRequestInterface $request): ResponseInterface
{
$document = new Document;
foreach (array_reverse(array_merge([static::class], class_parents($this))) as $class) {
if (isset(static::$beforeDataCallbacks[$class])) {
foreach (static::$beforeDataCallbacks[$class] as $callback) {
$callback($this);
}
}
}
$data = $this->data($request, $document);
foreach (array_reverse(array_merge([static::class], class_parents($this))) as $class) {
if (isset(static::$beforeSerializationCallbacks[$class])) {
foreach (static::$beforeSerializationCallbacks[$class] as $callback) {
$callback($this, $data, $request, $document);
}
}
}
if (empty($this->serializer)) {
throw new InvalidArgumentException('Serializer required for controller: '.static::class);
}
$serializer = static::$container->make($this->serializer);
$serializer->setRequest($request);
$element = $this->createElement($data, $serializer)
->with($this->extractInclude($request))
->fields($this->extractFields($request));
$document->setData($element);
return new JsonApiResponse($document);
}
/**
* Get the data to be serialized and assigned to the response document.
*/
abstract protected function data(ServerRequestInterface $request, Document $document): mixed;
/**
* Create a PHP JSON-API Element for output in the document.
*/
abstract protected function createElement(mixed $data, SerializerInterface $serializer): ElementInterface;
/**
* Returns the relations to load added by extenders.
*
* @return string[]
*/
protected function getRelationsToLoad(Collection $models): array
{
$addedRelations = [];
foreach (array_reverse(array_merge([static::class], class_parents($this))) as $class) {
if (isset(static::$loadRelations[$class])) {
$addedRelations = array_merge($addedRelations, static::$loadRelations[$class]);
}
}
return $addedRelations;
}
/**
* Returns the relation callables to load added by extenders.
*
* @return array<string, callable>
*/
protected function getRelationCallablesToLoad(Collection $models): array
{
$addedRelationCallables = [];
foreach (array_reverse(array_merge([static::class], class_parents($this))) as $class) {
if (isset(static::$loadRelationCallables[$class])) {
$addedRelationCallables = array_merge($addedRelationCallables, static::$loadRelationCallables[$class]);
}
}
return $addedRelationCallables;
}
/**
* Eager loads the required relationships.
*/
protected function loadRelations(Collection $models, array $relations, ServerRequestInterface $request = null): void
{
$addedRelations = $this->getRelationsToLoad($models);
$addedRelationCallables = $this->getRelationCallablesToLoad($models);
foreach ($addedRelationCallables as $name => $relation) {
$addedRelations[] = $name;
}
if (! empty($addedRelations)) {
usort($addedRelations, function ($a, $b) {
return substr_count($a, '.') - substr_count($b, '.');
});
foreach ($addedRelations as $relation) {
if (str_contains($relation, '.')) {
$parentRelation = Str::beforeLast($relation, '.');
if (! in_array($parentRelation, $relations, true)) {
continue;
}
}
$relations[] = $relation;
}
}
if (! empty($relations)) {
$relations = array_unique($relations);
}
$callableRelations = [];
$nonCallableRelations = [];
foreach ($relations as $relation) {
if (isset($addedRelationCallables[$relation])) {
$load = $addedRelationCallables[$relation];
$callableRelations[$relation] = function ($query) use ($load, $request, $relations) {
$load($query, $request, $relations);
};
} else {
$nonCallableRelations[] = $relation;
}
}
if (! empty($callableRelations)) {
$models->loadMissing($callableRelations);
}
if (! empty($nonCallableRelations)) {
$models->loadMissing($nonCallableRelations);
}
}
/**
* @throws \Tobscure\JsonApi\Exception\InvalidParameterException
*/
protected function extractInclude(ServerRequestInterface $request): array
{
$available = array_merge($this->include, $this->optionalInclude);
return $this->buildParameters($request)->getInclude($available) ?: $this->include;
}
protected function extractFields(ServerRequestInterface $request): array
{
return $this->buildParameters($request)->getFields();
}
/**
* @throws \Tobscure\JsonApi\Exception\InvalidParameterException
*/
protected function extractSort(ServerRequestInterface $request): ?array
{
return $this->buildParameters($request)->getSort($this->sortFields) ?: $this->sort;
}
/**
* @throws \Tobscure\JsonApi\Exception\InvalidParameterException
*/
protected function extractOffset(ServerRequestInterface $request): int
{
return (int) $this->buildParameters($request)->getOffset($this->extractLimit($request)) ?: 0;
}
/**
* @throws \Tobscure\JsonApi\Exception\InvalidParameterException
*/
protected function extractLimit(ServerRequestInterface $request): int
{
return (int) $this->buildParameters($request)->getLimit($this->maxLimit) ?: $this->limit;
}
protected function extractFilter(ServerRequestInterface $request): array
{
return $this->buildParameters($request)->getFilter() ?: [];
}
protected function buildParameters(ServerRequestInterface $request): Parameters
{
return new Parameters($request->getQueryParams());
}
protected function sortIsDefault(ServerRequestInterface $request): bool
{
return ! Arr::get($request->getQueryParams(), 'sort');
}
/**
* Set the serializer that will serialize data for the endpoint.
*/
public function setSerializer(string $serializer): void
{
$this->serializer = $serializer;
}
/**
* Include the given relationship by default.
*/
public function addInclude(array|string $name): void
{
$this->include = array_merge($this->include, (array) $name);
}
/**
* Don't include the given relationship by default.
*/
public function removeInclude(array|string $name): void
{
$this->include = array_diff($this->include, (array) $name);
}
/**
* Make the given relationship available for inclusion.
*/
public function addOptionalInclude(array|string $name): void
{
$this->optionalInclude = array_merge($this->optionalInclude, (array) $name);
}
/**
* Don't allow the given relationship to be included.
*/
public function removeOptionalInclude(array|string $name): void
{
$this->optionalInclude = array_diff($this->optionalInclude, (array) $name);
}
/**
* Set the default number of results.
*/
public function setLimit(int $limit): void
{
$this->limit = $limit;
}
/**
* Set the maximum number of results.
*/
public function setMaxLimit(int $max): void
{
$this->maxLimit = $max;
}
/**
* Allow sorting results by the given field.
*/
public function addSortField(array|string $field): void
{
$this->sortFields = array_merge($this->sortFields, (array) $field);
}
/**
* Disallow sorting results by the given field.
*/
public function removeSortField(array|string $field): void
{
$this->sortFields = array_diff($this->sortFields, (array) $field);
}
/**
* Set the default sort order for the results.
*/
public function setSort(array $sort): void
{
$this->sort = $sort;
}
public static function getContainer(): Container
{
return static::$container;
}
/**
* @internal
*/
public static function setContainer(Container $container): void
{
static::$container = $container;
}
/**
* @internal
*/
public static function addDataPreparationCallback(string $controllerClass, callable $callback): void
{
if (! isset(static::$beforeDataCallbacks[$controllerClass])) {
static::$beforeDataCallbacks[$controllerClass] = [];
}
static::$beforeDataCallbacks[$controllerClass][] = $callback;
}
/**
* @internal
*/
public static function addSerializationPreparationCallback(string $controllerClass, callable $callback): void
{
if (! isset(static::$beforeSerializationCallbacks[$controllerClass])) {
static::$beforeSerializationCallbacks[$controllerClass] = [];
}
static::$beforeSerializationCallbacks[$controllerClass][] = $callback;
}
/**
* @internal
*/
public static function setLoadRelations(string $controllerClass, array $relations): void
{
if (! isset(static::$loadRelations[$controllerClass])) {
static::$loadRelations[$controllerClass] = [];
}
static::$loadRelations[$controllerClass] = array_merge(static::$loadRelations[$controllerClass], $relations);
}
/**
* @internal
*/
public static function setLoadRelationCallables(string $controllerClass, array $relations): void
{
if (! isset(static::$loadRelationCallables[$controllerClass])) {
static::$loadRelationCallables[$controllerClass] = [];
}
static::$loadRelationCallables[$controllerClass] = array_merge(static::$loadRelationCallables[$controllerClass], $relations);
}
}

View File

@ -1,21 +0,0 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\Api\Controller;
use Tobscure\JsonApi\Resource;
use Tobscure\JsonApi\SerializerInterface;
abstract class AbstractShowController extends AbstractSerializeController
{
protected function createElement(mixed $data, SerializerInterface $serializer): \Tobscure\JsonApi\ElementInterface
{
return new Resource($data, $serializer);
}
}

View File

@ -1,35 +0,0 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\Api\Controller;
use Flarum\Api\Serializer\UserSerializer;
use Flarum\Http\RequestUtil;
use Flarum\User\Command\DeleteAvatar;
use Illuminate\Contracts\Bus\Dispatcher;
use Illuminate\Support\Arr;
use Psr\Http\Message\ServerRequestInterface;
use Tobscure\JsonApi\Document;
class DeleteAvatarController extends AbstractShowController
{
public ?string $serializer = UserSerializer::class;
public function __construct(
protected Dispatcher $bus
) {
}
protected function data(ServerRequestInterface $request, Document $document): mixed
{
return $this->bus->dispatch(
new DeleteAvatar(Arr::get($request->getQueryParams(), 'id'), RequestUtil::getActor($request))
);
}
}

View File

@ -1,37 +0,0 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\Api\Controller;
use Flarum\Api\Serializer\ExtensionReadmeSerializer;
use Flarum\Extension\Extension;
use Flarum\Extension\ExtensionManager;
use Flarum\Http\RequestUtil;
use Illuminate\Support\Arr;
use Psr\Http\Message\ServerRequestInterface;
use Tobscure\JsonApi\Document;
class ShowExtensionReadmeController extends AbstractShowController
{
public ?string $serializer = ExtensionReadmeSerializer::class;
public function __construct(
protected ExtensionManager $extensions
) {
}
protected function data(ServerRequestInterface $request, Document $document): ?Extension
{
$extensionName = Arr::get($request->getQueryParams(), 'name');
RequestUtil::getActor($request)->assertAdmin();
return $this->extensions->getExtension($extensionName);
}
}

View File

@ -9,7 +9,6 @@
namespace Flarum\Api\Controller;
use Flarum\Api\Endpoint\Show;
use Flarum\Api\JsonApi;
use Flarum\Api\Resource\ForumResource;
use Psr\Http\Message\ResponseInterface;
@ -26,7 +25,7 @@ class ShowForumController implements RequestHandlerInterface
{
return $this->api
->forResource(ForumResource::class)
->forEndpoint(Show::class)
->forEndpoint('show')
->handle($request);
}
}

View File

@ -9,36 +9,23 @@
namespace Flarum\Api\Controller;
use Flarum\Api\Serializer\MailSettingsSerializer;
use Flarum\Http\RequestUtil;
use Flarum\Settings\SettingsRepositoryInterface;
use Illuminate\Contracts\Validation\Factory;
use Flarum\Api\JsonApi;
use Flarum\Api\Resource\MailSettingResource;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Tobscure\JsonApi\Document;
use Psr\Http\Server\RequestHandlerInterface;
class ShowMailSettingsController extends AbstractShowController
class ShowMailSettingsController implements RequestHandlerInterface
{
public ?string $serializer = MailSettingsSerializer::class;
public function __construct(
protected JsonApi $api
) {}
protected function data(ServerRequestInterface $request, Document $document): array
public function handle(ServerRequestInterface $request): ResponseInterface
{
RequestUtil::getActor($request)->assertAdmin();
$drivers = array_map(function ($driver) {
return self::$container->make($driver);
}, self::$container->make('mail.supported_drivers'));
$settings = self::$container->make(SettingsRepositoryInterface::class);
$configured = self::$container->make('flarum.mail.configured_driver');
$actual = self::$container->make('mail.driver');
$validator = self::$container->make(Factory::class);
$errors = $configured->validate($settings, $validator);
return [
'drivers' => $drivers,
'sending' => $actual->canSend(),
'errors' => $errors,
];
return $this->api
->forResource(MailSettingResource::class)
->forEndpoint('show')
->handle($request);
}
}

View File

@ -1,40 +0,0 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\Api\Controller;
use Flarum\Api\Serializer\UserSerializer;
use Flarum\Http\RequestUtil;
use Flarum\User\Command\UploadAvatar;
use Flarum\User\User;
use Illuminate\Contracts\Bus\Dispatcher;
use Illuminate\Support\Arr;
use Psr\Http\Message\ServerRequestInterface;
use Tobscure\JsonApi\Document;
class UploadAvatarController extends AbstractShowController
{
public ?string $serializer = UserSerializer::class;
public function __construct(
protected Dispatcher $bus
) {
}
protected function data(ServerRequestInterface $request, Document $document): User
{
$id = Arr::get($request->getQueryParams(), 'id');
$actor = RequestUtil::getActor($request);
$file = Arr::get($request->getUploadedFiles(), 'avatar');
return $this->bus->dispatch(
new UploadAvatar($id, $file, $actor)
);
}
}

View File

@ -9,6 +9,7 @@
namespace Flarum\Api\Controller;
use Flarum\Api\JsonApi;
use Flarum\Http\RequestUtil;
use Flarum\Settings\SettingsRepositoryInterface;
use Illuminate\Contracts\Filesystem\Factory;
@ -16,9 +17,9 @@ use Illuminate\Contracts\Filesystem\Filesystem;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
use Intervention\Image\Interfaces\EncodedImageInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\UploadedFileInterface;
use Tobscure\JsonApi\Document;
abstract class UploadImageController extends ShowForumController
{
@ -28,13 +29,16 @@ abstract class UploadImageController extends ShowForumController
protected string $filenamePrefix = '';
public function __construct(
JsonApi $api,
protected SettingsRepositoryInterface $settings,
Factory $filesystemFactory
) {
parent::__construct($api);
$this->uploadDir = $filesystemFactory->disk('flarum-assets');
}
public function data(ServerRequestInterface $request, Document $document): array
public function handle(ServerRequestInterface $request): ResponseInterface
{
RequestUtil::getActor($request)->assertAdmin();
@ -52,7 +56,7 @@ abstract class UploadImageController extends ShowForumController
$this->settings->set($this->filePathSettingKey, $uploadName);
return parent::data($request, $document);
return parent::handle($request);
}
abstract protected function makeImage(UploadedFileInterface $file): EncodedImageInterface;

View File

@ -20,6 +20,8 @@ trait HasAuthorization
*/
protected null|string|Closure $ability = null;
protected bool $admin = false;
public function authenticated(bool|Closure $condition = true): self
{
$this->authenticated = $condition;
@ -34,6 +36,13 @@ trait HasAuthorization
return $this;
}
public function admin(bool $admin = true): self
{
$this->admin = $admin;
return $this;
}
public function getAuthenticated(Context $context): bool
{
if (is_bool($this->authenticated)) {
@ -68,6 +77,10 @@ trait HasAuthorization
$actor->assertRegistered();
}
if ($this->admin) {
$actor->assertAdmin();
}
if ($ability = $this->getAuthorized($context)) {
$actor->assertCan($ability, $context->model);
}

View File

@ -1,15 +0,0 @@
<?php
namespace Flarum\Api\Endpoint\Concerns;
trait HasCustomRoute
{
protected string $path;
public function path(string $path): self
{
$this->path = $path;
return $this;
}
}

View File

@ -2,12 +2,13 @@
namespace Flarum\Api\Endpoint\Concerns;
use Flarum\Api\Context;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Support\Str;
use Psr\Http\Message\ServerRequestInterface;
use Tobyz\JsonApiServer\Laravel\EloquentResource;
/**
* This is directed at eager loading relationships apart from the request includes.
@ -66,8 +67,14 @@ trait HasEagerLoading
/**
* Eager loads the required relationships.
*/
protected function loadRelations(Collection $models, ServerRequestInterface $request, array $included = []): void
protected function loadRelations(Collection $models, Context $context, array $included = []): void
{
if (! $context->collection instanceof EloquentResource) {
return;
}
$request = $context->request;
$included = $this->stringInclude($included);
$models = $models->filter(fn ($model) => $model instanceof Model);

View File

@ -2,81 +2,27 @@
namespace Flarum\Api\Endpoint;
use Flarum\Api\Context;
use Flarum\Api\Endpoint\Concerns\HasAuthorization;
use Flarum\Api\Endpoint\Concerns\HasCustomHooks;
use Flarum\Api\Endpoint\Concerns\HasCustomRoute;
use Flarum\Api\Endpoint\Concerns\HasEagerLoading;
use Illuminate\Database\Eloquent\Collection;
use Psr\Http\Message\ResponseInterface;
use RuntimeException;
use Tobyz\JsonApiServer\Context;
use Tobyz\JsonApiServer\Endpoint\Create as BaseCreate;
use Tobyz\JsonApiServer\Exception\ForbiddenException;
use Tobyz\JsonApiServer\Resource\Creatable;
use function Tobyz\JsonApiServer\json_api_response;
class Create extends BaseCreate implements Endpoint
class Create extends BaseCreate implements EndpointInterface
{
use HasAuthorization;
use HasEagerLoading;
use HasCustomRoute;
use HasCustomHooks;
public function handle(Context $context): ?ResponseInterface
public function setUp(): void
{
$model = $this->execute($context);
parent::setUp();
return json_api_response($document = $this->showResource($context, $model))
->withStatus(201)
->withHeader('Location', $document['data']['links']['self']);
}
$this->after(function (Context $context, object $model) {
$this->loadRelations(Collection::make([$model]), $context, $this->getInclude($context));
public function execute(Context $context): object
{
$collection = $context->collection;
if (!$collection instanceof Creatable) {
throw new RuntimeException(
sprintf('%s must implement %s', get_class($collection), Creatable::class),
);
}
if (!$this->isVisible($context)) {
throw new ForbiddenException();
}
$this->callBeforeHook($context);
$data = $this->parseData($context);
$context = $context
->withResource($resource = $context->resource($data['type']))
->withModel($model = $collection->newModel($context));
$this->assertFieldsValid($context, $data);
$this->fillDefaultValues($context, $data);
$this->deserializeValues($context, $data);
$this->assertDataValid($context, $data);
$this->setValues($context, $data);
$context = $context->withModel($model = $resource->create($model, $context));
$this->saveFields($context, $data);
$model = $this->callAfterHook($context, $model);
$this->loadRelations(Collection::make([$model]), $context->request, $this->getInclude($context));
return $model;
}
public function route(): EndpointRoute
{
return new EndpointRoute(
name: 'create',
path: $this->path ?? '/',
method: 'POST',
);
return $model;
});
}
}

View File

@ -3,70 +3,9 @@
namespace Flarum\Api\Endpoint;
use Flarum\Api\Endpoint\Concerns\HasAuthorization;
use Flarum\Api\Endpoint\Concerns\HasCustomRoute;
use Flarum\Api\Resource\Contracts\Deletable;
use Nyholm\Psr7\Response;
use Psr\Http\Message\ResponseInterface;
use RuntimeException;
use Tobyz\JsonApiServer\Context;
use Tobyz\JsonApiServer\Endpoint\Delete as BaseDelete;
use Tobyz\JsonApiServer\Exception\ForbiddenException;
use function Tobyz\JsonApiServer\json_api_response;
class Delete extends BaseDelete implements Endpoint
class Delete extends BaseDelete implements EndpointInterface
{
use HasAuthorization;
use HasCustomRoute;
/** {@inheritdoc} */
public function handle(Context $context): ?ResponseInterface
{
$segments = explode('/', $context->path());
if (count($segments) !== 2) {
return null;
}
$context = $context->withModelId($segments[1]);
$this->execute($context);
if ($meta = $this->serializeMeta($context)) {
return json_api_response(['meta' => $meta]);
}
return new Response(204);
}
public function execute(Context $context): bool
{
$model = $this->findResource($context, $context->getModelId());
$context = $context->withResource(
$resource = $context->resource($context->collection->resource($model, $context)),
);
if (!$resource instanceof Deletable) {
throw new RuntimeException(
sprintf('%s must implement %s', get_class($resource), Deletable::class),
);
}
if (!$this->isVisible($context = $context->withModel($model))) {
throw new ForbiddenException();
}
$resource->deleteAction($model, $context);
return true;
}
public function route(): EndpointRoute
{
return new EndpointRoute(
name: 'delete',
path: $this->path ?? '/{id}',
method: 'DELETE',
);
}
}

View File

@ -2,16 +2,9 @@
namespace Flarum\Api\Endpoint;
use Psr\Http\Message\ResponseInterface as Response;
use Tobyz\JsonApiServer\Context;
use Tobyz\JsonApiServer\Endpoint\Endpoint as BaseEndpoint;
interface Endpoint extends BaseEndpoint
class Endpoint extends BaseEndpoint implements EndpointInterface
{
/** @var \Flarum\Api\Context $context */
public function handle(Context $context): ?Response;
public function execute(Context $context): mixed;
public function route(): EndpointRoute;
//
}

View File

@ -0,0 +1,8 @@
<?php
namespace Flarum\Api\Endpoint;
interface EndpointInterface
{
//
}

View File

@ -1,13 +0,0 @@
<?php
namespace Flarum\Api\Endpoint;
class EndpointRoute
{
public function __construct(
public string $name,
public string $path,
public string $method,
) {
}
}

View File

@ -23,11 +23,10 @@ use Tobyz\JsonApiServer\Resource\Listable;
use Tobyz\JsonApiServer\Serializer;
use function Tobyz\JsonApiServer\json_api_response;
class Index extends BaseIndex implements Endpoint
class Index extends BaseIndex implements EndpointInterface
{
use HasAuthorization;
use HasEagerLoading;
use HasCustomRoute;
use ExtractsListingParams;
use HasCustomHooks;
@ -45,11 +44,6 @@ class Index extends BaseIndex implements Endpoint
return $this;
}
public function execute(Context $context): mixed
{
return null;
}
/** {@inheritDoc} */
public function handle(Context $context): ?Response
{
@ -121,7 +115,7 @@ class Index extends BaseIndex implements Endpoint
$include = $this->getInclude($context);
$this->loadRelations($models, $context->request, $include);
$this->loadRelations($models, $context, $include);
$serializer = new Serializer($context);
@ -142,13 +136,4 @@ class Index extends BaseIndex implements Endpoint
return json_api_response(compact('data', 'included', 'meta', 'links'));
}
public function route(): EndpointRoute
{
return new EndpointRoute(
name: 'index',
path: $this->path ?? '/',
method: 'GET',
);
}
}

View File

@ -2,68 +2,29 @@
namespace Flarum\Api\Endpoint;
use Flarum\Api\Context;
use Flarum\Api\Endpoint\Concerns\ExtractsListingParams;
use Flarum\Api\Endpoint\Concerns\HasAuthorization;
use Flarum\Api\Endpoint\Concerns\HasCustomHooks;
use Flarum\Api\Endpoint\Concerns\HasCustomRoute;
use Flarum\Api\Endpoint\Concerns\HasEagerLoading;
use Illuminate\Database\Eloquent\Collection;
use Psr\Http\Message\ResponseInterface;
use Tobyz\JsonApiServer\Context;
use Tobyz\JsonApiServer\Endpoint\Concerns\FindsResources;
use Tobyz\JsonApiServer\Endpoint\Concerns\ShowsResources;
use Tobyz\JsonApiServer\Endpoint\Show as BaseShow;
use Tobyz\JsonApiServer\Exception\ForbiddenException;
use function Tobyz\JsonApiServer\json_api_response;
class Show extends BaseShow implements Endpoint
class Show extends BaseShow implements EndpointInterface
{
use FindsResources;
use ShowsResources;
use HasAuthorization;
use HasEagerLoading;
use HasCustomRoute;
use ExtractsListingParams;
use HasCustomHooks;
public function handle(Context $context): ?ResponseInterface
public function setUp(): void
{
$segments = explode('/', $context->path());
parent::setUp();
$path = $this->route()->path;
$this->after(function (Context $context, object $model) {
$this->loadRelations(Collection::make([$model]), $context, $this->getInclude($context));
if ($path !== '/' && count($segments) !== 2) {
return null;
}
$context = $context->withModelId($path === '/' ? 1 : $segments[1]);
$this->callBeforeHook($context);
$model = $this->execute($context);
if (!$this->isVisible($context = $context->withModel($model))) {
throw new ForbiddenException();
}
$model = $this->callAfterHook($context, $model);
$this->loadRelations(Collection::make([$model]), $context->request, $this->getInclude($context));
return json_api_response($this->showResource($context, $model));
}
public function execute(Context $context): object
{
return $this->findResource($context, $context->getModelId());
}
public function route(): EndpointRoute
{
return new EndpointRoute(
name: 'show',
path: $this->path ?? '/{id}',
method: 'GET',
);
return $model;
});
}
}

View File

@ -2,85 +2,27 @@
namespace Flarum\Api\Endpoint;
use Flarum\Api\Context;
use Flarum\Api\Endpoint\Concerns\HasAuthorization;
use Flarum\Api\Endpoint\Concerns\HasCustomHooks;
use Flarum\Api\Endpoint\Concerns\HasCustomRoute;
use Flarum\Api\Endpoint\Concerns\HasEagerLoading;
use Illuminate\Database\Eloquent\Collection;
use Psr\Http\Message\ResponseInterface;
use RuntimeException;
use Tobyz\JsonApiServer\Context;
use Tobyz\JsonApiServer\Endpoint\Update as BaseUpdate;
use Tobyz\JsonApiServer\Exception\ForbiddenException;
use Tobyz\JsonApiServer\Resource\Updatable;
use function Tobyz\JsonApiServer\json_api_response;
class Update extends BaseUpdate implements Endpoint
class Update extends BaseUpdate implements EndpointInterface
{
use HasAuthorization;
use HasEagerLoading;
use HasCustomRoute;
use HasCustomHooks;
public function handle(Context $context): ?ResponseInterface
public function setUp(): void
{
$segments = explode('/', $context->path());
parent::setUp();
if (count($segments) !== 2) {
return null;
}
$this->after(function (Context $context, object $model) {
$this->loadRelations(Collection::make([$model]), $context, $this->getInclude($context));
$context = $context->withModelId($segments[1]);
$model = $this->execute($context);
return json_api_response($this->showResource($context, $model));
}
public function execute(Context $context): object
{
$model = $this->findResource($context, $context->getModelId());
$context = $context->withResource(
$resource = $context->resource($context->collection->resource($model, $context)),
);
if (!$resource instanceof Updatable) {
throw new RuntimeException(
sprintf('%s must implement %s', get_class($resource), Updatable::class),
);
}
if (!$this->isVisible($context = $context->withModel($model))) {
throw new ForbiddenException();
}
$this->callBeforeHook($context);
$data = $this->parseData($context);
$this->assertFieldsValid($context, $data);
$this->deserializeValues($context, $data);
$this->assertDataValid($context, $data);
$this->setValues($context, $data);
$context = $context->withModel($model = $resource->update($model, $context));
$this->saveFields($context, $data);
$model = $this->callAfterHook($context, $model);
$this->loadRelations(Collection::make([$model]), $context->request, $this->getInclude($context));
return $model;
}
public function route(): EndpointRoute
{
return new EndpointRoute(
name: 'update',
path: $this->path ?? '/{id}',
method: 'PATCH',
);
return $model;
});
}
}

View File

@ -2,8 +2,7 @@
namespace Flarum\Api;
use Flarum\Api\Endpoint\Endpoint;
use Flarum\Api\Endpoint\EndpointRoute;
use Flarum\Api\Endpoint\EndpointInterface;
use Flarum\Api\Resource\AbstractDatabaseResource;
use Flarum\Http\RequestUtil;
use Illuminate\Contracts\Container\Container;
@ -11,6 +10,7 @@ use Laminas\Diactoros\ServerRequestFactory;
use Laminas\Diactoros\Uri;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Tobyz\JsonApiServer\Endpoint\Endpoint;
use Tobyz\JsonApiServer\Exception\BadRequestException;
use Tobyz\JsonApiServer\JsonApi as BaseJsonApi;
use Tobyz\JsonApiServer\Resource\Collection;
@ -19,7 +19,7 @@ use Tobyz\JsonApiServer\Resource\Resource;
class JsonApi extends BaseJsonApi
{
protected string $resourceClass;
protected string $endpoint;
protected string $endpointName;
protected ?Request $baseRequest = null;
protected ?Container $container = null;
@ -30,16 +30,16 @@ class JsonApi extends BaseJsonApi
return $this;
}
public function forEndpoint(string $endpoint): self
public function forEndpoint(string $endpointName): self
{
$this->endpoint = $endpoint;
$this->endpointName = $endpointName;
return $this;
}
protected function makeContext(Request $request): Context
{
if (! $this->endpoint || ! $this->resourceClass || ! class_exists($this->resourceClass)) {
if (! $this->endpointName || ! $this->resourceClass || ! class_exists($this->resourceClass)) {
throw new BadRequestException('No resource or endpoint specified');
}
@ -50,11 +50,11 @@ class JsonApi extends BaseJsonApi
->withEndpoint($this->findEndpoint($collection));
}
protected function findEndpoint(?Collection $collection): Endpoint
protected function findEndpoint(?Collection $collection): Endpoint&EndpointInterface
{
/** @var \Flarum\Api\Endpoint\Endpoint $endpoint */
/** @var Endpoint&EndpointInterface $endpoint */
foreach ($collection->resolveEndpoints() as $endpoint) {
if ($endpoint::class === $this->endpoint) {
if ($endpoint->name === $this->endpointName) {
return $endpoint;
}
}
@ -76,11 +76,8 @@ class JsonApi extends BaseJsonApi
return $context->endpoint->handle($context);
}
public function execute(array $body, array $internal = [], array $options = []): mixed
public function process(array $body, array $internal = [], array $options = []): mixed
{
/** @var EndpointRoute $route */
$route = (new $this->endpoint)->route();
$request = $this->baseRequest ?? ServerRequestFactory::fromGlobals();
if (! empty($options['actor'])) {
@ -90,8 +87,6 @@ class JsonApi extends BaseJsonApi
$resource = $this->getCollection($this->resourceClass);
$request = $request
->withMethod($route->method)
->withUri(new Uri($route->path))
->withParsedBody([
...$body,
'data' => [
@ -110,7 +105,13 @@ class JsonApi extends BaseJsonApi
$context = $context->withInternal($key, $value);
}
return $context->endpoint->execute($context);
$context = $context->withRequest(
$request
->withMethod($context->endpoint->method)
->withUri(new Uri($context->endpoint->path))
);
return $context->endpoint->process($context);
}
public function validateQueryParameters(Request $request): void

View File

@ -10,11 +10,10 @@
namespace Flarum\Api;
use Laminas\Diactoros\Response\JsonResponse;
use Tobscure\JsonApi\Document;
class JsonApiResponse extends JsonResponse
{
public function __construct(Document $document, $status = 200, array $headers = [], $encodingOptions = 15)
public function __construct(array $document, $status = 200, array $headers = [], $encodingOptions = 15)
{
$headers['content-type'] = 'application/vnd.api+json';

View File

@ -59,18 +59,18 @@ abstract class AbstractDatabaseResource extends BaseResource implements
throw new RuntimeException('Not supported in Flarum, please use a model searcher instead https://docs.flarum.org/extend/search.');
}
public function create(object $model, Context $context): object
public function createAction(object $model, Context $context): object
{
$model = parent::create($model, $context);
$model = parent::createAction($model, $context);
$this->dispatchEventsFor($model, $context->getActor());
return $model;
}
public function update(object $model, Context $context): object
public function updateAction(object $model, Context $context): object
{
$model = parent::update($model, $context);
$model = parent::updateAction($model, $context);
$this->dispatchEventsFor($model, $context->getActor());

View File

@ -7,5 +7,5 @@ use Tobyz\JsonApiServer\Resource\Deletable as BaseDeletable;
interface Deletable extends BaseDeletable
{
public function deleteAction(object $model, Context $context): void;
//
}

View File

@ -5,7 +5,6 @@ namespace Flarum\Api\Resource;
use Carbon\Carbon;
use Flarum\Api\Context;
use Flarum\Api\Endpoint;
use Flarum\Api\Endpoint\Create;
use Flarum\Api\JsonApi;
use Flarum\Api\Schema;
use Flarum\Api\Sort\SortColumn;
@ -302,9 +301,9 @@ class DiscussionResource extends AbstractDatabaseResource
// Now that the discussion has been created, we can add the first post.
// We will do this by running the PostReply command.
$post = $api->forResource(PostResource::class)
->forEndpoint(Create::class)
->forEndpoint('create')
->withRequest($context->request)
->execute([
->process([
'data' => [
'attributes' => [
'content' => Arr::get($context->body(), 'data.attributes.content'),

View File

@ -0,0 +1,60 @@
<?php
namespace Flarum\Api\Resource;
use Flarum\Api\Endpoint;
use Flarum\Api\Resource\Contracts\Findable;
use Flarum\Api\Schema;
use Flarum\Extension\Extension;
use Flarum\Extension\ExtensionManager;
use Flarum\Mail\DriverInterface;
use Flarum\Settings\SettingsRepositoryInterface;
use Illuminate\Contracts\Container\Container;
use Illuminate\Contracts\Validation\Factory;
use stdClass;
use Tobyz\JsonApiServer\Context;
/**
* @todo: change to a simple ExtensionResource with readme field.
*/
class ExtensionReadmeResource extends AbstractResource implements Findable
{
public function __construct(
protected ExtensionManager $extensions
) {
}
public function type(): string
{
return 'extension-readmes';
}
/**
* @param Extension $model
*/
public function getId(object $model, Context $context): string
{
return $model->getId();
}
public function find(string $id, Context $context): ?object
{
return $this->extensions->getExtension($id);
}
public function endpoints(): array
{
return [
Endpoint\Show::make()
->admin(),
];
}
public function fields(): array
{
return [
Schema\Str::make('content')
->get(fn (Extension $extension) => $extension->getReadme()),
];
}
}

View File

@ -38,6 +38,11 @@ class ForumResource extends AbstractResource implements Findable
return '1';
}
public function id(\Tobyz\JsonApiServer\Context $context): ?string
{
return '1';
}
public function find(string $id, \Tobyz\JsonApiServer\Context $context): ?object
{
return new stdClass();
@ -48,7 +53,7 @@ class ForumResource extends AbstractResource implements Findable
return [
Endpoint\Show::make()
->defaultInclude(['groups', 'actor.groups'])
->path('/'),
->route('GET', '/'),
];
}

View File

@ -0,0 +1,78 @@
<?php
namespace Flarum\Api\Resource;
use Flarum\Api\Endpoint;
use Flarum\Api\Resource\Contracts\Findable;
use Flarum\Api\Schema;
use Flarum\Mail\DriverInterface;
use Flarum\Settings\SettingsRepositoryInterface;
use Illuminate\Contracts\Container\Container;
use Illuminate\Contracts\Validation\Factory;
use stdClass;
use Tobyz\JsonApiServer\Context;
class MailSettingResource extends AbstractResource implements Findable
{
public function __construct(
protected SettingsRepositoryInterface $settings,
protected Factory $validator,
protected Container $container
) {
}
public function type(): string
{
return 'mail-settings';
}
public function getId(object $model, Context $context): string
{
return '1';
}
public function id(Context $context): ?string
{
return '1';
}
public function find(string $id, Context $context): ?object
{
return new stdClass();
}
public function endpoints(): array
{
return [
Endpoint\Show::make()
->route('GET', '/')
->admin(),
];
}
public function fields(): array
{
return [
Schema\Arr::make('fields')
->get(function () {
return array_map(fn (DriverInterface $driver) => $driver->availableSettings(), array_map(function ($driver) {
return $this->container->make($driver);
}, $this->container->make('mail.supported_drivers')));
}),
Schema\Boolean::make('sending')
->get(function () {
/** @var DriverInterface $actual */
$actual = $this->container->make('mail.driver');
return $actual->canSend();
}),
Schema\Arr::make('errors')
->get(function () {
/** @var DriverInterface $configured */
$configured = $this->container->make('flarum.mail.configured_driver');
return $configured->validate($this->settings, $this->validator);
}),
];
}
}

View File

@ -6,11 +6,15 @@ use Flarum\Api\Context;
use Flarum\Api\Endpoint;
use Flarum\Api\Schema;
use Flarum\Api\Sort\SortColumn;
use Flarum\Bus\Dispatcher;
use Flarum\Foundation\ValidationException;
use Flarum\Http\RequestUtil;
use Flarum\Http\SlugManager;
use Flarum\Locale\TranslatorInterface;
use Flarum\Settings\SettingsRepositoryInterface;
use Flarum\User\AvatarUploader;
use Flarum\User\Command\DeleteAvatar;
use Flarum\User\Command\UploadAvatar;
use Flarum\User\Event\Deleting;
use Flarum\User\Event\GroupsChanged;
use Flarum\User\Event\RegisteringFromProvider;
@ -32,7 +36,8 @@ class UserResource extends AbstractDatabaseResource
protected SlugManager $slugManager,
protected SettingsRepositoryInterface $settings,
protected ImageManager $imageManager,
protected AvatarUploader $avatarUploader
protected AvatarUploader $avatarUploader,
protected Dispatcher $bus,
) {
}
@ -105,6 +110,22 @@ class UserResource extends AbstractDatabaseResource
->can('searchUsers')
->defaultInclude(['groups'])
->paginate(),
Endpoint\Endpoint::make('avatar.upload')
->route('POST', '/{id}/avatar')
->action(function (Context $context) {
$file = Arr::get($context->request->getUploadedFiles(), 'avatar');
return $this->bus->dispatch(
new UploadAvatar((int) $context->modelId, $file, $context->getActor())
);
}),
Endpoint\Endpoint::make('avatar.delete')
->route('DELETE', '/{id}/avatar')
->action(function (Context $context) {
return $this->bus->dispatch(
new DeleteAvatar(Arr::get($context->request->getQueryParams(), 'id'), $context->getActor())
);
}),
];
}

View File

@ -1,234 +0,0 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\Api\Serializer;
use Closure;
use DateTime;
use Flarum\Http\RequestUtil;
use Flarum\User\User;
use Illuminate\Contracts\Container\Container;
use Illuminate\Support\Arr;
use InvalidArgumentException;
use LogicException;
use Psr\Http\Message\ServerRequestInterface as Request;
use Tobscure\JsonApi\AbstractSerializer as BaseAbstractSerializer;
use Tobscure\JsonApi\Collection;
use Tobscure\JsonApi\Relationship;
use Tobscure\JsonApi\Resource;
use Tobscure\JsonApi\SerializerInterface;
abstract class AbstractSerializer extends BaseAbstractSerializer
{
protected Request $request;
protected User $actor;
protected static Container $container;
/**
* @var array<string, callable[]>
*/
protected static array $attributeMutators = [];
/**
* @var array<string, array<string, callable>>
*/
protected static array $customRelations = [];
public function getRequest(): Request
{
return $this->request;
}
public function setRequest(Request $request): void
{
$this->request = $request;
$this->actor = RequestUtil::getActor($request);
}
public function getActor(): User
{
return $this->actor;
}
public function getAttributes(mixed $model, array $fields = null): array
{
if (! is_object($model) && ! is_array($model)) {
return [];
}
$attributes = $this->getDefaultAttributes($model);
foreach (array_reverse(array_merge([static::class], class_parents($this))) as $class) {
if (isset(static::$attributeMutators[$class])) {
foreach (static::$attributeMutators[$class] as $callback) {
$attributes = array_merge(
$attributes,
$callback($this, $model, $attributes)
);
}
}
}
return $attributes;
}
/**
* Get the default set of serialized attributes for a model.
*/
abstract protected function getDefaultAttributes(object|array $model): array;
public function formatDate(DateTime $date = null): ?string
{
return $date?->format(DateTime::RFC3339);
}
public function getRelationship($model, $name)
{
if ($relationship = $this->getCustomRelationship($model, $name)) {
return $relationship;
}
return parent::getRelationship($model, $name);
}
/**
* Get a custom relationship.
*/
protected function getCustomRelationship(object|array $model, string $name): ?Relationship
{
foreach (array_merge([static::class], class_parents($this)) as $class) {
$callback = Arr::get(static::$customRelations, "$class.$name");
if (is_callable($callback)) {
$relationship = $callback($this, $model);
if (isset($relationship) && ! ($relationship instanceof Relationship)) {
throw new LogicException(
'GetApiRelationship handler must return an instance of '.Relationship::class
);
}
return $relationship;
}
}
return null;
}
/**
* Get a relationship builder for a has-one relationship.
*/
public function hasOne(object|array $model, SerializerInterface|Closure|string $serializer, string $relation = null): ?Relationship
{
return $this->buildRelationship($model, $serializer, $relation);
}
/**
* Get a relationship builder for a has-many relationship.
*/
public function hasMany(object|array $model, SerializerInterface|Closure|string $serializer, string $relation = null): ?Relationship
{
return $this->buildRelationship($model, $serializer, $relation, true);
}
protected function buildRelationship(object|array $model, SerializerInterface|Closure|string $serializer, string $relation = null, bool $many = false): ?Relationship
{
if (is_null($relation)) {
list(, , $caller) = debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT, 3);
$relation = $caller['function'];
}
$data = $this->getRelationshipData($model, $relation);
if ($data) {
$serializer = $this->resolveSerializer($serializer, $model, $data);
$type = $many ? Collection::class : Resource::class;
$element = new $type($data, $serializer);
return new Relationship($element);
}
return null;
}
protected function getRelationshipData(object|array $model, string $relation): mixed
{
if (is_object($model)) {
return $model->$relation;
}
return $model[$relation];
}
/**
* @throws InvalidArgumentException
*/
protected function resolveSerializer(SerializerInterface|Closure|string $serializer, object|array $model, mixed $data): SerializerInterface
{
if ($serializer instanceof Closure) {
$serializer = call_user_func($serializer, $model, $data);
}
if (is_string($serializer)) {
$serializer = $this->resolveSerializerClass($serializer);
}
if (! ($serializer instanceof SerializerInterface)) {
throw new InvalidArgumentException('Serializer must be an instance of '
.SerializerInterface::class);
}
return $serializer;
}
protected function resolveSerializerClass(string $class): object
{
$serializer = static::$container->make($class);
$serializer->setRequest($this->request);
return $serializer;
}
public static function getContainer(): Container
{
return static::$container;
}
/**
* @internal
*/
public static function setContainer(Container $container): void
{
static::$container = $container;
}
/**
* @internal
*/
public static function addAttributeMutator(string $serializerClass, callable $callback): void
{
if (! isset(static::$attributeMutators[$serializerClass])) {
static::$attributeMutators[$serializerClass] = [];
}
static::$attributeMutators[$serializerClass][] = $callback;
}
/**
* @internal
*/
public static function setRelationship(string $serializerClass, string $relation, callable $callback): void
{
static::$customRelations[$serializerClass][$relation] = $callback;
}
}

View File

@ -1,66 +0,0 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\Api\Serializer;
use Flarum\Http\AccessToken;
use Flarum\Locale\TranslatorInterface;
use InvalidArgumentException;
use Jenssegers\Agent\Agent;
class AccessTokenSerializer extends AbstractSerializer
{
protected $type = 'access-tokens';
public function __construct(
protected TranslatorInterface $translator
) {
}
protected function getDefaultAttributes(object|array $model): array
{
if (! ($model instanceof AccessToken)) {
throw new InvalidArgumentException(
$this::class.' can only serialize instances of '.AccessToken::class
);
}
$session = $this->request->getAttribute('session');
$agent = new Agent();
$agent->setUserAgent($model->last_user_agent);
$attributes = [
'token' => $model->token,
'userId' => $model->user_id,
'createdAt' => $this->formatDate($model->created_at),
'lastActivityAt' => $this->formatDate($model->last_activity_at),
'isCurrent' => $session && $session->get('access_token') === $model->token,
'isSessionToken' => in_array($model->type, ['session', 'session_remember'], true),
'title' => $model->title,
'lastIpAddress' => $model->last_ip_address,
'device' => $this->translator->trans('core.forum.security.browser_on_operating_system', [
'browser' => $agent->browser(),
'os' => $agent->platform(),
]),
];
// Unset hidden attributes (like the token value on session tokens)
foreach ($model->getHidden() as $name) {
unset($attributes[$name]);
}
// Hide the token value to non-actors no matter who they are.
if (isset($attributes['token']) && $this->getActor()->id !== $model->user_id) {
unset($attributes['token']);
}
return $attributes;
}
}

View File

@ -1,77 +0,0 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\Api\Serializer;
use Flarum\Discussion\Discussion;
use Flarum\Http\SlugManager;
use InvalidArgumentException;
use Tobscure\JsonApi\Relationship;
class BasicDiscussionSerializer extends AbstractSerializer
{
protected $type = 'discussions';
public function __construct(
protected SlugManager $slugManager
) {
}
/**
* @throws InvalidArgumentException
*/
protected function getDefaultAttributes(object|array $model): array
{
if (! ($model instanceof Discussion)) {
throw new InvalidArgumentException(
$this::class.' can only serialize instances of '.Discussion::class
);
}
return [
'title' => $model->title,
'slug' => $this->slugManager->forResource(Discussion::class)->toSlug($model),
];
}
protected function user(Discussion $discussion): ?Relationship
{
return $this->hasOne($discussion, BasicUserSerializer::class);
}
protected function firstPost(Discussion $discussion): ?Relationship
{
return $this->hasOne($discussion, BasicPostSerializer::class);
}
protected function lastPostedUser(Discussion $discussion): ?Relationship
{
return $this->hasOne($discussion, BasicUserSerializer::class);
}
protected function lastPost(Discussion $discussion): ?Relationship
{
return $this->hasOne($discussion, BasicPostSerializer::class);
}
protected function posts(Discussion $discussion): ?Relationship
{
return $this->hasMany($discussion, PostSerializer::class);
}
protected function mostRelevantPost(Discussion $discussion): ?Relationship
{
return $this->hasOne($discussion, PostSerializer::class);
}
protected function hiddenUser(Discussion $discussion): ?Relationship
{
return $this->hasOne($discussion, BasicUserSerializer::class);
}
}

View File

@ -1,72 +0,0 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\Api\Serializer;
use Exception;
use Flarum\Foundation\ErrorHandling\LogReporter;
use Flarum\Locale\TranslatorInterface;
use Flarum\Post\CommentPost;
use Flarum\Post\Post;
use InvalidArgumentException;
use Tobscure\JsonApi\Relationship;
class BasicPostSerializer extends AbstractSerializer
{
protected $type = 'posts';
public function __construct(
protected LogReporter $log,
protected TranslatorInterface $translator
) {
}
/**
* @throws InvalidArgumentException
*/
protected function getDefaultAttributes(object|array $model): array
{
if (! ($model instanceof Post)) {
throw new InvalidArgumentException(
$this::class.' can only serialize instances of '.Post::class
);
}
$attributes = [
'number' => (int) $model->number,
'createdAt' => $this->formatDate($model->created_at),
'contentType' => $model->type
];
if ($model instanceof CommentPost) {
try {
$attributes['contentHtml'] = $model->formatContent($this->request);
$attributes['renderFailed'] = false;
} catch (Exception $e) {
$attributes['contentHtml'] = $this->translator->trans('core.lib.error.render_failed_message');
$this->log->report($e);
$attributes['renderFailed'] = true;
}
} else {
$attributes['content'] = $model->content;
}
return $attributes;
}
protected function user(Post $post): ?Relationship
{
return $this->hasOne($post, BasicUserSerializer::class);
}
protected function discussion(Post $post): ?Relationship
{
return $this->hasOne($post, BasicDiscussionSerializer::class);
}
}

View File

@ -1,53 +0,0 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\Api\Serializer;
use Flarum\Http\SlugManager;
use Flarum\User\User;
use InvalidArgumentException;
use Tobscure\JsonApi\Relationship;
class BasicUserSerializer extends AbstractSerializer
{
protected $type = 'users';
public function __construct(
protected SlugManager $slugManager
) {
}
/**
* @throws InvalidArgumentException
*/
protected function getDefaultAttributes(object|array $model): array
{
if (! ($model instanceof User)) {
throw new InvalidArgumentException(
$this::class.' can only serialize instances of '.User::class
);
}
return [
'username' => $model->username,
'displayName' => $model->display_name,
'avatarUrl' => $model->avatar_url,
'slug' => $this->slugManager->forResource(User::class)->toSlug($model)
];
}
protected function groups(User $user): Relationship
{
if ($this->getActor()->can('viewHiddenGroups')) {
return $this->hasMany($user, GroupSerializer::class);
}
return $this->hasMany($user, GroupSerializer::class, 'visibleGroups');
}
}

View File

@ -1,39 +0,0 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\Api\Serializer;
use Flarum\User\User;
use InvalidArgumentException;
class CurrentUserSerializer extends UserSerializer
{
protected function getDefaultAttributes(object|array $model): array
{
if (! ($model instanceof User)) {
throw new InvalidArgumentException(
$this::class.' can only serialize instances of '.User::class
);
}
$attributes = parent::getDefaultAttributes($model);
$attributes += [
'isEmailConfirmed' => (bool) $model->is_email_confirmed,
'email' => $model->email,
'markedAllAsReadAt' => $this->formatDate($model->marked_all_as_read_at),
'unreadNotificationCount' => (int) $model->getUnreadNotificationCount(),
'newNotificationCount' => (int) $model->getNewNotificationCount(),
'preferences' => (array) $model->preferences,
'isAdmin' => $model->isAdmin(),
];
return $attributes;
}
}

View File

@ -1,49 +0,0 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\Api\Serializer;
use Flarum\Discussion\Discussion;
class DiscussionSerializer extends BasicDiscussionSerializer
{
/**
* @param Discussion $model
*/
protected function getDefaultAttributes(object|array $model): array
{
$attributes = parent::getDefaultAttributes($model) + [
'commentCount' => (int) $model->comment_count,
'participantCount' => (int) $model->participant_count,
'createdAt' => $this->formatDate($model->created_at),
'lastPostedAt' => $this->formatDate($model->last_posted_at),
'lastPostNumber' => (int) $model->last_post_number,
'canReply' => $this->actor->can('reply', $model),
'canRename' => $this->actor->can('rename', $model),
'canDelete' => $this->actor->can('delete', $model),
'canHide' => $this->actor->can('hide', $model)
];
if ($model->hidden_at) {
$attributes['isHidden'] = true;
$attributes['hiddenAt'] = $this->formatDate($model->hidden_at);
}
Discussion::setStateUser($this->actor);
if ($state = $model->state) {
$attributes += [
'lastReadAt' => $this->formatDate($state->last_read_at),
'lastReadPostNumber' => (int) $state->last_read_post_number
];
}
return $attributes;
}
}

View File

@ -1,35 +0,0 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\Api\Serializer;
use Flarum\Extension\Extension;
class ExtensionReadmeSerializer extends AbstractSerializer
{
/**
* @param Extension $model
*/
protected function getDefaultAttributes(object|array $model): array
{
return [
'content' => $model->getReadme()
];
}
public function getId($extension)
{
return $extension->getId();
}
public function getType($extension)
{
return 'extension-readmes';
}
}

View File

@ -1,133 +0,0 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\Api\Serializer;
use Flarum\Foundation\Application;
use Flarum\Foundation\Config;
use Flarum\Http\UrlGenerator;
use Flarum\Settings\SettingsRepositoryInterface;
use Illuminate\Contracts\Filesystem\Cloud;
use Illuminate\Contracts\Filesystem\Factory;
use Tobscure\JsonApi\Relationship;
class ForumSerializer extends AbstractSerializer
{
protected $type = 'forums';
/**
* @var Config
*/
protected $config;
/**
* @var SettingsRepositoryInterface
*/
protected $settings;
/**
* @var UrlGenerator
*/
protected $url;
/**
* @var Cloud
*/
protected $assetsFilesystem;
/**
* @param Config $config
* @param Factory $filesystemFactory
* @param SettingsRepositoryInterface $settings
* @param UrlGenerator $url
*/
public function __construct(Config $config, Factory $filesystemFactory, SettingsRepositoryInterface $settings, UrlGenerator $url)
{
$this->config = $config;
$this->assetsFilesystem = $filesystemFactory->disk('flarum-assets');
$this->settings = $settings;
$this->url = $url;
}
public function getId($model)
{
return '1';
}
/**
* @param array $model
*/
protected function getDefaultAttributes(object|array $model): array
{
$attributes = [
'title' => $this->settings->get('forum_title'),
'description' => $this->settings->get('forum_description'),
'showLanguageSelector' => (bool) $this->settings->get('show_language_selector', true),
'baseUrl' => $url = $this->url->to('forum')->base(),
'basePath' => $path = parse_url($url, PHP_URL_PATH) ?: '',
'baseOrigin' => substr($url, 0, strlen($url) - strlen($path)),
'debug' => $this->config->inDebugMode(),
'apiUrl' => $this->url->to('api')->base(),
'welcomeTitle' => $this->settings->get('welcome_title'),
'welcomeMessage' => $this->settings->get('welcome_message'),
'themePrimaryColor' => $this->settings->get('theme_primary_color'),
'themeSecondaryColor' => $this->settings->get('theme_secondary_color'),
'logoUrl' => $this->getLogoUrl(),
'faviconUrl' => $this->getFaviconUrl(),
'headerHtml' => $this->settings->get('custom_header'),
'footerHtml' => $this->settings->get('custom_footer'),
'allowSignUp' => (bool) $this->settings->get('allow_sign_up'),
'defaultRoute' => $this->settings->get('default_route'),
'canViewForum' => $this->actor->can('viewForum'),
'canStartDiscussion' => $this->actor->can('startDiscussion'),
'canSearchUsers' => $this->actor->can('searchUsers'),
'canCreateAccessToken' => $this->actor->can('createAccessToken'),
'canModerateAccessTokens' => $this->actor->can('moderateAccessTokens'),
'canEditUserCredentials' => $this->actor->hasPermission('user.editCredentials'),
'assetsBaseUrl' => rtrim($this->assetsFilesystem->url(''), '/'),
'jsChunksBaseUrl' => $this->assetsFilesystem->url('js'),
];
if ($this->actor->can('administrate')) {
$attributes['adminUrl'] = $this->url->to('admin')->base();
$attributes['version'] = Application::VERSION;
}
return $attributes;
}
protected function groups(array $model): ?Relationship
{
return $this->hasMany($model, GroupSerializer::class);
}
protected function getLogoUrl(): ?string
{
$logoPath = $this->settings->get('logo_path');
return $logoPath ? $this->getAssetUrl($logoPath) : null;
}
protected function getFaviconUrl(): ?string
{
$faviconPath = $this->settings->get('favicon_path');
return $faviconPath ? $this->getAssetUrl($faviconPath) : null;
}
public function getAssetUrl(string $assetPath): string
{
return $this->assetsFilesystem->url($assetPath);
}
protected function actor(array $model): ?Relationship
{
return $this->hasOne($model, CurrentUserSerializer::class);
}
}

View File

@ -1,40 +0,0 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\Api\Serializer;
use Flarum\Mail\DriverInterface;
use InvalidArgumentException;
class MailSettingsSerializer extends AbstractSerializer
{
protected $type = 'mail-settings';
/**
* @throws InvalidArgumentException
*/
protected function getDefaultAttributes(object|array $model): array
{
return [
'fields' => array_map([$this, 'serializeDriver'], $model['drivers']),
'sending' => $model['sending'],
'errors' => $model['errors'],
];
}
private function serializeDriver(DriverInterface $driver): array
{
return $driver->availableSettings();
}
public function getId($model)
{
return 'global';
}
}

View File

@ -1,66 +0,0 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\Api\Serializer;
use Flarum\Notification\Notification;
use InvalidArgumentException;
use Tobscure\JsonApi\Relationship;
class NotificationSerializer extends AbstractSerializer
{
protected $type = 'notifications';
/**
* A map of notification types (key) to the serializer that should be used
* to output the notification's subject (value).
*/
protected static array $subjectSerializers = [];
/**
* @throws InvalidArgumentException
*/
protected function getDefaultAttributes(object|array $model): array
{
if (! ($model instanceof Notification)) {
throw new InvalidArgumentException(
$this::class.' can only serialize instances of '.Notification::class
);
}
return [
'contentType' => $model->type,
'content' => $model->data,
'createdAt' => $this->formatDate($model->created_at),
'isRead' => (bool) $model->read_at
];
}
protected function user(Notification $notification): ?Relationship
{
return $this->hasOne($notification, BasicUserSerializer::class);
}
protected function fromUser(Notification $notification): ?Relationship
{
return $this->hasOne($notification, BasicUserSerializer::class);
}
protected function subject(Notification $notification): ?Relationship
{
return $this->hasOne($notification, function (Notification $notification) {
return static::$subjectSerializers[$notification->type];
});
}
public static function setSubjectSerializer(string $type, string $serializer): void
{
static::$subjectSerializers[$type] = $serializer;
}
}

View File

@ -1,77 +0,0 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\Api\Serializer;
use Flarum\Post\CommentPost;
use Flarum\Post\Post;
use Tobscure\JsonApi\Relationship;
class PostSerializer extends BasicPostSerializer
{
/**
* @param Post $model
*/
protected function getDefaultAttributes(object|array $model): array
{
$attributes = parent::getDefaultAttributes($model);
unset($attributes['content']);
$canEdit = $this->actor->can('edit', $model);
if ($model instanceof CommentPost) {
if ($canEdit) {
$attributes['content'] = $model->content;
}
if ($this->actor->can('viewIps', $model)) {
$attributes['ipAddress'] = $model->ip_address;
}
} else {
$attributes['content'] = $model->content;
}
if ($model->edited_at) {
$attributes['editedAt'] = $this->formatDate($model->edited_at);
}
if ($model->hidden_at) {
$attributes['isHidden'] = true;
$attributes['hiddenAt'] = $this->formatDate($model->hidden_at);
}
$attributes += [
'canEdit' => $canEdit,
'canDelete' => $this->actor->can('delete', $model),
'canHide' => $this->actor->can('hide', $model)
];
return $attributes;
}
protected function user(Post $post): ?Relationship
{
return $this->hasOne($post, UserSerializer::class);
}
protected function discussion(Post $post): ?Relationship
{
return $this->hasOne($post, BasicDiscussionSerializer::class);
}
protected function editedUser(Post $post): ?Relationship
{
return $this->hasOne($post, BasicUserSerializer::class);
}
protected function hiddenUser(Post $post): ?Relationship
{
return $this->hasOne($post, BasicUserSerializer::class);
}
}

View File

@ -1,48 +0,0 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\Api\Serializer;
use Flarum\User\User;
class UserSerializer extends BasicUserSerializer
{
/**
* @param User $model
*/
protected function getDefaultAttributes(object|array $model): array
{
$attributes = parent::getDefaultAttributes($model);
$attributes += [
'joinTime' => $this->formatDate($model->joined_at),
'discussionCount' => (int) $model->discussion_count,
'commentCount' => (int) $model->comment_count,
'canEdit' => $this->actor->can('edit', $model),
'canEditCredentials' => $this->actor->can('editCredentials', $model),
'canEditGroups' => $this->actor->can('editGroups', $model),
'canDelete' => $this->actor->can('delete', $model),
];
if ($model->getPreference('discloseOnline') || $this->actor->can('viewLastSeenAt', $model)) {
$attributes += [
'lastSeenAt' => $this->formatDate($model->last_seen_at)
];
}
if ($attributes['canEditCredentials'] || $this->actor->id === $model->id) {
$attributes += [
'isEmailConfirmed' => (bool) $model->is_email_confirmed,
'email' => $model->email
];
}
return $attributes;
}
}

View File

@ -46,20 +46,6 @@ return function (RouteCollection $map, RouteHandlerFactory $route) {
|--------------------------------------------------------------------------
*/
// Upload avatar
$map->post(
'/users/{id}/avatar',
'users.avatar.upload',
$route->toController(Controller\UploadAvatarController::class)
);
// Remove avatar
$map->delete(
'/users/{id}/avatar',
'users.avatar.delete',
$route->toController(Controller\DeleteAvatarController::class)
);
// send confirmation email
$map->post(
'/users/{id}/send-confirmation',
@ -107,13 +93,6 @@ return function (RouteCollection $map, RouteHandlerFactory $route) {
$route->toController(Controller\UninstallExtensionController::class)
);
// Get readme for an extension
$map->get(
'/extension-readmes/{name}',
'extension-readmes.show',
$route->toController(Controller\ShowExtensionReadmeController::class)
);
// Update settings
$map->post(
'/settings',

View File

@ -3,7 +3,7 @@
namespace Flarum\Extend;
use Flarum\Api\Controller\AbstractSerializeController;
use Flarum\Api\Endpoint\Endpoint;
use Flarum\Api\Endpoint\EndpointInterface;
use Flarum\Api\Resource\Contracts\Collection;
use Flarum\Api\Resource\Contracts\Resource;
use Flarum\Extension\Extension;
@ -64,7 +64,7 @@ class ApiResource implements ExtenderInterface
/**
* Modify an endpoint.
*
* @param class-string<\Flarum\Api\Endpoint\Endpoint>|array<\Flarum\Api\Endpoint\Endpoint> $endpointClass the class name of the endpoint.
* @param class-string<\Flarum\Api\Endpoint\EndpointInterface>|array<\Flarum\Api\Endpoint\EndpointInterface> $endpointClass the class name of the endpoint.
* or an array of class names of the endpoints.
* @param callable|class-string $mutator a callable that accepts an endpoint and returns the modified endpoint.
*/
@ -182,7 +182,7 @@ class ApiResource implements ExtenderInterface
[$endpointsToRemove, $condition] = $removeEndpointClass;
if ($this->isApplicable($condition, $resource, $container)) {
$endpoints = array_filter($endpoints, fn (Endpoint $endpoint) => ! in_array($endpoint::class, $endpointsToRemove));
$endpoints = array_filter($endpoints, fn (EndpointInterface $endpoint) => ! in_array($endpoint::class, $endpointsToRemove));
}
}
@ -194,8 +194,8 @@ class ApiResource implements ExtenderInterface
$mutateEndpoint = ContainerUtil::wrapCallback($mutator, $container);
$endpoint = $mutateEndpoint($endpoint, $resource);
if (! $endpoint instanceof Endpoint) {
throw new \RuntimeException('The endpoint mutator must return an instance of ' . Endpoint::class);
if (! $endpoint instanceof EndpointInterface) {
throw new \RuntimeException('The endpoint mutator must return an instance of ' . EndpointInterface::class);
}
}
}

View File

@ -12,7 +12,6 @@ namespace Flarum\Foundation\ErrorHandling;
use Flarum\Api\JsonApiResponse;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Tobscure\JsonApi\Document;
/**
* A formatter to render exceptions as valid {JSON:API} error object.
@ -28,15 +27,13 @@ class JsonApiFormatter implements HttpFormatter
public function format(HandledError $error, Request $request): Response
{
$document = new Document;
if ($error->hasDetails()) {
$document->setErrors($this->withDetails($error));
$errors = $this->withDetails($error);
} else {
$document->setErrors($this->default($error));
$errors = $this->default($error);
}
return new JsonApiResponse($document, $error->getStatusCode());
return new JsonApiResponse(compact('errors'), $error->getStatusCode());
}
private function default(HandledError $error): array

View File

@ -15,7 +15,6 @@ use Laminas\Diactoros\Response\JsonResponse;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Tobscure\JsonApi\Document;
class MaintenanceModeHandler implements RequestHandlerInterface
{
@ -46,10 +45,12 @@ class MaintenanceModeHandler implements RequestHandlerInterface
private function apiResponse(): ResponseInterface
{
return new JsonResponse(
(new Document)->setErrors([
'status' => '503',
'title' => self::MESSAGE
]),
[
'errors' => [
'status' => '503',
'title' => self::MESSAGE
],
],
503,
['Content-Type' => 'application/vnd.api+json']
);

View File

@ -41,11 +41,10 @@ class RouteHandlerFactory
/**
* @param class-string<\Tobyz\JsonApiServer\Resource\AbstractResource> $resourceClass
* @param class-string<\Flarum\Api\Endpoint\Endpoint> $endpointClass
*/
public function toApiResource(string $resourceClass, string $endpointClass): Closure
public function toApiResource(string $resourceClass, string $endpointName): Closure
{
return function (Request $request, array $routeParams) use ($resourceClass, $endpointClass) {
return function (Request $request, array $routeParams) use ($resourceClass, $endpointName) {
/** @var JsonApi $api */
$api = $this->container->make(JsonApi::class);
@ -54,7 +53,7 @@ class RouteHandlerFactory
$request = $request->withQueryParams(array_merge($request->getQueryParams(), $routeParams));
return $api->forResource($resourceClass)
->forEndpoint($endpointClass)
->forEndpoint($endpointName)
->handle($request);
};
}

View File

@ -1,53 +0,0 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\Tests\integration\api;
use Flarum\Api\Controller\AbstractSerializeController;
use Flarum\Extend;
use Flarum\Testing\integration\TestCase;
use Psr\Http\Message\ServerRequestInterface;
use Tobscure\JsonApi\Document;
use Tobscure\JsonApi\ElementInterface;
use Tobscure\JsonApi\SerializerInterface;
class AbstractSerializeControllerTest extends TestCase
{
public function test_missing_serializer_class_throws_exception()
{
$this->extend(
(new Extend\Routes('api'))
->get('/dummy-serialize', 'dummy-serialize', DummySerializeController::class)
);
$response = $this->send(
$this->request('GET', '/api/dummy-serialize')
);
$json = json_decode($contents = (string) $response->getBody(), true);
$this->assertEquals(500, $response->getStatusCode(), $contents);
$this->assertStringStartsWith('InvalidArgumentException: Serializer required for controller: '.DummySerializeController::class, $json['errors'][0]['detail']);
}
}
class DummySerializeController extends AbstractSerializeController
{
public ?string $serializer = null;
protected function data(ServerRequestInterface $request, Document $document): mixed
{
return [];
}
protected function createElement(mixed $data, SerializerInterface $serializer): ElementInterface
{
return $data;
}
}

View File

@ -32,8 +32,8 @@ class EventTest extends TestCase
$api = $this->app()->getContainer()->make(JsonApi::class);
return $api->forResource(GroupResource::class)
->forEndpoint(Create::class)
->execute(
->forEndpoint('create')
->process(
body: [
'data' => [
'attributes' => [

View File

@ -34,7 +34,11 @@ class MailTest extends TestCase
])
);
$fields = json_decode($response->getBody()->getContents(), true)['data']['attributes']['fields'];
$body = $response->getBody()->getContents();
$this->assertEquals(200, $response->getStatusCode(), $body);
$fields = json_decode($body, true)['data']['attributes']['fields'];
// The custom driver does not exist
$this->assertArrayNotHasKey('custom', $fields);

View File

@ -101,8 +101,8 @@ class DiscussionPolicyTest extends TestCase
$api
->forResource(PostResource::class)
->forEndpoint(Create::class)
->execute(
->forEndpoint('create')
->process(
body: [
'data' => [
'attributes' => [