1
0
mirror of https://github.com/flarum/core.git synced 2025-10-18 18:26:07 +02:00

Upgrade to L5 + huge refactor + more. closes #2

New stuff:
- Signup + email confirmation.
- Updated authentication strategy with remember cookies. closes #5
- New search system with some example gambits! This is cool - check out
the source. Fulltext drivers will be implemented as decorators
overriding the EloquentPostRepository’s findByContent method.
- Lay down the foundation for bootstrapping the Ember app.
- Update Web layer’s asset manager to properly publish CSS/JS files.
- Console commands to run installation migrations and seeds.

Refactoring:
- New structure: move models, repositories, commands, and events into
their own namespaces, rather than grouping by entity.
- All events are classes.
- Use L5 middleware and command bus implementations.
- Clearer use of repositories and the Active Record pattern.
Repositories are used only for retrieval of ActiveRecord objects, and
then save/delete operations are called directly on those ActiveRecords.
This way, we don’t over-abstract at the cost of Eloquent magic, but
testing is still easy.
- Refactor of Web layer so that it uses the Actions routing
architecture.
- “Actor” concept instead of depending on Laravel’s Auth.
- General cleanup!
This commit is contained in:
Toby Zerner
2015-02-24 20:33:18 +10:30
parent 0e4e44c358
commit 2c46888db5
266 changed files with 5562 additions and 4658 deletions

View File

@@ -0,0 +1,89 @@
<?php namespace Flarum\Api\Actions;
class ApiParams
{
protected $params;
public function __construct(array $params)
{
$this->params = $params;
}
public function get($key, $default = null)
{
return array_get($this->params, $key, $default);
}
public function range($key, $default = null, $min = null, $max = null)
{
$value = (int) $this->get($key, $default);
if (! is_null($min)) {
$value = max($value, $min);
}
if (! is_null($max)) {
$value = min($value, $max);
}
return $value;
}
public function included($available)
{
$requested = explode(',', $this->get('include'));
return array_intersect((array) $available, $requested);
}
// public function explodeIds($ids)
// {
// return array_unique(array_map('intval', array_filter(explode(',', $ids))));
// }
public function in($key, $options)
{
$value = $this->get($key);
if (array_key_exists($key, $options)) {
return $options[$key];
}
if (! in_array($value, $options)) {
$value = reset($options);
}
return $value;
}
public function sort($options)
{
$criteria = (string) $this->get('sort', '');
$order = null;
if ($criteria && $criteria[0] == '-') {
$order = 'desc';
$criteria = substr($criteria, 1);
}
if (! in_array($criteria, $options)) {
$criteria = reset($options);
}
if ($criteria && ! $order) {
$order = 'asc';
}
return [
'field' => $criteria,
'order' => $order,
'string' => ($order == 'desc' ? '-' : '').$criteria
];
}
public function start()
{
return $this->range('start', 0, 0);
}
public function count($default, $max = 100)
{
return $this->range('count', $default, 1, $max);
}
}

View File

@@ -0,0 +1,130 @@
<?php namespace Flarum\Api\Actions;
use Illuminate\Http\Request;
use Illuminate\Contracts\Bus\Dispatcher;
use Tobscure\JsonApi\Document;
use Flarum\Core\Support\Actor;
use Flarum\Api\Events\CommandWillBeDispatched;
use Flarum\Api\Events\WillRespondWithDocument;
use Flarum\Web\Actions\Action;
use Config;
use App;
use Response;
abstract class BaseAction extends Action
{
abstract protected function run(ApiParams $params);
public function __construct(Actor $actor, Dispatcher $bus)
{
$this->actor = $actor;
$this->bus = $bus;
}
public function handle(Request $request, $routeParams = [])
{
$this->registerErrorHandlers(); // @todo convert to middleware and add to route group?
$params = array_merge($request->all(), $routeParams);
return $this->call($params);
}
public function call($params = [])
{
$params = new ApiParams($params);
return $this->run($params);
}
public function hydrate($object, $params)
{
foreach ($params as $k => $v) {
$object->$k = $v;
}
}
protected function dispatch($command, $params)
{
$this->event(new CommandWillBeDispatched($command, $params));
return $this->bus->dispatch($command);
}
protected function event($event)
{
event($event);
}
public function document()
{
return new Document;
}
protected function buildUrl($route, $params = [], $input = [])
{
$url = route('flarum.api.'.$route, $params);
$queryString = $input ? '?'.http_build_query($input) : '';
return $url.$queryString;
}
protected function respondWithoutContent($statusCode = 204, $headers = [])
{
return Response::make('', $statusCode, $headers);
}
protected function respondWithArray($array, $statusCode = 200, $headers = [])
{
return Response::json($array, $statusCode, $headers);
}
protected function respondWithDocument($document, $statusCode = 200, $headers = [])
{
$headers['Content-Type'] = 'application/vnd.api+json';
$this->event(new WillRespondWithDocument($document, $statusCode, $headers));
return $this->respondWithArray($document->toArray(), $statusCode, $headers);
}
protected function registerErrorHandlers()
{
// if (! Config::get('app.debug')) {
// App::error(function ($exception, $code) {
// return $this->respondWithError('ApplicationError', $code);
// });
// }
// App::error(function (ModelNotFoundException $exception) {
// return $this->respondWithError('ResourceNotFound', 404);
// });
// App::error(function (ValidationFailureException $exception) {
// $errors = [];
// foreach ($exception->getErrors()->getMessages() as $field => $messages) {
// $errors[] = [
// 'code' => 'ValidationFailure',
// 'detail' => implode("\n", $messages),
// 'path' => $field
// ];
// }
// return $this->respondWithErrors($errors, 422);
// });
}
protected function respondWithErrors($errors, $httpCode = 500)
{
return Response::json(['errors' => $errors], $httpCode);
}
protected function respondWithError($error, $httpCode = 500, $detail = null)
{
$error = ['code' => $error];
if ($detail) {
$error['detail'] = $detail;
}
return $this->respondWithErrors([$error], $httpCode);
}
}

View File

