1
0
mirror of https://github.com/flarum/core.git synced 2025-08-05 16:07:34 +02:00

feat: allow dependency injection in resources

This commit is contained in:
Sami Mazouz
2024-02-17 14:53:40 +01:00
parent 0619662c48
commit c36f034672
17 changed files with 167 additions and 171 deletions

View File

@@ -23,6 +23,7 @@ use Flarum\Http\RouteHandlerFactory;
use Flarum\Http\UrlGenerator; use Flarum\Http\UrlGenerator;
use Illuminate\Contracts\Container\Container; use Illuminate\Contracts\Container\Container;
use Laminas\Stratigility\MiddlewarePipe; use Laminas\Stratigility\MiddlewarePipe;
use ReflectionClass;
class ApiServiceProvider extends AbstractServiceProvider class ApiServiceProvider extends AbstractServiceProvider
{ {
@@ -48,11 +49,12 @@ class ApiServiceProvider extends AbstractServiceProvider
$resources = $this->container->make('flarum.api.resources'); $resources = $this->container->make('flarum.api.resources');
$api = new JsonApi('/'); $api = new JsonApi('/');
$api->container($container);
foreach ($resources as $resourceClass) { foreach ($resources as $resourceClass) {
/** @var \Flarum\Api\Resource\AbstractResource|\Flarum\Api\Resource\AbstractDatabaseResource $resource */ /** @var \Flarum\Api\Resource\AbstractResource|\Flarum\Api\Resource\AbstractDatabaseResource $resource */
$resource = new $resourceClass; $resource = $container->make($resourceClass);
$resource->boot($container); $resource->boot($api);
$api->resource($resource); $api->resource($resource);
} }
@@ -61,9 +63,9 @@ class ApiServiceProvider extends AbstractServiceProvider
$this->container->alias('flarum.api.resource_handler', JsonApi::class); $this->container->alias('flarum.api.resource_handler', JsonApi::class);
$this->container->singleton('flarum.api.routes', function () { $this->container->singleton('flarum.api.routes', function (Container $container) {
$routes = new RouteCollection; $routes = new RouteCollection;
$this->populateRoutes($routes); $this->populateRoutes($routes, $container);
return $routes; return $routes;
}); });
@@ -158,10 +160,10 @@ class ApiServiceProvider extends AbstractServiceProvider
AbstractSerializer::setContainer($container); AbstractSerializer::setContainer($container);
} }
protected function populateRoutes(RouteCollection $routes): void protected function populateRoutes(RouteCollection $routes, Container $container): void
{ {
/** @var RouteHandlerFactory $factory */ /** @var RouteHandlerFactory $factory */
$factory = $this->container->make(RouteHandlerFactory::class); $factory = $container->make(RouteHandlerFactory::class);
$callback = include __DIR__.'/routes.php'; $callback = include __DIR__.'/routes.php';
$callback($routes, $factory); $callback($routes, $factory);
@@ -169,16 +171,30 @@ class ApiServiceProvider extends AbstractServiceProvider
$resources = $this->container->make('flarum.api.resources'); $resources = $this->container->make('flarum.api.resources');
foreach ($resources as $resourceClass) { foreach ($resources as $resourceClass) {
/** @var \Flarum\Api\Resource\AbstractResource|\Flarum\Api\Resource\AbstractDatabaseResource $resource */ /**
$resource = new $resourceClass; * This is an empty shell instance,
/** @var \Flarum\Api\Endpoint\Endpoint[] $endpoints */ * we only need it to get the endpoint routes and types.
$endpoints = $resource->endpoints(); *
* We avoid dependency injection here to avoid early resolution.
*
* @var \Flarum\Api\Resource\AbstractResource|\Flarum\Api\Resource\AbstractDatabaseResource $resource
*/
$resource = (new ReflectionClass($resourceClass))->newInstanceWithoutConstructor();
$type = $resource->type(); $type = $resource->type();
/**
* None of the injected dependencies should be directly used within
* the `endpoints` method. Encourage using callbacks.
*
* @var \Flarum\Api\Endpoint\Endpoint[] $endpoints
*/
$endpoints = $resource->endpoints();
foreach ($endpoints as $endpoint) { foreach ($endpoints as $endpoint) {
$route = $endpoint->route(); $route = $endpoint->route();
$routes->addRoute($route->method, rtrim("/$type$route->path", '/'), "$type.$route->name", $factory->toApiResource($resourceClass, $endpoint::class)); $routes->addRoute($route->method, rtrim("/$type$route->path", '/'), "$type.$route->name", $factory->toApiResource($resource::class, $endpoint::class));
} }
} }
} }

View File

