Introducing API

Right now there's a lot of messy code in controllers. Furthermore, there
is no way to interact with szurubooru via vanilla HTTP, since API is
next to non-existent. So, basing upon my experiences from another
project, I plan to:

- Create actual API. It is going to consist of well-defined "jobs" that
  do things currently done by controllers. Benefits of such approach are
  as follows:
  - defining them in their own classes allows to clean up code a lot,
  - it allows to abstract from input method (POST data, part of URL,
	whatever), and leave processing of these to controllers,
  - it allows to make proxy controller, whose purpose would be to let
	users interact with API (jobs) directly in well-documented and
	consistent way.
- Make controllers responsible only for mediating between views and API.
  Behavior of these may remain inconsistent, since views they're talking
  to are also messy to begin with. Such controllers might be removed
  altogether in the future in favor of making views talk to API directly
  through previously mentioned ApiController.
- Organize all sorts of privilege checking and possibly other stuff into
  methods within jobs.
- Actually distinguish POST from GET requests.
- Leave POST-only controller methods as Actions, but rename GET-only
  methods to Views. Example: editAction for editing comments, but
  listView for showing comment list. The choice of these suffixes might
  be subject to changes in future.
- Get rid of ?json and $context->transport. They now look like disease
  to me.

This commit introduces job system and converts CommentController to use
the new API.
This commit is contained in:
Marcin Kurczewski 2014-05-01 16:25:10 +02:00
parent feec48ed83
commit 902aed7278
12 changed files with 203 additions and 67 deletions

View file