@@ -0,0 +1,41 @@
<?php namespace Flarum\Api\Actions\Discussions;
use Flarum\Core\Commands\StartDiscussionCommand;
use Flarum\Core\Commands\ReadDiscussionCommand;
use Flarum\Api\Actions\BaseAction;
use Flarum\Api\Actions\ApiParams;
use Flarum\Api\Serializers\DiscussionSerializer;
class CreateAction extends BaseAction
{
/**
* Start a new discussion.
*
* @return Response
*/
protected function run(ApiParams $params)
{
// By default, the only required attributes of a discussion are the
// title and the content. We'll extract these from the rbaseequest data
// and pass them through to the StartDiscussionCommand.
$title = $params->get('discussions.title');
$content = $params->get('discussions.content');
$user = $this->actor->getUser();
$command = new StartDiscussionCommand($title, $content, $user, app('flarum.forum'));
$discussion = $this->dispatch($command, $params);
// After creating the discussion, we assume that the user has seen all
// of the posts in the discussion; thus, we will mark the discussion
// as read if they are logged in.
if ($user->exists) {
$command = new ReadDiscussionCommand($discussion->id, $user, 1);
$this->dispatch($command, $params);
}
$serializer = new DiscussionSerializer(['posts']);
$document = $this->document()->setPrimaryElement($serializer->resource($discussion));
return $this->respondWithDocument($document);
}
}

View File

@@ -0,0 +1,23 @@
<?php namespace Flarum\Api\Actions\Discussions;
use Flarum\Core\Commands\DeleteDiscussionCommand;
use Flarum\Api\Actions\BaseAction;
use Flarum\Api\Actions\ApiParams;
class DeleteAction extends BaseAction
{
/**
* Delete a discussion.
*
* @return Response
*/
protected function run(ApiParams $params)
{
$discussionId = $params->get('id');
$command = new DeleteDiscussionCommand($discussionId, $this->actor->getUser());
$this->dispatch($command, $params);
return $this->respondWithoutContent();
}
}

View File

@@ -0,0 +1,81 @@
<?php namespace Flarum\Api\Actions\Discussions;
use Flarum\Core\Search\Discussions\DiscussionSearchCriteria;
use Flarum\Core\Search\Discussions\DiscussionSearcher;
use Flarum\Core\Support\Actor;
use Flarum\Api\Actions\BaseAction;
use Flarum\Api\Actions\ApiParams;
use Flarum\Api\Serializers\DiscussionSerializer;
class IndexAction extends BaseAction
{
/**
* The discussion searcher.
*
* @var DiscussionSearcher
*/
protected $searcher;
/**
* Instantiate the action.
*
* @param DiscussionSearcher $searcher
*/
public function __construct(Actor $actor, DiscussionSearcher $searcher)
{
$this->actor = $actor;
$this->searcher = $searcher;
}
/**
* Show a list of discussions.
*
* @todo custom rate limit for this function? determined by if $key was valid?
* @return Response
*/
protected function run(ApiParams $params)
{
$query = $params->get('q');
$start = $params->start();
$include = $params->included(['startPost', 'lastPost', 'relevantPosts']);
$count = $params->count(20, 50);
$sort = $params->sort(['', 'lastPost', 'replies', 'created']);
$relations = array_merge(['startUser', 'lastUser'], $include);
// Set up the discussion finder with our search criteria, and get the
// requested range of results with the necessary relations loaded.
$criteria = new DiscussionSearchCriteria($this->actor->getUser(), $query, $sort['field'], $sort['order']);
$load = array_merge($relations, ['state']);
$results = $this->searcher->search($criteria, $count, $start, $load);
$document = $this->document();
if (($total = $results->getTotal()) !== null) {
$document->addMeta('total', $total);
}
// If there are more results, then we need to construct a URL to the
// next results page and add that to the metadata. We do this by
// compacting all of the valid query parameters which have been
// specified.
if ($results->areMoreResults()) {
$start += $count;
$include = implode(',', $include);
$sort = $sort['string'];
$input = array_filter(compact('query', 'sort', 'start', 'count', 'include'));
$moreUrl = $this->buildUrl('discussions.index', [], $input);
} else {
$moreUrl = '';
}
$document->addMeta('moreUrl', $moreUrl);
// Finally, we can set up the discussion serializer and use it to create
// a collection of discussion results.
$serializer = new DiscussionSerializer($relations);
$document->setPrimaryElement($serializer->collection($results->getDiscussions()));
return $this->respondWithDocument($document);
}
}

View File

@@ -0,0 +1,70 @@
<?php namespace Flarum\Api\Actions\Discussions;
use Flarum\Core\Support\Actor;
use Flarum\Core\Repositories\DiscussionRepositoryInterface as DiscussionRepository;
use Flarum\Core\Repositories\PostRepositoryInterface as PostRepository;
use Flarum\Api\Actions\BaseAction;
use Flarum\Api\Actions\ApiParams;
use Flarum\Api\Actions\Posts\GetsPostsForDiscussion;
use Flarum\Api\Serializers\DiscussionSerializer;
class ShowAction extends BaseAction
{
use GetsPostsForDiscussion;
/**
* The discussion repository.
*
* @var DiscussionRepository
*/
protected $discussions;
/**
* The post repository.
*
* @var PostRepository
*/
protected $posts;
/**
* Instantiate the action.
*
* @param PostRepository $posts
*/
public function __construct(Actor $actor, DiscussionRepository $discussions, PostRepository $posts)
{
$this->actor = $actor;
$this->discussions = $discussions;
$this->posts = $posts;
}
/**
* Show a single discussion.
*
* @return Response
*/
protected function run(ApiParams $params)
{
$include = $params->included(['startPost', 'lastPost', 'posts']);
$discussion = $this->discussions->findOrFail($params->get('id'), $this->actor->getUser());
if (in_array('posts', $include)) {
$relations = ['user', 'user.groups', 'editUser', 'hideUser'];
$discussion->posts = $this->getPostsForDiscussion($params, $discussion->id)->load($relations);
$include = array_merge($include, array_map(function ($relation) {
return 'posts.'.$relation;
}, $relations));
}
// Set up the discussion serializer, which we will use to create the
// document's primary resource. As well as including the requested
// relations, we will specify that we want the 'posts' relation to be
// linked so that a list of post IDs will show up in the response.
$serializer = new DiscussionSerializer($include, ['posts']);
$document = $this->document()->setPrimaryElement($serializer->resource($discussion));
return $this->respondWithDocument($document);
}
}

View File