@@ -5,6 +5,7 @@ namespace Flarum\Api;
use Flarum\Http\RequestUtil; use Flarum\Http\RequestUtil;
use Flarum\Search\SearchResults; use Flarum\Search\SearchResults;
use Flarum\User\User; use Flarum\User\User;
use Illuminate\Contracts\Container\Container;
use Tobyz\JsonApiServer\Context as BaseContext; use Tobyz\JsonApiServer\Context as BaseContext;
class Context extends BaseContext class Context extends BaseContext

View File

@@ -1,74 +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\DiscussionSerializer;
use Flarum\Discussion\Command\EditDiscussion;
use Flarum\Discussion\Command\ReadDiscussion;
use Flarum\Discussion\Discussion;
use Flarum\Http\RequestUtil;
use Flarum\Post\Post;
use Illuminate\Contracts\Bus\Dispatcher;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Arr;
use Psr\Http\Message\ServerRequestInterface;
use Tobscure\JsonApi\Document;
class UpdateDiscussionController extends AbstractShowController
{
public ?string $serializer = DiscussionSerializer::class;
public function __construct(
protected Dispatcher $bus
) {
}
protected function data(ServerRequestInterface $request, Document $document): Discussion
{
$actor = RequestUtil::getActor($request);
$discussionId = (int) Arr::get($request->getQueryParams(), 'id');
$data = Arr::get($request->getParsedBody(), 'data', []);
/** @var Discussion $discussion */
$discussion = $this->bus->dispatch(
new EditDiscussion($discussionId, $actor, $data)
);
// TODO: Refactor the ReadDiscussion (state) command into EditDiscussion?
// That's what extensions will do anyway.
if ($readNumber = Arr::get($data, 'attributes.lastReadPostNumber')) {
$state = $this->bus->dispatch(
new ReadDiscussion($discussionId, $actor, $readNumber)
);
$discussion = $state->discussion;
}
if ($posts = $discussion->getModifiedPosts()) {
/** @var Collection<int, Post> $posts */
$posts = (new Collection($posts))->load('discussion', 'user');
$discussionPosts = $discussion->posts()->whereVisibleTo($actor)->oldest()->pluck('id')->all();
foreach ($discussionPosts as &$id) {
foreach ($posts as $post) {
if ($id == $post->id) {
$id = $post;
}
}
}
$discussion->setRelation('posts', $discussionPosts);
$this->include = array_merge($this->include, ['posts', 'posts.discussion', 'posts.user']);
}
return $discussion;
}
}

View File

@@ -71,7 +71,7 @@ class Index extends BaseIndex implements Endpoint
// This model has a searcher API, so we'll use that instead of the default. // This model has a searcher API, so we'll use that instead of the default.
// The searcher API allows swapping the default search engine for a custom one. // The searcher API allows swapping the default search engine for a custom one.
$search = resolve(SearchManager::class); $search = $context->api->getContainer()->make(SearchManager::class);
$modelClass = $query->getModel()::class; $modelClass = $query->getModel()::class;
if ($query instanceof Builder && $search->searchable($modelClass)) { if ($query instanceof Builder && $search->searchable($modelClass)) {
@@ -86,8 +86,7 @@ class Index extends BaseIndex implements Endpoint
$sortIsDefault = ! $context->queryParam('sort'); $sortIsDefault = ! $context->queryParam('sort');
// @todo: resources and endpoints have no room for dependency injection $results = $search->query(
$results = resolve(SearchManager::class)->query(
$modelClass, $modelClass,
new SearchCriteria($actor, $filters, $limit, $offset, $sort, $sortIsDefault), new SearchCriteria($actor, $filters, $limit, $offset, $sort, $sortIsDefault),
); );

View File

@@ -6,20 +6,22 @@ use Flarum\Api\Endpoint\Endpoint;
use Flarum\Api\Endpoint\EndpointRoute; use Flarum\Api\Endpoint\EndpointRoute;
use Flarum\Api\Resource\AbstractDatabaseResource; use Flarum\Api\Resource\AbstractDatabaseResource;
use Flarum\Http\RequestUtil; use Flarum\Http\RequestUtil;
use Illuminate\Contracts\Container\Container;
use Laminas\Diactoros\ServerRequestFactory; use Laminas\Diactoros\ServerRequestFactory;
use Laminas\Diactoros\Uri; use Laminas\Diactoros\Uri;
use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\ServerRequestInterface as Request; use Psr\Http\Message\ServerRequestInterface as Request;
use Tobyz\JsonApiServer\Exception\BadRequestException; use Tobyz\JsonApiServer\Exception\BadRequestException;
use Tobyz\JsonApiServer\JsonApi as BaseJsonApi; use Tobyz\JsonApiServer\JsonApi as BaseJsonApi;
use Tobyz\JsonApiServer\Resource\Collection; use Tobyz\JsonApiServer\Resource\Collection;
use Tobyz\JsonApiServer\Resource\Resource;
class JsonApi extends BaseJsonApi class JsonApi extends BaseJsonApi
{ {
protected string $resourceClass; protected string $resourceClass;
protected string $endpoint; protected string $endpoint;
protected ?Request $baseRequest = null; protected ?Request $baseRequest = null;
protected ?Container $container = null;
public function forResource(string $resourceClass): self public function forResource(string $resourceClass): self
{ {
@@ -41,7 +43,7 @@ class JsonApi extends BaseJsonApi
throw new BadRequestException('No resource or endpoint specified'); throw new BadRequestException('No resource or endpoint specified');
} }
$collection = $this->getCollection((new $this->resourceClass)->type()); $collection = $this->getCollection($this->resourceClass);
return (new Context($this, $request)) return (new Context($this, $request))
->withCollection($collection) ->withCollection($collection)
@@ -85,6 +87,8 @@ class JsonApi extends BaseJsonApi
$request = RequestUtil::withActor($request, $options['actor']); $request = RequestUtil::withActor($request, $options['actor']);
} }
$resource = $this->getCollection($this->resourceClass);
$request = $request $request = $request
->withMethod($route->method) ->withMethod($route->method)
->withUri(new Uri($route->path)) ->withUri(new Uri($route->path))
@@ -93,7 +97,9 @@ class JsonApi extends BaseJsonApi
'data' => [ 'data' => [
...($request->getParsedBody()['data'] ?? []), ...($request->getParsedBody()['data'] ?? []),
...($body['data'] ?? []), ...($body['data'] ?? []),
'type' => (new $this->resourceClass)->type(), 'type' => $resource instanceof Resource
? $resource->type()
: $resource->name(),
], ],
]); ]);
@@ -136,4 +142,16 @@ class JsonApi extends BaseJsonApi
{ {
return array_values(array_unique(array_map(fn ($modelClass) => $this->typeForModel($modelClass), $modelClasses))); return array_values(array_unique(array_map(fn ($modelClass) => $this->typeForModel($modelClass), $modelClasses)));
} }
public function container(Container $container): static
{
$this->container = $container;
return $this;
}
public function getContainer(): ?Container
{
return $this->container;
}
} }