@ -35,7 +35,7 @@ $context->simpleActionName = null;
TextCaseConverter::SPINAL_CASE);
$context->simpleActionName = TextCaseConverter::convert(
str_replace('Action', '', $methodName),
preg_replace('/Action|View/', '', $methodName),
TextCaseConverter::CAMEL_CASE,
TextCaseConverter::SPINAL_CASE);
@ -45,6 +45,13 @@ $context->simpleActionName = null;
$context->simpleActionName);
});
\Chibi\Router::register(['CommentController', 'listView'], 'GET', '/comments');
\Chibi\Router::register(['CommentController', 'listView'], 'GET', '/comments/{page}', ['page' => '\d+']);
\Chibi\Router::register(['CommentController', 'addAction'], 'POST', '/comment/add');
\Chibi\Router::register(['CommentController', 'deleteAction'], 'POST', '/comment/{id}/delete', ['id' => '\d+']);
\Chibi\Router::register(['CommentController', 'editView'], 'GET', '/comment/{id}/edit', ['id' => '\d+']);
\Chibi\Router::register(['CommentController', 'editAction'], 'POST', '/comment/{id}/edit', ['id' => '\d+']);
foreach (['GET', 'POST'] as $method)
{
\Chibi\Router::register(['IndexController', 'indexAction'], $method, '');
@ -59,11 +66,6 @@ foreach (['GET', 'POST'] as $method)
\Chibi\Router::register(['AuthController', 'logoutAction'], $method, '/auth/logout');
\Chibi\Router::register(['AuthController', 'loginAction'], 'POST', '/auth/login');
\Chibi\Router::register(['AuthController', 'logoutAction'], 'POST', '/auth/logout');
\Chibi\Router::register(['CommentController', 'listAction'], $method, '/comments');
\Chibi\Router::register(['CommentController', 'listAction'], $method, '/comments/{page}', ['page' => '\d+']);
\Chibi\Router::register(['CommentController', 'addAction'], $method, '/post/{postId}/add-comment', ['postId' => '\d+']);
\Chibi\Router::register(['CommentController', 'deleteAction'], $method, '/comment/{id}/delete', ['id' => '\d+']);
\Chibi\Router::register(['CommentController', 'editAction'], $method, '/comment/{id}/edit', ['id' => '\d+']);
$postValidation =
[

33
src/Api.php Normal file
View file

@ -0,0 +1,33 @@
<?php
class Api
{
public static function run($job, $jobArgs)
{
$user = Auth::getCurrentUser();
return \Chibi\Database::transaction(function() use ($job, $jobArgs)
{
if ($job->requiresAuthentication())
Access::assertAuthentication();
if ($job->requiresConfirmedEmail())
Access::assertEmailConfirmation();
return $job->execute($jobArgs);
});
}
public static function runMultiple($jobs)
{
$statuses = [];
\Chibi\Database::transaction(function() use ($jobs, &$statuses)
{
foreach ($jobs as $jobItem)
{
list ($job, $jobArgs) = $jobItem;
$statuses []= self::run($job, $jobArgs);
}
});
return $statuses;
}
}

View file

@ -85,6 +85,7 @@ class Auth
private static function getAnonymousUser()
{
$dummy = UserModel::spawn();
$dummy->id = null;
$dummy->name = UserModel::getAnonymousName();
$dummy->accessRank = AccessRank::Anonymous;
return $dummy;

View file

@ -1,7 +1,7 @@
<?php
class CommentController
{
public function listAction($page)
public function listView($page)
{
Access::assert(Privilege::ListComments);
@ -30,64 +30,46 @@ class CommentController
$context->transport->paginator->params = func_get_args();
}
public function addAction($postId)
public function previewAction()
{
$context = getContext();
Access::assert(Privilege::AddComment);
if (getConfig()->registration->needEmailForCommenting)
Access::assertEmailConfirmation();
$comment = Api::run(
new PreviewCommentJob(),
[
'text' => InputHelper::get('text')
]);
$post = PostModel::findById($postId);
$context->transport->post = $post;
if (!InputHelper::get('submit'))
return;
$text = InputHelper::get('text');
$text = CommentModel::validateText($text);
$comment = CommentModel::spawn();
$comment->setPost($post);
if (Auth::isLoggedIn())
$comment->setCommenter(Auth::getCurrentUser());
else
$comment->setCommenter(null);
$comment->commentDate = time();
$comment->text = $text;
if (InputHelper::get('sender') != 'preview')
{
CommentModel::save($comment);
LogHelper::log('{user} commented on {post}', ['post' => TextHelper::reprPost($post->id)]);
getContext()->transport->textPreview = $comment->getText();
}
$context->transport->textPreview = $comment->getText();
public function addAction()
{
if (InputHelper::get('sender') == 'preview')
return $this->previewAction();
$comment = Api::run(
new AddCommentJob(),
[
'post-id' => InputHelper::get('post-id'),
'text' => InputHelper::get('text')
]);
}
public function editView($id)
{
getContext()->transport->comment = CommentModel::findById($id);
}
public function editAction($id)
{
$context = getContext();
$comment = CommentModel::findById($id);
$context->transport->comment = $comment;
if (InputHelper::get('sender') == 'preview')
return $this->previewAction();
Access::assert(
Privilege::EditComment,
Access::getIdentity($comment->getCommenter()));
if (!InputHelper::get('submit'))
return;
$text = InputHelper::get('text');
$text = CommentModel::validateText($text);
$comment->text = $text;
if (InputHelper::get('sender') != 'preview')
{
CommentModel::save($comment);
LogHelper::log('{user} edited comment in {post}', [
'post' => TextHelper::reprPost($comment->getPost())]);
}
$context->transport->textPreview = $comment->getText();
$comment = Api::run(
new EditCommentJob(),
[
'comment-id' => $id,
'text' => InputHelper::get('text')
]);
}
public function deleteAction($id)

View file

@ -71,11 +71,17 @@ class LogEvent
$this->text = $text;
$this->ip = isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '0.0.0.0';
//todo: deprecated
if (!isset($tokens['anon']))
$tokens['anon'] = UserModel::getAnonymousName();
if (!isset($tokens['user']))
{
if (Auth::isLoggedIn())
$tokens['user'] = TextHelper::reprUser(Auth::getCurrentUser()->name);
else
$tokens['user'] = $tokens['anon'];
}
$this->tokens = $tokens;
}

9
src/Jobs/AbstractJob.php Normal file
View file

@ -0,0 +1,9 @@
<?php
abstract class AbstractJob
{
public abstract function execute($arguments);
public abstract function requiresAuthentication();
public abstract function requiresConfirmedEmail();
public abstract function requiresPrivilege();
}

View file

@ -0,0 +1,38 @@
<?php
class AddCommentJob extends AbstractJob
{
public function execute($arguments)
{
$post = PostModel::findById($arguments['post-id']);
$user = Auth::getCurrentUser();
$text = CommentModel::validateText($arguments['text']);
$comment = CommentModel::spawn();
$comment->setCommenter($user);
$comment->setPost($post);
$comment->commentDate = time();
$comment->text = $text;
CommentModel::save($comment);
LogHelper::log('{user} commented on {post}', [
'user' => TextHelper::reprUser(Auth::getCurrentUser()),
'post' => TextHelper::reprPost($comment->getPost()->id)]);
return $comment;
}
public function requiresPrivilege()
{
return Privilege::AddComment;
}
public function requiresAuthentication()
{
return true;
}
public function requiresConfirmedEmail()
{
return getConfig()->registration->needEmailForCommenting;
}
}

View file

@ -0,0 +1,35 @@
<?php
class EditCommentJob extends AbstractJob
{
public function execute($arguments)
{
$user = Auth::getCurrentUser();
$comment = CommentModel::findById($arguments['comment-id']);
$text = CommentModel::validateText($arguments['text']);
$comment->commentDate = time();
$comment->text = $text;
CommentModel::save($comment);
LogHelper::log('{user} edited comment in {post}', [
'user' => TextHelper::reprUser(Auth::getCurrentUser()),
'post' => TextHelper::reprPost($comment->getPost())]);
return $comment;
}
public function requiresPrivilege()
{
return Privilege::EditComment;
}
public function requiresAuthentication()
{
return true;
}
public function requiresConfirmedEmail()
{
return getConfig()->registration->needEmailForCommenting;
}
}

View file

@ -0,0 +1,31 @@
<?php
class PreviewCommentJob extends AbstractJob
{
public function execute($arguments)
{
$user = Auth::getCurrentUser();
$text = CommentModel::validateText($arguments['text']);
$comment = CommentModel::spawn();
$comment->setCommenter($user);
$comment->commentDate = time();
$comment->text = $text;
return $comment;
}
public function requiresPrivilege()
{
return Privilege::AddComment;
}
public function requiresAuthentication()
{
return true;
}
public function requiresConfirmedEmail()
{
return getConfig()->registration->needEmailForCommenting;
}
}

View file

@ -5,9 +5,7 @@ Assets::addScript('comment-edit.js');
<form
method="post"
action="<?= \Chibi\Router::linkTo(
['CommentController', 'addAction'],
['postId' => $this->context->transport->post->id]) ?>"
action="<?= \Chibi\Router::linkTo(['CommentController', 'addAction']) ?>"
class="add-comment">
<h1>add comment</h1>
@ -18,6 +16,7 @@ Assets::addScript('comment-edit.js');
<div class="input-wrapper"><textarea name="text" cols="50" rows="3"></textarea></div>
</div>
<input type="hidden" name="post-id" value="<?= $this->context->transport->post->id ?>">
<input type="hidden" name="submit" value="1"/>
<div class="form-row">

View file

@ -41,15 +41,15 @@ Assets::addScript('comment-edit.js');
Access::getIdentity($commenter))): ?>
<span class="edit">
<a href="<?= \Chibi\Router::linkTo(
['CommentController', 'editAction'],
['CommentController', 'editView'],
['id' => $this->context->comment->id]) ?>">
edit
</a>
</span>
<?php endif ?>
<?php if (
Access::check(Privilege::DeleteComment,
<?php if (Access::check(
Privilege::DeleteComment,
Access::getIdentity($commenter))): ?>
<span class="delete">
<a class="simple-action confirmable"

View file

@ -38,7 +38,7 @@
{
$registerNavItem(
'Comments',
\Chibi\Router::linkTo(['CommentController', 'listAction']),
\Chibi\Router::linkTo(['CommentController', 'listView']),
$activeController == 'comment');
}