@@ -0,0 +1,54 @@
<?php namespace Flarum\Api\Actions\Discussions;
use Flarum\Core\Commands\EditDiscussionCommand;
use Flarum\Core\Commands\ReadDiscussionCommand;
use Flarum\Core\Exceptions\PermissionDeniedException;
use Flarum\Api\Actions\BaseAction;
use Flarum\Api\Actions\ApiParams;
use Flarum\Api\Serializers\DiscussionSerializer;
class UpdateAction extends BaseAction
{
/**
* Edit a discussion. Allows renaming the discussion, and updating its read
* state with regards to the current user.
*
* @return Response
*/
protected function run(ApiParams $params)
{
$discussionId = $params->get('id');
$user = $this->actor->getUser();
// First, we will run the EditDiscussionCommand. This will update the
// discussion's direct properties; by default, this is just the title.
// As usual, however, we will fire an event to allow plugins to update
// additional properties.
if ($data = array_except($params->get('discussions'), ['readNumber'])) {
$command = new EditDiscussionCommand($discussionId, $user);
$this->hydrate($command, $params->get('discussions'));
$discussion = $this->dispatch($command, $params);
}
// Next, if a read number was specified in the request, we will run the
// ReadDiscussionCommand.
//
// @todo Currently, if the user doesn't have permission to edit a
// discussion, they're unable to update their readNumber because a
// PermissionsDeniedException is thrown by the
// EditDiscussionCommand above. So this needs to be extracted into
// its own endpoint.
if ($readNumber = $params->get('discussions.readNumber')) {
$command = new ReadDiscussionCommand($discussionId, $user, $readNumber);
$this->dispatch($command, $params);
}
// Presumably, the discussion was updated successfully. (One of the command
// handlers would have thrown an exception if not.) We set this
// discussion as our document's primary element.
$serializer = new DiscussionSerializer(['addedPosts', 'addedPosts.user']);
$document = $this->document()->setPrimaryElement($serializer->resource($discussion));
return $this->respondWithDocument($document);
}
}

View File

@@ -0,0 +1,18 @@
<?php namespace Flarum\Api\Actions\Groups;
use Flarum\Core\Models\Group;
use Flarum\Api\Actions\Base;
use Flarum\Api\Serializers\GroupSerializer;
class Index extends Base
{
protected function run()
{
$groups = Group::get();
$serializer = new GroupSerializer;
$this->document->setPrimaryElement($serializer->collection($groups));
return $this->respondWithDocument();
}
}

View File

@@ -0,0 +1,47 @@
<?php namespace Flarum\Api\Actions\Posts;
use Flarum\Core\Commands\PostReplyCommand;
use Flarum\Core\Commands\ReadDiscussionCommand;
use Flarum\Api\Actions\ApiParams;
use Flarum\Api\Actions\BaseAction;
use Flarum\Api\Serializers\PostSerializer;
class CreateAction extends BaseAction
{
/**
* Reply to a discussion.
*
* @return Response
*/
protected function run(ApiParams $params)
{
$user = $this->actor->getUser();
// We've received a request to post a reply. By default, the only
// required attributes of a post is the ID of the discussion to post in,
// the post content, and the author's user account. Let's set up a
// command with this information. We also fire an event to allow plugins
// to add data to the command.
$discussionId = $params->get('posts.links.discussion');
$content = $params->get('posts.content');
$command = new PostReplyCommand($discussionId, $content, $user);
$post = $this->dispatch($command, $params);
// After replying, we assume that the user has seen all of the posts
// in the discussion; thus, we will mark the discussion as read if
// they are logged in.
if ($user->exists) {
$command = new ReadDiscussionCommand($discussionId, $user, $post->number);
$this->dispatch($command, $params);
}
// Presumably, the post was created successfully. (The command handler
// would have thrown an exception if not.) We set this post as our
// document's primary element.
$serializer = new PostSerializer;
$document = $this->document()->setPrimaryElement($serializer->resource($post));
return $this->respondWithDocument($document, 201);
}
}

View File

@@ -0,0 +1,23 @@
<?php namespace Flarum\Api\Actions\Posts;
use Flarum\Core\Commands\DeletePostCommand;
use Flarum\Api\Actions\ApiParams;
use Flarum\Api\Actions\BaseAction;
class DeleteAction extends BaseAction
{
/**
* Delete a post.
*
* @return Response
*/
protected function run(ApiParams $params)
{
$postId = $params->get('id');
$command = new DeletePostCommand($postId, $this->actor->getUser());
$this->dispatch($command, $params);
return $this->respondWithoutContent();
}
}

View File

@@ -0,0 +1,31 @@
<?php namespace Flarum\Api\Actions\Posts;
use Flarum\Core\Repositories\PostRepositoryInterface;
use Flarum\Core\Models\User;
use Flarum\Api\Actions\ApiParams;
trait GetsPostsForDiscussion
{
protected function getPostsForDiscussion(ApiParams $params, $discussionId)
{
$sort = $params->sort(['time']);
$count = $params->count(20, 50);
$user = $this->actor->getUser();
if (($near = $params->get('near')) > 1) {
$start = $this->posts->getIndexForNumber($discussionId, $near, $user);
$start = max(0, $start - $count / 2);
} else {
$start = 0;
}
return $this->posts->findByDiscussion(
$discussionId,
$user,
$sort['field'],
$sort['order'] ?: 'asc',
$count,
$start
);
}
}

View File

@@ -0,0 +1,62 @@
<?php namespace Flarum\Api\Actions\Posts;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Flarum\Core\Repositories\PostRepositoryInterface;
use Flarum\Core\Support\Actor;
use Flarum\Api\Actions\BaseAction;
use Flarum\Api\Actions\ApiParams;
use Flarum\Api\Serializers\PostSerializer;
class IndexAction extends BaseAction
{
use GetsPostsForDiscussion;
/**
* The post repository.
*
* @var Post
*/
protected $posts;
/**
* Instantiate the action.
*
* @param Post $posts
*/
public function __construct(Actor $actor, PostRepositoryInterface $posts)
{
$this->actor = $actor;
$this->posts = $posts;
}
/**
* Show posts from a discussion, or by providing an array of IDs.
*
* @return Response
*/
protected function run(ApiParams $params)
{
$postIds = (array) $params->get('ids');
$include = ['user', 'user.groups', 'editUser', 'hideUser'];
$user = $this->actor->getUser();
if (count($postIds)) {
$posts = $this->posts->findByIds($postIds, $user);
} else {
$discussionId = $params->get('discussions');
$posts = $this->getPostsForDiscussion($params, $discussionId, $user);
}
if (! count($posts)) {
throw new ModelNotFoundException;
}
// Finally, we can set up the post serializer and use it to create
// a post resource or collection, depending on how many posts were
// requested.
$serializer = new PostSerializer($include);
$document = $this->document()->setPrimaryElement($serializer->collection($posts->load($include)));
return $this->respondWithDocument($document);
}
}