View File

@@ -34,7 +34,6 @@ abstract class AbstractDatabaseResource extends BaseResource implements
use DispatchEventsTrait { use DispatchEventsTrait {
dispatchEventsFor as traitDispatchEventsFor; dispatchEventsFor as traitDispatchEventsFor;
} }
use ResolvesValidationFactory;
abstract public function model(): string; abstract public function model(): string;
@@ -147,7 +146,7 @@ abstract class AbstractDatabaseResource extends BaseResource implements
$savingEvent = $this->newSavingEvent($context, Arr::get($context->body(), 'data', [])); $savingEvent = $this->newSavingEvent($context, Arr::get($context->body(), 'data', []));
if ($savingEvent) { if ($savingEvent) {
$this->container->make(Dispatcher::class)->dispatch($savingEvent); $this->events->dispatch($savingEvent);
$dirtyAfterEvent = $context->model->getDirty(); $dirtyAfterEvent = $context->model->getDirty();

View File

@@ -9,5 +9,4 @@ use Tobyz\JsonApiServer\Resource\AbstractResource as BaseResource;
abstract class AbstractResource extends BaseResource abstract class AbstractResource extends BaseResource
{ {
use Bootable; use Bootable;
use ResolvesValidationFactory;
} }

View File

@@ -19,6 +19,11 @@ use Jenssegers\Agent\Agent;
class AccessTokenResource extends AbstractDatabaseResource class AccessTokenResource extends AbstractDatabaseResource
{ {
public function __construct(
protected TranslatorInterface $translator
) {
}
public function type(): string public function type(): string
{ {
return 'access-tokens'; return 'access-tokens';
@@ -84,12 +89,10 @@ class AccessTokenResource extends AbstractDatabaseResource
Schema\Str::make('lastIpAddress'), Schema\Str::make('lastIpAddress'),
Schema\Str::make('device') Schema\Str::make('device')
->get(function (AccessToken $token) { ->get(function (AccessToken $token) {
$translator = resolve(TranslatorInterface::class);
$agent = new Agent(); $agent = new Agent();
$agent->setUserAgent($token->last_user_agent); $agent->setUserAgent($token->last_user_agent);
return $translator->trans('core.forum.security.browser_on_operating_system', [ return $this->translator->trans('core.forum.security.browser_on_operating_system', [
'browser' => $agent->browser(), 'browser' => $agent->browser(),
'os' => $agent->platform(), 'os' => $agent->platform(),
]); ]);

View File

@@ -2,17 +2,32 @@
namespace Flarum\Api\Resource\Concerns; namespace Flarum\Api\Resource\Concerns;
use Flarum\Api\JsonApi;
use Illuminate\Contracts\Container\Container; use Illuminate\Contracts\Container\Container;
use Illuminate\Contracts\Events\Dispatcher; use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Contracts\Validation\Factory;
trait Bootable trait Bootable
{ {
protected readonly Container $container; protected readonly JsonApi $api;
protected readonly Dispatcher $events; protected readonly Dispatcher $events;
protected readonly Factory $validation;
public function boot(Container $container): void /**
* Avoids polluting the constructor of the resource with dependencies.
*/
public function boot(JsonApi $api): void
{ {
$this->container = $container; $this->api = $api;
$this->events = $container->make(Dispatcher::class); $this->events = $api->getContainer()->make(Dispatcher::class);
$this->validation = $api->getContainer()->make(Factory::class);
}
/**
* Called by the JSON:API server package to resolve the validation factory.
*/
public function validationFactory(): Factory
{
return $this->validation;
} }
} }

View File

@@ -1,16 +0,0 @@
<?php
namespace Flarum\Api\Resource\Concerns;
use Illuminate\Contracts\Validation\Factory;
trait ResolvesValidationFactory
{
/**
* Called by the JSON:API server package to resolve the validation factory.
*/
public function validationFactory(): Factory
{
return resolve(Factory::class);
}
}

View File

@@ -24,6 +24,13 @@ use Tobyz\JsonApiServer\Laravel\Sort\SortColumn;
class DiscussionResource extends AbstractDatabaseResource class DiscussionResource extends AbstractDatabaseResource
{ {
public function __construct(
protected Dispatcher $bus,
protected SlugManager $slugManager,
protected PostRepository $posts
) {
}
public function type(): string public function type(): string
{ {
return 'discussions'; return 'discussions';
@@ -41,11 +48,10 @@ class DiscussionResource extends AbstractDatabaseResource
public function find(string $id, \Tobyz\JsonApiServer\Context $context): ?object public function find(string $id, \Tobyz\JsonApiServer\Context $context): ?object
{ {
$slugManager = resolve(SlugManager::class);
$actor = $context->getActor(); $actor = $context->getActor();
if (Arr::get($context->request->getQueryParams(), 'bySlug', false)) { if (Arr::get($context->request->getQueryParams(), 'bySlug', false)) {
$discussion = $slugManager->forResource(Discussion::class)->fromSlug($id, $actor); $discussion = $this->slugManager->forResource(Discussion::class)->fromSlug($id, $actor);
} else { } else {
$discussion = $this->query($context)->findOrFail($id); $discussion = $this->query($context)->findOrFail($id);
} }
@@ -113,7 +119,7 @@ class DiscussionResource extends AbstractDatabaseResource
->set(fn () => null), ->set(fn () => null),
Schema\Str::make('slug') Schema\Str::make('slug')
->get(function (Discussion $discussion) { ->get(function (Discussion $discussion) {
return resolve(SlugManager::class)->forResource(Discussion::class)->toSlug($discussion); return $this->slugManager->forResource(Discussion::class)->toSlug($discussion);
}), }),
Schema\Integer::make('commentCount'), Schema\Integer::make('commentCount'),
Schema\Integer::make('participantCount'), Schema\Integer::make('participantCount'),
@@ -167,7 +173,7 @@ class DiscussionResource extends AbstractDatabaseResource
->set(function (Discussion $discussion, int $value, Context $context) { ->set(function (Discussion $discussion, int $value, Context $context) {
if ($readNumber = Arr::get($context->body(), 'data.attributes.lastReadPostNumber')) { if ($readNumber = Arr::get($context->body(), 'data.attributes.lastReadPostNumber')) {
$discussion->afterSave(function (Discussion $discussion) use ($readNumber, $context) { $discussion->afterSave(function (Discussion $discussion) use ($readNumber, $context) {
resolve(Dispatcher::class)->dispatch( $this->bus->dispatch(
new ReadDiscussion($discussion->id, $context->getActor(), $readNumber) new ReadDiscussion($discussion->id, $context->getActor(), $readNumber)
); );
}); });
@@ -196,7 +202,7 @@ class DiscussionResource extends AbstractDatabaseResource
$limit = $context->endpoint->extractLimitValue($context, $context->endpoint->defaultExtracts($context)); $limit = $context->endpoint->extractLimitValue($context, $context->endpoint->defaultExtracts($context));
if (($near = Arr::get($context->request->getQueryParams(), 'page.near')) > 1) { if (($near = Arr::get($context->request->getQueryParams(), 'page.near')) > 1) {
$offset = resolve(PostRepository::class)->getIndexForNumber($discussion->id, $near, $actor); $offset = $this->posts->getIndexForNumber($discussion->id, $near, $actor);
$offset = max(0, $offset - $limit / 2); $offset = max(0, $offset - $limit / 2);
} else { } else {
$offset = $context->endpoint->extractOffsetValue($context, $context->endpoint->defaultExtracts($context)); $offset = $context->endpoint->extractOffsetValue($context, $context->endpoint->defaultExtracts($context));
@@ -263,7 +269,7 @@ class DiscussionResource extends AbstractDatabaseResource
$actor = $context->getActor(); $actor = $context->getActor();
if ($actor->exists) { if ($actor->exists) {
resolve(Dispatcher::class)->dispatch( $this->bus->dispatch(
new ReadDiscussion($model->id, $actor, 1) new ReadDiscussion($model->id, $actor, 1)
); );
} }

View File

@@ -12,10 +12,22 @@ use Flarum\Group\Group;
use Flarum\Http\UrlGenerator; use Flarum\Http\UrlGenerator;
use Flarum\Settings\SettingsRepositoryInterface; use Flarum\Settings\SettingsRepositoryInterface;
use Illuminate\Contracts\Filesystem\Factory; use Illuminate\Contracts\Filesystem\Factory;
use Illuminate\Contracts\Filesystem\Filesystem;
use stdClass; use stdClass;
class ForumResource extends AbstractResource implements Findable class ForumResource extends AbstractResource implements Findable
{ {
protected Filesystem $assetsFilesystem;
public function __construct(
protected UrlGenerator $url,
protected SettingsRepositoryInterface $settings,
protected Config $config,
Factory $filesystemFactory
) {
$this->assetsFilesystem = $filesystemFactory->disk('flarum-assets');
}
public function type(): string public function type(): string
{ {
return 'forums'; return 'forums';
@@ -42,21 +54,16 @@ class ForumResource extends AbstractResource implements Findable
public function fields(): array public function fields(): array
{ {
$url = resolve(UrlGenerator::class); $forumUrl = $this->url->to('forum')->base();
$settings = resolve(SettingsRepositoryInterface::class);
$config = resolve(Config::class);
$assetsFilesystem = resolve(Factory::class)->disk('flarum-assets');
$forumUrl = $url->to('forum')->base();
$path = parse_url($forumUrl, PHP_URL_PATH) ?: ''; $path = parse_url($forumUrl, PHP_URL_PATH) ?: '';
return [ return [
Schema\Str::make('title') Schema\Str::make('title')
->get(fn () => $settings->get('forum_title')), ->get(fn () => $this->settings->get('forum_title')),
Schema\Str::make('description') Schema\Str::make('description')
->get(fn () => $settings->get('forum_description')), ->get(fn () => $this->settings->get('forum_description')),
Schema\Boolean::make('showLanguageSelector') Schema\Boolean::make('showLanguageSelector')
->get(fn () => $settings->get('show_language_selector', true)), ->get(fn () => $this->settings->get('show_language_selector', true)),
Schema\Str::make('baseUrl') Schema\Str::make('baseUrl')
->get(fn () => $forumUrl), ->get(fn () => $forumUrl),
Schema\Str::make('basePath') Schema\Str::make('basePath')
@@ -64,29 +71,29 @@ class ForumResource extends AbstractResource implements Findable
Schema\Str::make('baseOrigin') Schema\Str::make('baseOrigin')
->get(fn () => substr($forumUrl, 0, strlen($forumUrl) - strlen($path))), ->get(fn () => substr($forumUrl, 0, strlen($forumUrl) - strlen($path))),
Schema\Str::make('debug') Schema\Str::make('debug')
->get(fn () => $config->inDebugMode()), ->get(fn () => $this->config->inDebugMode()),
Schema\Str::make('apiUrl') Schema\Str::make('apiUrl')
->get(fn () => $url->to('api')->base()), ->get(fn () => $this->url->to('api')->base()),
Schema\Str::make('welcomeTitle') Schema\Str::make('welcomeTitle')
->get(fn () => $settings->get('welcome_title')), ->get(fn () => $this->settings->get('welcome_title')),
Schema\Str::make('welcomeMessage') Schema\Str::make('welcomeMessage')
->get(fn () => $settings->get('welcome_message')), ->get(fn () => $this->settings->get('welcome_message')),
Schema\Str::make('themePrimaryColor') Schema\Str::make('themePrimaryColor')
->get(fn () => $settings->get('theme_primary_color')), ->get(fn () => $this->settings->get('theme_primary_color')),
Schema\Str::make('themeSecondaryColor') Schema\Str::make('themeSecondaryColor')
->get(fn () => $settings->get('theme_secondary_color')), ->get(fn () => $this->settings->get('theme_secondary_color')),
Schema\Str::make('logoUrl') Schema\Str::make('logoUrl')
->get(fn () => $this->getLogoUrl()), ->get(fn () => $this->getLogoUrl()),
Schema\Str::make('faviconUrl') Schema\Str::make('faviconUrl')
->get(fn () => $this->getFaviconUrl()), ->get(fn () => $this->getFaviconUrl()),
Schema\Str::make('headerHtml') Schema\Str::make('headerHtml')
->get(fn () => $settings->get('custom_header')), ->get(fn () => $this->settings->get('custom_header')),
Schema\Str::make('footerHtml') Schema\Str::make('footerHtml')
->get(fn () => $settings->get('custom_footer')), ->get(fn () => $this->settings->get('custom_footer')),
Schema\Boolean::make('allowSignUp') Schema\Boolean::make('allowSignUp')
->get(fn () => $settings->get('allow_sign_up')), ->get(fn () => $this->settings->get('allow_sign_up')),
Schema\Str::make('defaultRoute') Schema\Str::make('defaultRoute')
->get(fn () => $settings->get('default_route')), ->get(fn () => $this->settings->get('default_route')),
Schema\Boolean::make('canViewForum') Schema\Boolean::make('canViewForum')
->get(fn ($model, Context $context) => $context->getActor()->can('viewForum')), ->get(fn ($model, Context $context) => $context->getActor()->can('viewForum')),
Schema\Boolean::make('canStartDiscussion') Schema\Boolean::make('canStartDiscussion')
@@ -100,13 +107,13 @@ class ForumResource extends AbstractResource implements Findable
Schema\Boolean::make('canEditUserCredentials') Schema\Boolean::make('canEditUserCredentials')
->get(fn ($model, Context $context) => $context->getActor()->hasPermission('user.editCredentials')), ->get(fn ($model, Context $context) => $context->getActor()->hasPermission('user.editCredentials')),
Schema\Str::make('assetsBaseUrl') Schema\Str::make('assetsBaseUrl')
->get(fn () => rtrim($assetsFilesystem->url(''), '/')), ->get(fn () => rtrim($this->assetsFilesystem->url(''), '/')),
Schema\Str::make('jsChunksBaseUrl') Schema\Str::make('jsChunksBaseUrl')
->get(fn () => $assetsFilesystem->url('js')), ->get(fn () => $this->assetsFilesystem->url('js')),
Schema\Str::make('adminUrl') Schema\Str::make('adminUrl')
->visible(fn ($model, Context $context) => $context->getActor()->can('administrate')) ->visible(fn ($model, Context $context) => $context->getActor()->can('administrate'))
->get(fn () => $url->to('admin')->base()), ->get(fn () => $this->url->to('admin')->base()),
Schema\Str::make('version') Schema\Str::make('version')
->visible(fn ($model, Context $context) => $context->getActor()->can('administrate')) ->visible(fn ($model, Context $context) => $context->getActor()->can('administrate'))
->get(fn () => Application::VERSION), ->get(fn () => Application::VERSION),
@@ -123,20 +130,20 @@ class ForumResource extends AbstractResource implements Findable
protected function getLogoUrl(): ?string protected function getLogoUrl(): ?string
{ {
$logoPath = resolve(SettingsRepositoryInterface::class)->get('logo_path'); $logoPath = $this->settings->get('logo_path');
return $logoPath ? $this->getAssetUrl($logoPath) : null; return $logoPath ? $this->getAssetUrl($logoPath) : null;
} }
protected function getFaviconUrl(): ?string protected function getFaviconUrl(): ?string
{ {
$faviconPath = resolve(SettingsRepositoryInterface::class)->get('favicon_path'); $faviconPath = $this->settings->get('favicon_path');
return $faviconPath ? $this->getAssetUrl($faviconPath) : null; return $faviconPath ? $this->getAssetUrl($faviconPath) : null;
} }
public function getAssetUrl(string $assetPath): string public function getAssetUrl(string $assetPath): string
{ {
return resolve(Factory::class)->disk('flarum-assets')->url($assetPath); return $this->assetsFilesystem->url($assetPath);
} }
} }

View File

@@ -15,6 +15,11 @@ use Tobyz\JsonApiServer\Laravel\Sort\SortColumn;
class GroupResource extends AbstractDatabaseResource class GroupResource extends AbstractDatabaseResource
{ {
public function __construct(
protected TranslatorInterface $translator
) {
}
public function type(): string public function type(): string
{ {
return 'groups'; return 'groups';
@@ -92,7 +97,7 @@ class GroupResource extends AbstractDatabaseResource
private function translateGroupName(string $name): string private function translateGroupName(string $name): string
{ {
$translation = resolve(TranslatorInterface::class)->trans($key = 'core.group.'.strtolower($name)); $translation = $this->translator->trans($key = 'core.group.'.strtolower($name));
if ($translation !== $key) { if ($translation !== $key) {
return $translation; return $translation;

View File

@@ -14,6 +14,12 @@ use Tobyz\JsonApiServer\Pagination\Pagination;
class NotificationResource extends AbstractDatabaseResource class NotificationResource extends AbstractDatabaseResource
{ {
public function __construct(
protected Dispatcher $bus,
protected NotificationRepository $notifications,
) {
}
public function type(): string public function type(): string
{ {
return 'notifications'; return 'notifications';
@@ -30,7 +36,7 @@ class NotificationResource extends AbstractDatabaseResource
/** @var Pagination $pagination */ /** @var Pagination $pagination */
$pagination = ($context->endpoint->paginationResolver)($context); $pagination = ($context->endpoint->paginationResolver)($context);
return resolve(NotificationRepository::class)->query($context->getActor(), $pagination->limit, $pagination->offset); return $this->notifications->query($context->getActor(), $pagination->limit, $pagination->offset);
} }
return parent::query($context); return parent::query($context);
@@ -57,7 +63,7 @@ class NotificationResource extends AbstractDatabaseResource
public function fields(): array public function fields(): array
{ {
$subjectTypes = resolve(JsonApi::class)->typesForModels( $subjectTypes = $this->api->typesForModels(
(new Notification())->getSubjectModels() (new Notification())->getSubjectModels()
); );
@@ -71,7 +77,7 @@ class NotificationResource extends AbstractDatabaseResource
->writable() ->writable()
->get(fn (Notification $notification) => (bool) $notification->read_at) ->get(fn (Notification $notification) => (bool) $notification->read_at)
->set(function (Notification $notification, Context $context) { ->set(function (Notification $notification, Context $context) {
resolve(Dispatcher::class)->dispatch( $this->bus->dispatch(
new ReadNotification($notification->id, $context->getActor()) new ReadNotification($notification->id, $context->getActor())
); );
}), }),

View File

@@ -23,6 +23,14 @@ use Tobyz\JsonApiServer\Laravel\Sort\SortColumn;
class PostResource extends AbstractDatabaseResource class PostResource extends AbstractDatabaseResource
{ {
public function __construct(
protected PostRepository $posts,
protected TranslatorInterface $translator,
protected LogReporter $log,
protected Dispatcher $bus
) {
}
public function type(): string public function type(): string
{ {
return 'posts'; return 'posts';
@@ -115,7 +123,7 @@ class PostResource extends AbstractDatabaseResource
} }
$limit = $defaultExtracts['limit']; $limit = $defaultExtracts['limit'];
$offset = resolve(PostRepository::class)->getIndexForNumber((int) $filter['discussion'], $near, $context->getActor()); $offset = $this->posts->getIndexForNumber((int) $filter['discussion'], $near, $context->getActor());
return max(0, $offset - $limit / 2); return max(0, $offset - $limit / 2);
} }
@@ -184,8 +192,8 @@ class PostResource extends AbstractDatabaseResource
$rendered = $post->formatContent($context->request); $rendered = $post->formatContent($context->request);
$post->setAttribute('renderFailed', false); $post->setAttribute('renderFailed', false);
} catch (\Exception $e) { } catch (\Exception $e) {
$rendered = resolve(TranslatorInterface::class)->trans('core.lib.error.render_failed_message'); $rendered = $this->translator->trans('core.lib.error.render_failed_message');
resolve(LogReporter::class)->report($e); $this->log->report($e);
$post->setAttribute('renderFailed', true); $post->setAttribute('renderFailed', true);
} }
@@ -258,7 +266,7 @@ class PostResource extends AbstractDatabaseResource
// in the discussion; thus, we will mark the discussion as read if // in the discussion; thus, we will mark the discussion as read if
// they are logged in. // they are logged in.
if ($actor->exists) { if ($actor->exists) {
resolve(Dispatcher::class)->dispatch( $this->bus->dispatch(
new ReadDiscussion($model->discussion_id, $actor, $model->number) new ReadDiscussion($model->discussion_id, $actor, $model->number)
); );
} }

View File

@@ -9,6 +9,7 @@ use Flarum\Foundation\ValidationException;
use Flarum\Http\SlugManager; use Flarum\Http\SlugManager;
use Flarum\Locale\TranslatorInterface; use Flarum\Locale\TranslatorInterface;
use Flarum\Settings\SettingsRepositoryInterface; use Flarum\Settings\SettingsRepositoryInterface;
use Flarum\User\AvatarUploader;
use Flarum\User\Event\Deleting; use Flarum\User\Event\Deleting;
use Flarum\User\Event\GroupsChanged; use Flarum\User\Event\GroupsChanged;
use Flarum\User\Event\RegisteringFromProvider; use Flarum\User\Event\RegisteringFromProvider;
@@ -19,11 +20,21 @@ use Flarum\User\User;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Arr; use Illuminate\Support\Arr;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Intervention\Image\ImageManager;
use InvalidArgumentException; use InvalidArgumentException;
use Tobyz\JsonApiServer\Laravel\Sort\SortColumn; use Tobyz\JsonApiServer\Laravel\Sort\SortColumn;
class UserResource extends AbstractDatabaseResource class UserResource extends AbstractDatabaseResource
{ {
public function __construct(
protected TranslatorInterface $translator,
protected SlugManager $slugManager,
protected SettingsRepositoryInterface $settings,
protected ImageManager $imageManager,
protected AvatarUploader $avatarUploader
) {
}
public function type(): string public function type(): string
{ {
return 'users'; return 'users';
@@ -41,11 +52,10 @@ class UserResource extends AbstractDatabaseResource
public function find(string $id, \Tobyz\JsonApiServer\Context $context): ?object public function find(string $id, \Tobyz\JsonApiServer\Context $context): ?object
{ {
$slugManager = resolve(SlugManager::class);
$actor = $context->getActor(); $actor = $context->getActor();
if (Arr::get($context->request->getQueryParams(), 'bySlug', false)) { if (Arr::get($context->request->getQueryParams(), 'bySlug', false)) {
$user = $slugManager->forResource(User::class)->fromSlug($id, $actor); $user = $this->slugManager->forResource(User::class)->fromSlug($id, $actor);
} else { } else {
$user = $this->query($context)->findOrFail($id); $user = $this->query($context)->findOrFail($id);
} }
@@ -58,9 +68,7 @@ class UserResource extends AbstractDatabaseResource
return [ return [
Endpoint\Create::make() Endpoint\Create::make()
->visible(function (Context $context) { ->visible(function (Context $context) {
$settings = resolve(SettingsRepositoryInterface::class); if (! $this->settings->get('allow_sign_up')) {
if (! $settings->get('allow_sign_up')) {
return $context->getActor()->isAdmin(); return $context->getActor()->isAdmin();
} }
@@ -101,7 +109,7 @@ class UserResource extends AbstractDatabaseResource
public function fields(): array public function fields(): array
{ {
$translator = resolve(TranslatorInterface::class); $translator = $this->translator;
return [ return [
Schema\Str::make('username') Schema\Str::make('username')
@@ -203,7 +211,7 @@ class UserResource extends AbstractDatabaseResource
Schema\Str::make('avatarUrl'), Schema\Str::make('avatarUrl'),
Schema\Str::make('slug') Schema\Str::make('slug')
->get(function (User $user) { ->get(function (User $user) {
return resolve(SlugManager::class)->forResource(User::class)->toSlug($user); return $this->slugManager->forResource(User::class)->toSlug($user);
}), }),
Schema\DateTime::make('joinTime') Schema\DateTime::make('joinTime')
->property('joined_at'), ->property('joined_at'),
@@ -363,12 +371,7 @@ class UserResource extends AbstractDatabaseResource
*/ */
private function uploadAvatarFromUrl(User $user, string $url): void private function uploadAvatarFromUrl(User $user, string $url): void
{ {
// @todo: constructor dependency injection $urlValidator = $this->validation->make(compact('url'), [
$this->validator = resolve(\Illuminate\Contracts\Validation\Factory::class);
$this->imageManager = resolve(\Intervention\Image\ImageManager::class);
$this->avatarUploader = resolve(\Flarum\User\AvatarUploader::class);
$urlValidator = $this->validator->make(compact('url'), [
'url' => 'required|active_url', 'url' => 'required|active_url',
]); ]);

View File

@@ -10,6 +10,7 @@
namespace Flarum\Http; namespace Flarum\Http;
use Closure; use Closure;
use Flarum\Api\JsonApi;
use Flarum\Frontend\Controller as FrontendController; use Flarum\Frontend\Controller as FrontendController;
use Illuminate\Contracts\Container\Container; use Illuminate\Contracts\Container\Container;
use InvalidArgumentException; use InvalidArgumentException;
@@ -45,8 +46,8 @@ class RouteHandlerFactory
public function toApiResource(string $resourceClass, string $endpointClass): Closure public function toApiResource(string $resourceClass, string $endpointClass): Closure
{ {
return function (Request $request, array $routeParams) use ($resourceClass, $endpointClass) { return function (Request $request, array $routeParams) use ($resourceClass, $endpointClass) {
/** @var \Flarum\Api\JsonApi $api */ /** @var JsonApi $api */
$api = $this->container->make(\Flarum\Api\JsonApi::class); $api = $this->container->make(JsonApi::class);
$api->validateQueryParameters($request); $api->validateQueryParameters($request);