1
0
mirror of https://github.com/flarum/core.git synced 2025-10-18 10:16:09 +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);
}
}