View File

@@ -0,0 +1,42 @@
<?php namespace Flarum\Api\Actions\Posts;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Flarum\Core\Repositories\PostRepositoryInterface;
use Flarum\Core\Support\Actor;
use Flarum\Api\Actions\BaseAction;
use Flarum\Api\Actions\ApiParams;
use Flarum\Api\Serializers\PostSerializer;
class ShowAction extends BaseAction
{
protected $posts;
public function __construct(Actor $actor, PostRepositoryInterface $posts)
{
$this->actor = $actor;
$this->posts = $posts;
}
/**
* Show a single post by ID.
*
* @return Response
*/
protected function run(ApiParams $params)
{
$id = $params->get('id');
$posts = $this->posts->findOrFail($id, $this->actor->getUser());
$include = $params->included(['discussion', 'replyTo']);
$relations = array_merge(['user', 'editUser', 'hideUser'], $include);
$posts->load($relations);
// Finally, we can set up the post serializer and use it to create
// a post resource or collection, depending on how many posts were
// requested.
$serializer = new PostSerializer($relations);
$document = $this->document()->setPrimaryElement($serializer->resource($posts->first()));
return $this->respondWithDocument($document);
}
}

View File

@@ -0,0 +1,34 @@
<?php namespace Flarum\Api\Actions\Posts;
use Flarum\Core\Commands\EditPostCommand;
use Flarum\Api\Actions\ApiParams;
use Flarum\Api\Actions\BaseAction;
use Flarum\Api\Serializers\PostSerializer;
class UpdateAction extends BaseAction
{
/**
* Edit a post. Allows revision of content, and hiding/unhiding.
*
* @return Response
*/
protected function run(ApiParams $params)
{
$postId = $params->get('id');
// EditPost is a single command because we don't want to allow partial
// updates (i.e. if we were to run one command and then another, if the
// second one failed, the first one would still have succeeded.)
$command = new EditPostCommand($postId, $this->actor->getUser());
$this->hydrate($command, $params->get('posts'));
$post = $this->dispatch($command, $params);
// Presumably, the post was updated successfully. (The command handler
// would have thrown an exception if not.) We set this post as our
// document's primary element.
$serializer = new PostSerializer;
$document = $this->document()->setPrimaryElement($serializer->resource($post));
return $this->respondWithDocument($document);
}
}

View File

@@ -0,0 +1,43 @@
<?php namespace Flarum\Api\Actions;
use Illuminate\Http\JsonResponse;
use Illuminate\Contracts\Bus\Dispatcher;
use Flarum\Core\Commands\GenerateAccessTokenCommand;
use Flarum\Core\Repositories\UserRepositoryInterface;
use Flarum\Api\Actions\BaseAction;
class TokenAction extends BaseAction
{
protected $users;
public function __construct(UserRepositoryInterface $users, Dispatcher $bus)
{
$this->users = $users;
$this->bus = $bus;
}
/**
* Log in and return a token.
*
* @return Response
*/
public function run(ApiParams $params)
{
$identification = $params->get('identification');
$password = $params->get('password');
$user = $this->users->findByIdentification($identification);
if (! $user || ! $user->checkPassword($password)) {
return $this->respondWithError('invalidLogin', 401);
}
$command = new GenerateAccessTokenCommand($user->id);
$token = $this->dispatch($command, $params);
return new JsonResponse([
'token' => $token->id,
'userId' => $user->id
]);
}
}

View File

@@ -0,0 +1,36 @@
<?php namespace Flarum\Api\Actions\Users;
use Flarum\Core\Commands\RegisterUserCommand;
use Flarum\Api\Actions\ApiParams;
use Flarum\Api\Actions\BaseAction;
use Flarum\Api\Serializers\UserSerializer;
class CreateAction extends BaseAction
{
/**
* Register a user.
*
* @return Response
*/
protected function run(ApiParams $params)
{
// We've received a request to register a user. By default, the only
// required attributes of a user is the username, email, and password.
// Let's set up a command with this information. We also fire an event
// to allow plugins to add data to the command.
$username = $params->get('users.username');
$email = $params->get('users.email');
$password = $params->get('users.password');
$command = new RegisterUserCommand($username, $email, $password, $this->actor->getUser());
$this->dispatch($command, $params);
// Presumably, the user was created successfully. (The command handler
// would have thrown an exception if not.) We set this post as our
// document's primary element.
$serializer = new UserSerializer;
$document = $this->document()->setPrimaryElement($serializer->resource($user));
return $this->respondWithDocument($document, 201);
}
}

View File

@@ -0,0 +1,23 @@
<?php namespace Flarum\Api\Actions\Users;
use Flarum\Core\Commands\DeleteUserCommand;
use Flarum\Api\Actions\ApiParams;
use Flarum\Api\Actions\BaseAction;
class DeleteAction extends BaseAction
{
/**
* Delete a user.
*
* @return Response
*/
protected function run(ApiParams $params)
{
$userId = $params->get('id');
$command = new DeleteUserCommand($userId, $this->actor->getUser());
$this->dispatch($command, $params);
return $this->respondWithoutContent();
}
}

View File

@@ -0,0 +1,83 @@
<?php namespace Flarum\Api\Actions\Users;
use Flarum\Core\Users\User;
use Flarum\Core\Users\UserFinder;
use Flarum\Api\Actions\Base;
use Flarum\Api\Serializers\UserSerializer;
class IndexAction extends BaseAction
{
/**
* The user finder.
*
* @var UserFinder
*/
protected $finder;
/**
* Instantiate the action.
*
* @param UserFinder $finder
*/
public function __construct(UserFinder $finder)
{
$this->finder = $finder;
}
/**
* Show a list of users.
*
* @todo custom rate limit for this function? determined by if $key was valid?
* @return Response
*/
protected function run()
{
$query = $this->input('q');
$key = $this->input('key');
$sort = $this->sort(['', 'username', 'posts', 'discussions', 'lastActive', 'created']);
$start = $this->start();
$count = $this->count(50, 100);
$include = $this->included(['groups']);
$relations = array_merge(['groups'], $include);
// Set up the user finder with our search criteria, and get the
// requested range of results with the necessary relations loaded.
$this->finder->setUser(User::current());
$this->finder->setQuery($query);
$this->finder->setSort($sort['by']);
$this->finder->setOrder($sort['order']);
$this->finder->setKey($key);
$users = $this->finder->results($count, $start);
$users->load($relations);
if (($total = $this->finder->getCount()) !== null) {
$this->document->addMeta('total', $total);
}
if (($key = $this->finder->getKey()) !== null) {
$this->document->addMeta('key', $key);
}
// If there are more results, then we need to construct a URL to the
// next results page and add that to the metadata. We do this by
// compacting all of the valid query parameters which have been
// specified.
if ($this->finder->areMoreResults()) {
$start += $count;
$include = implode(',', $include);
$sort = $sort['string'];
$input = array_filter(compact('query', 'key', 'sort', 'start', 'count', 'include'));
$moreUrl = $this->buildUrl('users.index', [], $input);
} else {
$moreUrl = '';
}
$this->document->addMeta('moreUrl', $moreUrl);
// Finally, we can set up the user serializer and use it to create
// a collection of user results.
$serializer = new UserSerializer($relations);
$this->document->setPrimaryElement($serializer->collection($users));
return $this->respondWithDocument();
}
}

View File

@@ -0,0 +1,38 @@
<?php namespace Flarum\Api\Actions\Users;
use Flarum\Core\Repositories\UserRepositoryInterface;
use Flarum\Core\Support\Actor;
use Flarum\Api\Actions\ApiParams;
use Flarum\Api\Actions\BaseAction;
use Flarum\Api\Serializers\UserSerializer;
class ShowAction extends BaseAction
{
protected $actor;
protected $users;
public function __construct(Actor $actor, UserRepositoryInterface $users)
{
$this->actor = $actor;
$this->users = $users;
}
/**
* Show a single user.
*
* @return Response
*/
public function run(ApiParams $params)
{
$user = $this->users->findOrFail($params->get('id'), $this->actor->getUser());
// Set up the user serializer, which we will use to create the
// document's primary resource. We will specify that we want the
// 'groups' relation to be included by default.
$serializer = new UserSerializer(['groups']);
$document = $this->document()->setPrimaryElement($serializer->resource($user));
return $this->respondWithDocument($document);
}
}

View File

@@ -0,0 +1,35 @@
<?php namespace Flarum\Api\Actions\Users;
use Flarum\Core\Commands\EditUserCommand;
use Flarum\Api\Actions\ApiParams;
use Flarum\Api\Actions\BaseAction;
use Flarum\Api\Serializers\UserSerializer;
class UpdateAction extends BaseAction
{
/**
* Edit a user. Allows renaming the user, changing their email, and setting
* their password.
*
* @return Response
*/
protected function run(ApiParams $params)
{
$userId = $params->get('id');
// EditUser is a single command because we don't want to allow partial
// updates (i.e. if we were to run one command and then another, if the
// second one failed, the first one would still have succeeded.)
$command = new EditUserCommand($userId, $this->actor->getUser());
$this->hydrate($command, $params->get('users'));
$this->dispatch($command);
// Presumably, the user was updated successfully. (The command handler
// would have thrown an exception if not.) We set this user as our
// document's primary element.
$serializer = new UserSerializer;
$document = $this->document()->setPrimaryElement($serializer->resource($user));
return $this->respondWithDocument($document);
}
}

View File

@@ -0,0 +1,29 @@
<?php namespace Flarum\Api;
use Illuminate\Support\ServiceProvider;
use Flarum\Api\Serializers\BaseSerializer;
class ApiServiceProvider extends ServiceProvider
{
/**
* Bootstrap the application events.
*
* @return void
*/
public function boot()
{
include __DIR__.'/routes.php';
BaseSerializer::setActor($this->app['Flarum\Core\Support\Actor']);
}
/**
* Register the service provider.
*
* @return void
*/
public function register()
{
$this->app->singleton('Flarum\Core\Support\Actor');
}
}

View File

@@ -0,0 +1,14 @@
<?php namespace Flarum\Api\Events;
class CommandWillBeDispatched
{
public $command;
public $params;
public function __construct($command, $params)
{
$this->command = $command;
$this->params = $params;
}
}

View File

@@ -0,0 +1,17 @@
<?php namespace Flarum\Api\Events;
class SerializeAttributes
{
public $serializer;
public $model;
public $attributes;
public function __construct($serializer, $model, &$attributes)
{
$this->serializer = $serializer;
$this->model = $model;
$this->attributes = $attributes;
}
}

View File

@@ -0,0 +1,23 @@
<?php namespace Flarum\Api\Events;
class SerializeRelationship
{
public $serializer;
public $model;
public $type;
public $name;
public $relations;
public function __construct($serializer, $model, $type, $name, $relations)
{
$this->serializer = $serializer;
$this->model = $model;
$this->type = $type;
$this->name = $name;
$this->relations = $relations;
}
}

View File

@@ -0,0 +1,17 @@
<?php namespace Flarum\Api\Events;
class WillRespondWithDocument
{
public $document;
public $statusCode;
public $headers;
public function __construct($document, &$statusCode, &$headers)
{
$this->document = $document;
$this->statusCode = $statusCode;
$this->headers = $headers;
}
}

View File

@@ -0,0 +1,29 @@
<?php namespace Flarum\Api\Middleware;
use Flarum\Core\Models\AccessToken;
use Flarum\Core\Support\Actor;
use Closure;
class LoginWithHeaderMiddleware
{
protected $actor;
protected $prefix = 'Token ';
public function __construct(Actor $actor)
{
$this->actor = $actor;
}
public function handle($request, Closure $next)
{
$header = $request->headers->get('authorization');
if (starts_with($header, $this->prefix) &&
($token = substr($header, strlen($this->prefix))) &&
($accessToken = AccessToken::where('id', $token)->first())) {
$this->actor->setUser($accessToken->user);
}
return $next($request);
}
}

View File

@@ -0,0 +1,19 @@
<?php namespace Flarum\Api\Serializers;
use Flarum\Core\Models\Activity;
use Event;
class ActivitySerializer extends BaseSerializer {
public function serialize(Activity $activity)
{
$serialized = [
'id' => (int) $activity->id
];
Event::fire('flarum.api.serialize.activity', [&$serialized]);
return $serialized;
}
}

View File

@@ -0,0 +1,62 @@
<?php namespace Flarum\Api\Serializers;
use Tobscure\JsonApi\SerializerAbstract;
use Flarum\Api\Events\SerializeAttributes;
use Flarum\Api\Events\SerializeRelationship;
use Flarum\Core\Support\Actor;
/**
* A base serializer to call Flarum events at common serialization points.
*/
abstract class BaseSerializer extends SerializerAbstract
{
/**
* The actor who is requesting the serialized objects.
*
* @var \Flarum\Core\Support\Actor
*/
protected static $actor;
/**
* Set the actor who is requesting the serialized objects.
*
* @param \Flarum\Core\Support\Actor $actor
* @return void
*/
public static function setActor(Actor $actor)
{
static::$actor = $actor;
}
/**
* Fire an event to allow custom serialization of attributes.
*
* @param mixed $model The model to serialize.
* @param array $attributes Attributes that have already been serialized.
* @return array
*/
protected function attributesEvent($model, $attributes = [])
{
event(new SerializeAttributes($this, $model, $attributes));
return $attributes;
}
/**
* Fire an event to allow for custom links and includes.
*
* @param string $name
* @param array $arguments
* @return void
*/
public function __call($name, $arguments)
{
if ($link = starts_with($name, 'link') || starts_with($name, 'include')) {
$model = isset($arguments[0]) ? $arguments[0] : null;
$relations = isset($arguments[1]) ? $arguments[1] : null;
$type = $link ? 'link' : 'include';
$name = substr($name, strlen($type));
return event(new SerializeRelationship($this, $model, $type, $name, $relations), null, true);
}
}
}

View File

@@ -0,0 +1,44 @@
<?php namespace Flarum\Api\Serializers;
use Flarum\Core\Models\Discussion;
class DiscussionBasicSerializer extends BaseSerializer
{
/**
* The resource type.
* @var string
*/
protected $type = 'discussions';
/**
* Serialize attributes of a Discussion model for JSON output.
*
* @param Discussion $discussion The Discussion model to serialize.
* @return array
*/
protected function attributes(Discussion $discussion)
{
$attributes = [
'id' => (int) $discussion->id,
'title' => $discussion->title,
];
return $this->attributesEvent($discussion, $attributes);
}
/**
* Get the URL templates where this resource and its related resources can
* be accessed.
*
* @return array
*/
protected function href()
{
$href = [
'discussions' => $this->action('DiscussionsController@show', ['id' => '{discussions.id}']),
'posts' => $this->action('PostsController@indexForDiscussion', ['id' => '{discussions.id}'])
];
return $this->hrefEvent($href);
}
}

View File

@@ -0,0 +1,145 @@
<?php namespace Flarum\Api\Serializers;
use Flarum\Core\Models\Discussion;
use Flarum\Core\Models\DiscussionState;
class DiscussionSerializer extends DiscussionBasicSerializer
{
/**
* Default relations to include.
* @var array
*/
protected $include = ['startUser', 'lastUser'];
/**
* Serialize attributes of a Discussion model for JSON output.
*
* @param Discussion $discussion The Discussion model to serialize.
* @return array
*/
protected function attributes(Discussion $discussion)
{
$attributes = parent::attributes($discussion);
$user = static::$actor->getUser();
$state = $discussion->stateFor($user);
$attributes += [
'commentsCount' => (int) $discussion->comments_count,
'startTime' => $discussion->start_time->toRFC3339String(),
'lastTime' => $discussion->last_time ? $discussion->last_time->toRFC3339String() : null,
'lastPostNumber' => $discussion->last_post_number,
'canReply' => $discussion->can($user, 'reply'),
'canEdit' => $discussion->can($user, 'edit'),
'canDelete' => $discussion->can($user, 'delete'),
'readTime' => $state && $state->read_time ? $state->read_time->toRFC3339String() : null,
'readNumber' => $state ? (int) $state->read_number : 0
];
$this->attributesEvent($discussion, $attributes);
return $attributes;
}
/**
* Get a collection containing a discussion's viewable post IDs.
*
* @param Discussion $discussion
* @return Tobscure\JsonApi\Collection
*/
public function linkPosts(Discussion $discussion)
{
$user = static::$actor->getUser();
return (new PostBasicSerializer)->collection($discussion->posts()->whereCan($user, 'view')->orderBy('time', 'asc')->ids());
}
/**
* Get a collection containing a discussion's viewable posts. Assumes that
* the discussion model's posts attributes has been filled.
*
* @param Discussion $discussion
* @param array $relations
* @return Tobscure\JsonApi\Collection
*/
public function includePosts(Discussion $discussion, $relations)
{
return (new PostSerializer($relations))->collection($discussion->posts);
}
/**
* Get a collection containing a discussion's relevant posts. Assumes that
* the discussion model's relevantPosts attributes has been filled (this
* happens in the DiscussionFinder.)
*
* @param Discussion $discussion
* @param array $relations
* @return Tobscure\JsonApi\Collection
*/
public function includeRelevantPosts(Discussion $discussion, $relations)
{
return (new PostBasicSerializer($relations))->collection($discussion->relevantPosts);
}
/**
* Get a resource containing a discussion's start user.
*
* @param Discussion $discussion
* @param array $relations
* @return Tobscure\JsonApi\Resource
*/
public function includeStartUser(Discussion $discussion, $relations)
{
return (new UserBasicSerializer($relations))->resource($discussion->startUser);
}
/**
* Get a resource containing a discussion's starting post.
*
* @param Discussion $discussion
* @param array $relations
* @return Tobscure\JsonApi\Resource
*/
public function includeStartPost(Discussion $discussion, $relations)
{
return (new PostBasicSerializer($relations))->resource($discussion->startPost);
}
/**
* Get a resource containing a discussion's last user.
*
* @param Discussion $discussion
* @param array $relations
* @return Tobscure\JsonApi\Resource
*/
public function includeLastUser(Discussion $discussion, $relations)
{
return (new UserBasicSerializer($relations))->resource($discussion->lastUser);
}
/**
* Get a resource containing a discussion's last post.
*
* @param Discussion $discussion
* @param array $relations
* @return Tobscure\JsonApi\Resource
*/
public function includeLastPost(Discussion $discussion, $relations)
{
return (new PostBasicSerializer($relations))->resource($discussion->lastPost);
}
/**
* Get a resource containing a discussion's list of posts that have been
* added during this request.
*
* @param Discussion $discussion
* @param array $relations
* @return Tobscure\JsonApi\Collection
*/
public function includeAddedPosts(Discussion $discussion, $relations)
{
return (new PostBasicSerializer($relations))->collection($discussion->getAddedPosts());
}
}

View File

@@ -0,0 +1,48 @@
<?php namespace Flarum\Api\Serializers;
use Flarum\Core\Models\Group;
class GroupSerializer extends BaseSerializer
{
/**
* The name to use for Flarum events.
* @var string
*/
protected static $eventName = 'Group';
/**
* The resource type.
* @var string
*/
protected $type = 'groups';
/**
* Serialize attributes of a Group model for JSON output.
*
* @param Group $group The Group model to serialize.
* @return array
*/
protected function attributes(Group $group)
{
$attributes = [
'id' => (int) $group->id,
'name' => $group->name
];
return $this->attributesEvent($group, $attributes);
}
/**
* Get the URL templates where this resource and its related resources can
* be accessed.
*
* @return array
*/
public function href()
{
return [
'groups' => $this->action('GroupsController@show', ['id' => '{groups.id}']),
'users' => $this->action('UsersController@indexForGroup', ['id' => '{groups.id}'])
];
}
}

View File

@@ -0,0 +1,90 @@
<?php namespace Flarum\Api\Serializers;
use Flarum\Core\Models\Post;
class PostBasicSerializer extends BaseSerializer
{
/**
* The name to use for Flarum events.
* @var string
*/
protected static $eventName = 'PostBasic';
/**
* The resource type.
* @var string
*/
protected $type = 'posts';
/**
* Default relations to link.
* @var array
*/
protected $link = ['discussion'];
/**
* Default relations to include.
* @var array
*/
protected $include = ['user'];
/**
* Serialize attributes of a Post model for JSON output.
*
* @param Post $post The Post model to serialize.
* @return array
*/
protected function attributes(Post $post)
{
$attributes = [
'id' => (int) $post->id,
'number' => (int) $post->number,
'time' => $post->time->toRFC3339String(),
'type' => $post->type
];
if ($post->type === 'comment') {
$attributes['content'] = str_limit($post->content, 200);
} else {
$attributes['content'] = json_encode($post->content);
}
return $this->attributesEvent($post, $attributes);
}
/**
* Get the URL templates where this resource and its related resources can
* be accessed.
*
* @return array
*/
public function href()
{
return [
'posts' => $this->action('PostsController@show', ['id' => '{posts.id}'])
];
}
/**
* Get a resource containing a post's user.
*
* @param Post $post
* @param array $relations
* @return Tobscure\JsonApi\Resource
*/
public function includeUser(Post $post, $relations)
{
return (new UserBasicSerializer($relations))->resource($post->user);
}
/**
* Get a resource containing a post's discussion ID.
*
* @param Post $post
* @return Tobscure\JsonApi\Resource
*/
public function linkDiscussion(Post $post)
{
return (new DiscussionBasicSerializer)->resource($post->discussion_id);
}
}

View File

@@ -0,0 +1,108 @@
<?php namespace Flarum\Api\Serializers;
use Flarum\Core\Models\Post;
use Flarum\Core\Models\User;
class PostSerializer extends PostBasicSerializer
{
/**
* Default relations to link.
* @var array
*/
protected $link = ['discussion'];
/**
* Default relations to include.
* @var array
*/
protected $include = ['user', 'editUser', 'hideUser'];
/**
* Serialize attributes of a Post model for JSON output.
*
* @param Post $post The Post model to serialize.
* @return array
*/
protected function attributes(Post $post)
{
$attributes = parent::attributes($post);
$user = static::$actor->getUser();
unset($attributes['content']);
$canEdit = $post->can($user, 'edit');
if ($post->type === 'comment') {
$attributes['contentHtml'] = $post->content_html;
if ($canEdit) {
$attributes['content'] = $post->content;
}
} else {
$attributes['content'] = json_encode($post->content);
}
if ($post->edit_time) {
$attributes['editTime'] = $post->edit_time->toRFC3339String();
}
if ($post->hide_time) {
$attributes['isHidden'] = true;
$attributes['hideTime'] = $post->hide_time->toRFC3339String();
}
$attributes += [
'canEdit' => $canEdit,
'canDelete' => $post->can($user, 'delete')
];
return $this->attributesEvent($post, $attributes);
}
/**
* Get a resource containing a post's user.
*
* @param Post $post
* @param array $relations
* @return Tobscure\JsonApi\Resource
*/
public function includeUser(Post $post, $relations = [])
{
return (new UserSerializer($relations))->resource($post->user);
}
/**
* Get a resource containing a post's discussion.
*
* @param Post $post
* @param array $relations
* @return Tobscure\JsonApi\Resource
*/
public function includeDiscussion(Post $post, $relations = [])
{
return (new DiscussionBasicSerializer($relations))->resource($post->discussion);
}
/**
* Get a resource containing a post's edit user.
*
* @param Post $post
* @param array $relations
* @return Tobscure\JsonApi\Resource
*/
public function includeEditUser(Post $post, $relations = [])
{
return (new UserBasicSerializer($relations))->resource($post->editUser);
}
/**
* Get a resource containing a post's hide user.
*
* @param Post $post
* @param array $relations
* @return Tobscure\JsonApi\Resource
*/
public function includeHideUser(Post $post, $relations = [])
{
return (new UserBasicSerializer($relations))->resource($post->hideUser);
}
}

View File

@@ -0,0 +1,50 @@
<?php namespace Flarum\Api\Serializers;
use Flarum\Core\Models\User;
class UserBasicSerializer extends BaseSerializer
{
/**
* The name to use for Flarum events.
* @var string
*/
protected static $eventName = 'UserBasic';
/**
* The resource type.
* @var string
*/
protected $type = 'users';
/**
* Serialize attributes of a User model for JSON output.
*
* @param User $user The User model to serialize.
* @return array
*/
protected function attributes(User $user)
{
$attributes = [
'id' => (int) $user->id,
'username' => $user->username,
'avatarUrl' => $user->avatar_url
];
return $this->attributesEvent($user, $attributes);
}
/**
* Get the URL templates where this resource and its related resources can
* be accessed.
*
* @return array
*/
protected function href()
{
$href = [
'users' => $this->action('UsersController@show', ['id' => '{users.id}'])
];
return $this->hrefEvent($href);
}
}

View File

@@ -0,0 +1,69 @@
<?php namespace Flarum\Api\Serializers;
use Flarum\Core\Models\User;
class UserSerializer extends UserBasicSerializer
{
/**
* The name to use for Flarum events.
* @var string
*/
protected static $eventName = 'User';
/**
* Default relations to include.
* @var array
*/
protected $include = ['groups'];
/**
* Serialize attributes of a User model for JSON output.
*
* @param User $user The User model to serialize.
* @return array
*/
protected function attributes(User $user)
{
$attributes = parent::attributes($user);
$actorUser = static::$actor->getUser();
$canEdit = $user->can($actorUser, 'edit');
$attributes += [
'joinTime' => $user->join_time ? $user->join_time->toRFC3339String() : null,
'lastSeenTime' => $user->last_seen_time ? $user->last_seen_time->toRFC3339String() : null,
'discussionsCount' => (int) $user->discussions_count,
'postsCount' => (int) $user->posts_count,
'canEdit' => $canEdit,
'canDelete' => $user->can($actorUser, 'delete'),
];
if ($canEdit) {
$attributes += [
'isActivated' => $user->is_activated,
'email' => $user->email,
'isConfirmed' => $user->is_confirmed
];
}
if ($user->id === $actorUser->id) {
$attributes += [
'readTime' => $user->read_time ? $user->read_time->toRFC3339String() : null,
];
}
return $this->attributesEvent($user, $attributes);
}
/**
* Get a collection containing a user's groups.
*
* @param User $user
* @param array $relations
* @return Tobscure\JsonApi\Collection
*/
protected function includeGroups(User $user, $relations)
{
return (new GroupSerializer($relations))->collection($user->groups);
}
}

182
src/Api/routes.php Normal file
View File

@@ -0,0 +1,182 @@
<?php
$action = function ($class) {
return function () use ($class) {
$action = $this->app->make($class);
$request = $this->app['request']->instance();
$parameters = $this->app['router']->current()->parameters();
return $action->handle($request, $parameters);
};
};
Route::group(['prefix' => 'api', 'middleware' => 'Flarum\Api\Middleware\LoginWithHeaderMiddleware'], function () use ($action) {
Route::post('token', [
'as' => 'flarum.api.token',
'uses' => $action('Flarum\Api\Actions\TokenAction')
]);
/*
|--------------------------------------------------------------------------
| Users
|--------------------------------------------------------------------------
*/
// List users
Route::get('users', [
'as' => 'flarum.api.users.index',
'uses' => $action('Flarum\Api\Actions\Users\IndexAction')
]);
// Register a user
Route::post('users', [
'as' => 'flarum.api.users.create',
'uses' => $action('Flarum\Api\Actions\Users\CreateAction')
]);
// Get a single user
Route::get('users/{id}', [
'as' => 'flarum.api.users.show',
'uses' => $action('Flarum\Api\Actions\Users\ShowAction')
]);
// Edit a user
Route::put('users/{id}', [
'as' => 'flarum.api.users.update',
'uses' => $action('Flarum\Api\Actions\Users\UpdateAction')
]);
// Delete a user
Route::delete('users/{id}', [
'as' => 'flarum.api.users.delete',
'uses' => $action('Flarum\Api\Actions\Users\DeleteAction')
]);
/*
|--------------------------------------------------------------------------
| Activity
|--------------------------------------------------------------------------
*/
// List activity
Route::get('activity', [
'as' => 'flarum.api.activity.index',
'uses' => $action('Flarum\Api\Actions\Activity\IndexAction')
]);
// List notifications for the current user
Route::get('notifications', [
'as' => 'flarum.api.notifications.index',
'uses' => $action('Flarum\Api\Actions\Notifications\IndexAction')
]);
/*
|--------------------------------------------------------------------------
| Discussions
|--------------------------------------------------------------------------
*/
// List discussions
Route::get('discussions', [
'as' => 'flarum.api.discussions.index',
'uses' => $action('Flarum\Api\Actions\Discussions\IndexAction')
]);
// Create a discussion
Route::post('discussions', [
'as' => 'flarum.api.discussions.create',
'uses' => $action('Flarum\Api\Actions\Discussions\CreateAction')
]);
// Show a single discussion
Route::get('discussions/{id}', [
'as' => 'flarum.api.discussions.show',
'uses' => $action('Flarum\Api\Actions\Discussions\ShowAction')
]);
// Edit a discussion
Route::put('discussions/{id}', [
'as' => 'flarum.api.discussions.update',
'uses' => $action('Flarum\Api\Actions\Discussions\UpdateAction')
]);
// Delete a discussion
Route::delete('discussions/{id}', [
'as' => 'flarum.api.discussions.delete',
'uses' => $action('Flarum\Api\Actions\Discussions\DeleteAction')
]);
/*
|--------------------------------------------------------------------------
| Posts
|--------------------------------------------------------------------------
*/
// List posts, usually for a discussion
Route::get('posts', [
'as' => 'flarum.api.posts.index',
'uses' => $action('Flarum\Api\Actions\Posts\IndexAction')
]);
// Create a post
// @todo consider 'discussions/{id}/links/posts'?
Route::post('posts', [
'as' => 'flarum.api.posts.create',
'uses' => $action('Flarum\Api\Actions\Posts\CreateAction')
]);
// Show a single or multiple posts by ID
Route::get('posts/{id}', [
'as' => 'flarum.api.posts.show',
'uses' => $action('Flarum\Api\Actions\Posts\ShowAction')
]);
// Edit a post
Route::put('posts/{id}', [
'as' => 'flarum.api.posts.update',
'uses' => $action('Flarum\Api\Actions\Posts\UpdateAction')
]);
// Delete a post
Route::delete('posts/{id}', [
'as' => 'flarum.api.posts.delete',
'uses' => $action('Flarum\Api\Actions\Posts\DeleteAction')
]);
/*
|--------------------------------------------------------------------------
| Groups
|--------------------------------------------------------------------------
*/
// List groups
Route::get('groups', [
'as' => 'flarum.api.groups.index',
'uses' => $action('Flarum\Api\Actions\Groups\IndexAction')
]);
// Create a group
Route::post('groups', [
'as' => 'flarum.api.groups.create',
'uses' => $action('Flarum\Api\Actions\Groups\CreateAction')
]);
// Show a single group
Route::get('groups/{id}', [
'as' => 'flarum.api.groups.show',
'uses' => $action('Flarum\Api\Actions\Groups\ShowAction')
]);
// Edit a group
Route::put('groups/{id}', [
'as' => 'flarum.api.groups.update',
'uses' => $action('Flarum\Api\Actions\Groups\UpdateAction')
]);
// Delete a group
Route::delete('groups/{id}', [
'as' => 'flarum.api.groups.delete',
'uses' => $action('Flarum\Api\Actions\Groups\DeleteAction')
]);
});