Finish client action refactoring. closes flarum/core#126

This commit is contained in:
Toby Zerner 2015-07-07 19:23:13 +09:30
parent 8a54b362c7
commit 38c2ff0306
9 changed files with 518 additions and 170 deletions

View File

@ -1,5 +1,6 @@
<?php namespace Flarum\Core\Users; <?php namespace Flarum\Core\Users;
use Flarum\Core;
use Flarum\Core\Groups\Group; use Flarum\Core\Groups\Group;
use Flarum\Core\Model; use Flarum\Core\Model;
use Flarum\Core\Notifications\Notification; use Flarum\Core\Notifications\Notification;
@ -282,7 +283,21 @@ class User extends Model
*/ */
public function getAvatarUrlAttribute() public function getAvatarUrlAttribute()
{ {
return $this->avatar_path ? app('Flarum\Http\UrlGeneratorInterface')->toAsset('assets/avatars/'.$this->avatar_path) : null; $urlGenerator = app('Flarum\Http\UrlGeneratorInterface');
return $this->avatar_path ? $urlGenerator->toAsset('assets/avatars/'.$this->avatar_path) : null;
}
/**
* Get the user's locale, falling back to the forum's default if they
* haven't set one.
*
* @param string $value
* @return string
*/
public function getLocaleAttribute($value)
{
return $value ?: Core::config('locale', 'en');
} }
/** /**

View File

@ -1,134 +1,16 @@
<?php namespace Flarum\Forum\Actions; <?php namespace Flarum\Forum\Actions;
use Flarum\Api\Client; use Flarum\Support\ClientAction as BaseClientAction;
use Flarum\Assets\AssetManager;
use Flarum\Assets\JsCompiler;
use Flarum\Assets\LessCompiler;
use Flarum\Core;
use Flarum\Core\Users\User;
use Flarum\Forum\Events\RenderView;
use Flarum\Locale\JsCompiler as LocaleJsCompiler;
use Flarum\Locale\LocaleManager;
use Flarum\Support\ClientView;
use Flarum\Support\HtmlAction;
use Illuminate\Database\ConnectionInterface;
use Psr\Http\Message\ServerRequestInterface as Request;
abstract class ClientAction extends HtmlAction abstract class ClientAction extends BaseClientAction
{ {
/** /**
* @var Client * {@inheritdoc}
*/ */
protected $apiClient; protected $clientName = 'forum';
protected $locales;
/** /**
* @param Client $apiClient * {@inheritdoc}
* @param LocaleManager $locales
*/ */
public function __construct(Client $apiClient, LocaleManager $locales) protected $layout = 'flarum.forum::forum';
{
$this->apiClient = $apiClient;
$this->locales = $locales;
}
/**
* @param Request $request
* @param array $routeParams
* @return \Flarum\Support\ClientView
*/
public function render(Request $request, array $routeParams = [])
{
$actor = app('flarum.actor');
$assets = $this->getAssets();
$locale = $this->getLocaleCompiler($actor);
$layout = 'flarum.forum::forum';
$view = new ClientView(
$request,
$actor,
$this->apiClient,
$layout,
$assets,
$locale
);
return $view;
}
protected function getAssets()
{
$public = $this->getAssetDirectory();
$assets = new AssetManager(
new JsCompiler($public, 'forum.js'),
new LessCompiler($public, 'forum.css')
);
$root = __DIR__.'/../../..';
$assets->addFile($root.'/js/forum/dist/app.js');
$assets->addFile($root.'/less/forum/app.less');
foreach ($this->getLessVariables() as $name => $value) {
$assets->addLess("@$name: $value;");
}
$assets->addLess(Core::config('custom_less'));
return $assets;
}
protected function getLessVariables()
{
return [
'fl-primary-color' => Core::config('theme_primary_color', '#000'),
'fl-secondary-color' => Core::config('theme_secondary_color', '#000'),
'fl-dark-mode' => Core::config('theme_dark_mode') ? 'true' : 'false',
'fl-colored-header' => Core::config('theme_colored_header') ? 'true' : 'false'
];
}
protected function getLocaleCompiler(User $actor)
{
$locale = $actor->locale ?: Core::config('locale', 'en');
// $translations = $this->locales->getTranslations($locale);
$jsFiles = $this->locales->getJsFiles($locale);
$compiler = new LocaleJsCompiler($this->getAssetDirectory(), 'locale-'.$locale.'.js');
// $compiler->setTranslations(static::filterTranslations($translations));
array_walk($jsFiles, [$compiler, 'addFile']);
return $compiler;
}
protected function getAssetDirectory()
{
return public_path().'/assets';
}
/**
* @param $translations
* @return array
*/
// protected static function filterTranslations($translations)
// {
// $filtered = [];
//
// foreach (static::$translations as $key) {
// $parts = explode('.', $key);
// $level = &$filtered;
//
// foreach ($parts as $part) {
// $level = &$level[$part];
// }
//
// $level = array_get($translations, $key);
// }
//
// return $filtered;
// }
} }

View File

@ -4,18 +4,21 @@ use Psr\Http\Message\ServerRequestInterface as Request;
class DiscussionAction extends ClientAction class DiscussionAction extends ClientAction
{ {
/**
* {@inheritdoc}
*/
public function render(Request $request, array $routeParams = []) public function render(Request $request, array $routeParams = [])
{ {
$view = parent::render($request, $routeParams); $view = parent::render($request, $routeParams);
$actor = app('flarum.actor');
$action = 'Flarum\Api\Actions\Discussions\ShowAction';
$params = [ $params = [
'id' => $routeParams['id'], 'id' => array_get($routeParams, 'id'),
'page.near' => $routeParams['near'] 'page.near' => array_get($routeParams, 'near')
]; ];
$document = $this->apiClient->send($actor, $action, $params)->getBody(); // FIXME: make sure this is extensible. 404s, pagination.
$document = $this->preload($params);
$view->setTitle($document->data->attributes->title); $view->setTitle($document->data->attributes->title);
$view->setDocument($document); $view->setDocument($document);
@ -23,4 +26,18 @@ class DiscussionAction extends ClientAction
return $view; return $view;
} }
/**
* Get the result of an API request to show a discussion.
*
* @param array $params
* @return object
*/
protected function preload(array $params)
{
$actor = app('flarum.actor');
$action = 'Flarum\Api\Actions\Discussions\ShowAction';
return $this->apiClient->send($actor, $action, $params)->getBody();
}
} }

View File

@ -1,22 +1,23 @@
<?php namespace Flarum\Forum\Actions; <?php namespace Flarum\Forum\Actions;
use Flarum\Api\Client;
use Flarum\Assets\AssetManager;
use Flarum\Assets\JsCompiler;
use Flarum\Assets\LessCompiler;
use Flarum\Core;
use Flarum\Forum\Events\RenderView;
use Flarum\Locale\JsCompiler as LocaleJsCompiler;
use Flarum\Support\HtmlAction;
use Illuminate\Database\ConnectionInterface;
use Psr\Http\Message\ServerRequestInterface as Request; use Psr\Http\Message\ServerRequestInterface as Request;
class IndexAction extends ClientAction class IndexAction extends ClientAction
{ {
/** /**
* @param Request $request * A map of sort query param values to their API sort param.
* @param array $routeParams *
* @return \Illuminate\Contracts\View\View * @var array
*/
protected $sortMap = [
'recent' => '-lastTime',
'replies' => '-commentsCount',
'newest' => '-startTime',
'oldest' => '+startTime'
];
/**
* {@inheritdoc}
*/ */
public function render(Request $request, array $routeParams = []) public function render(Request $request, array $routeParams = [])
{ {
@ -24,18 +25,35 @@ class IndexAction extends ClientAction
$queryParams = $request->getQueryParams(); $queryParams = $request->getQueryParams();
// Only preload data if we're viewing the default index with no filters, $sort = array_pull($queryParams, 'sort');
// otherwise we have to do all kinds of crazy stuff $q = array_pull($queryParams, 'q');
if (! count($queryParams) && $request->getUri()->getPath() === '/') {
$actor = app('flarum.actor');
$action = 'Flarum\Api\Actions\Discussions\IndexAction';
$document = $this->apiClient->send($actor, $action)->getBody(); $params = [
'sort' => $sort ? $this->sortMap[$sort] : '',
'q' => $q
];
$view->setDocument($document); // FIXME: make sure this is extensible. Support pagination.
$view->setContent(app('view')->make('flarum.forum::index', compact('document')));
} $document = $this->preload($params);
$view->setDocument($document);
$view->setContent(app('view')->make('flarum.forum::index', compact('document')));
return $view; return $view;
} }
/**
* Get the result of an API request to list discussions.
*
* @param array $params
* @return object
*/
protected function preload(array $params)
{
$actor = app('flarum.actor');
$action = 'Flarum\Api\Actions\Discussions\IndexAction';
return $this->apiClient->send($actor, $action, $params)->getBody();
}
} }

View File

@ -29,6 +29,8 @@ class LocaleServiceProvider extends ServiceProvider
public function register() public function register()
{ {
$this->app->singleton('flarum.localeManager', 'Flarum\Locale\LocaleManager'); $this->app->singleton('Flarum\Locale\LocaleManager');
$this->app->alias('Flarum\Locale\LocaleManager', 'flarum.localeManager');
} }
} }

View File

@ -0,0 +1,231 @@
<?php namespace Flarum\Support;
use Flarum\Api\Client;
use Flarum\Assets\AssetManager;
use Flarum\Assets\JsCompiler;
use Flarum\Assets\LessCompiler;
use Flarum\Core;
use Flarum\Core\Users\User;
use Flarum\Locale\JsCompiler as LocaleJsCompiler;
use Flarum\Locale\LocaleManager;
use Psr\Http\Message\ServerRequestInterface as Request;
/**
* This action sets up a ClientView, and preloads it with the assets necessary
* to boot a Flarum client.
*
* Subclasses should set a $clientName, $layout, and $translationKeys. The
* client name will be used to locate the client assets (or alternatively,
* subclasses can overwrite the addAssets method), and set up asset compilers
* which write to the assets directory. Configured LESS customizations will be
* appended.
*
* A locale compiler is set up for the actor's locale, including the
* translations specified in $translationKeys. Additionally, an event is fired
* before the ClientView is returned, giving extensions an opportunity to add
* assets, translations, or alter the view.
*/
abstract class ClientAction extends HtmlAction
{
/**
* The name of the client. This is used to locate assets within the js/
* and less/ directories. It is also used as the filename of the compiled
* asset files.
*
* @var string
*/
protected $clientName;
/**
* The name of the view to include as the page layout.
*
* @var string
*/
protected $layout;
/**
* The keys of the translations that should be included in the compiled
* locale file.
*
* @var array
*/
protected $translationKeys = [];
/**
* @var Client
*/
protected $apiClient;
/**
* @var LocaleManager
*/
protected $locales;
/**
* @param Client $apiClient
* @param LocaleManager $locales
*/
public function __construct(Client $apiClient, LocaleManager $locales)
{
$this->apiClient = $apiClient;
$this->locales = $locales;
}
/**
* {@inheritdoc}
*
* @return ClientView
*/
public function render(Request $request, array $routeParams = [])
{
$actor = app('flarum.actor');
$assets = $this->getAssets();
$locale = $this->getLocaleCompiler($actor);
$view = new ClientView(
$this->apiClient,
$request,
$actor,
$assets,
$locale,
$this->layout
);
// Now that we've set up the ClientView instance, we can fire an event
// to give extensions the opportunity to add their own assets and
// translations. We will pass an array to the event which specifies
// which translations should be included in the locale file. Afterwards,
// we will filter all of the translations for the actor's locale and
// compile only the ones we need.
$translations = $this->locales->getTranslations($actor->locale);
$keys = $this->translationKeys;
// TODO: event($this, $view, $keys)
$translations = $this->filterTranslations($translations, $keys);
$locale->setTranslations($translations);
return $view;
}
/**
* Set up the asset manager, preloaded with a JavaScript compiler and a LESS
* compiler. Automatically add the files necessary to boot a Flarum client,
* as well as any configured LESS customizations.
*
* @return AssetManager
*/
protected function getAssets()
{
$public = $this->getAssetDirectory();
$assets = new AssetManager(
new JsCompiler($public, "$this->clientName.js"),
new LessCompiler($public, "$this->clientName.css")
);
$this->addAssets($assets);
$this->addCustomizations($assets);
return $assets;
}
/**
* Add the assets necessary to boot a Flarum client, found within the
* directory specified by the $clientName property.
*
* @param AssetManager $assets
*/
protected function addAssets(AssetManager $assets)
{
$root = __DIR__.'/../..';
$assets->addFile("$root/js/$this->clientName/dist/app.js");
$assets->addFile("$root/less/$this->clientName/app.less");
}
/**
* Add any configured JS/LESS customizations to the asset manager.
*
* @param AssetManager $assets
*/
protected function addCustomizations(AssetManager $assets)
{
foreach ($this->getLessVariables() as $name => $value) {
$assets->addLess("@$name: $value;");
}
$assets->addLess(Core::config('custom_less'));
}
/**
* Get the values of any LESS variables to compile into the CSS, based on
* the forum's configuration.
*
* @return array
*/
protected function getLessVariables()
{
return [
'fl-primary-color' => Core::config('theme_primary_color', '#000'),
'fl-secondary-color' => Core::config('theme_secondary_color', '#000'),
'fl-dark-mode' => Core::config('theme_dark_mode') ? 'true' : 'false',
'fl-colored-header' => Core::config('theme_colored_header') ? 'true' : 'false'
];
}
/**
* Set up the locale compiler for the given user's locale.
*
* @param User $actor
* @return LocaleJsCompiler
*/
protected function getLocaleCompiler(User $actor)
{
$locale = $actor->locale;
$compiler = new LocaleJsCompiler($this->getAssetDirectory(), "$this->clientName-$locale.js");
foreach ($this->locales->getJsFiles($locale) as $file) {
$compiler->addFile($file);
}
return $compiler;
}
/**
* Get the path to the directory where assets should be written.
*
* @return string
*/
protected function getAssetDirectory()
{
return public_path().'/assets';
}
/**
* Take a selection of keys from a collection of translations.
*
* @param array $translations
* @param array $keys
* @return array
*/
protected function filterTranslations(array $translations, array $keys)
{
$filtered = [];
foreach ($keys as $key) {
$parts = explode('.', $key);
$level = &$filtered;
foreach ($parts as $part) {
$level = &$level[$part];
}
$level = array_get($translations, $key);
}
return $filtered;
}
}

View File

@ -3,61 +3,174 @@
use Flarum\Api\Client; use Flarum\Api\Client;
use Flarum\Assets\AssetManager; use Flarum\Assets\AssetManager;
use Flarum\Core\Users\User; use Flarum\Core\Users\User;
use Illuminate\Contracts\Support\Renderable;
use Psr\Http\Message\ServerRequestInterface as Request; use Psr\Http\Message\ServerRequestInterface as Request;
use Flarum\Locale\JsCompiler; use Flarum\Locale\JsCompiler;
class ClientView /**
* This class represents a view which boots up Flarum's client.
*/
class ClientView implements Renderable
{ {
/**
* The user who is using the client.
*
* @var User
*/
protected $actor; protected $actor;
protected $apiClient; /**
* The title of the document, displayed in the <title> tag.
*
* @var null|string
*/
protected $title; protected $title;
/**
* An API response that should be preloaded into the page.
*
* @var null|array|object
*/
protected $document; protected $document;
/**
* The SEO content of the page, displayed in <noscript> tags.
*
* @var string
*/
protected $content; protected $content;
protected $request; /**
* The name of the client layout view to display.
*
* @var string
*/
protected $layout; protected $layout;
/**
* An array of strings to append to the page's <head>.
*
* @var array
*/
protected $headStrings = [];
/**
* An array of strings to prepend before the page's </body>.
*
* @var array
*/
protected $footStrings = [];
/**
* @var Client
*/
protected $apiClient;
/**
* @var Request
*/
protected $request;
/**
* @var AssetManager
*/
protected $assets;
/**
* @var JsCompiler
*/
protected $locale;
/**
* @param Client $apiClient
* @param Request $request
* @param User $actor
* @param AssetManager $assets
* @param JsCompiler $locale
* @param string $layout
*/
public function __construct( public function __construct(
Client $apiClient,
Request $request, Request $request,
User $actor, User $actor,
Client $apiClient,
$layout,
AssetManager $assets, AssetManager $assets,
JsCompiler $locale JsCompiler $locale,
$layout
) { ) {
$this->apiClient = $apiClient;
$this->request = $request; $this->request = $request;
$this->actor = $actor; $this->actor = $actor;
$this->apiClient = $apiClient;
$this->layout = $layout;
$this->assets = $assets; $this->assets = $assets;
$this->locale = $locale; $this->locale = $locale;
$this->layout = $layout;
} }
public function setActor(User $actor) /**
{ * The title of the document, to be displayed in the <title> tag.
$this->actor = $actor; *
} * @param null|string $title
*/
public function setTitle($title) public function setTitle($title)
{ {
$this->title = $title; $this->title = $title;
} }
/**
* Set an API response to be preloaded into the page. This should be a
* JSON-API document.
*
* @param null|array|object $document
*/
public function setDocument($document) public function setDocument($document)
{ {
$this->document = $document; $this->document = $document;
} }
/**
* Set the SEO content of the page, to be displayed in <noscript> tags.
*
* @param null|string $content
*/
public function setContent($content) public function setContent($content)
{ {
$this->content = $content; $this->content = $content;
} }
/**
* Add a string to be appended to the page's <head>.
*
* @param string $string
*/
public function addHeadString($string)
{
$this->headStrings[] = $string;
}
/**
* Add a string to be prepended before the page's </body>.
*
* @param string $string
*/
public function addFootString($string)
{
$this->footStrings[] = $string;
}
/**
* Get the view's asset manager.
*
* @return AssetManager
*/
public function getAssets()
{
return $this->assets;
}
/**
* Get the string contents of the view.
*
* @return string
*/
public function render() public function render()
{ {
$view = app('view')->file(__DIR__.'/../../views/app.blade.php'); $view = app('view')->file(__DIR__.'/../../views/app.blade.php');
@ -81,25 +194,43 @@ class ClientView
$view->styles = [$this->assets->getCssFile()]; $view->styles = [$this->assets->getCssFile()];
$view->scripts = [$this->assets->getJsFile(), $this->locale->getFile()]; $view->scripts = [$this->assets->getJsFile(), $this->locale->getFile()];
$view->head = implode("\n", $this->headStrings);
$view->foot = implode("\n", $this->footStrings);
return $view->render(); return $view->render();
} }
/**
* Get the string contents of the view.
*
* @return string
*/
public function __toString() public function __toString()
{ {
return $this->render(); return $this->render();
} }
/**
* Get the result of an API request to show the forum.
*
* @return object
*/
protected function getForumDocument() protected function getForumDocument()
{ {
return $this->apiClient->send($this->actor, 'Flarum\Api\Actions\Forum\ShowAction')->getBody(); return $this->apiClient->send($this->actor, 'Flarum\Api\Actions\Forum\ShowAction')->getBody();
} }
/**
* Get the result of an API request to show the current user.
*
* @return object
*/
protected function getUserDocument() protected function getUserDocument()
{ {
// TODO: calling on the API here results in an extra query to get // TODO: calling on the API here results in an extra query to get
// the user + their groups, when we already have this information on // the user + their groups, when we already have this information on
// $this->actor. Can we simply run the CurrentUserSerializer // $this->actor. Can we simply run the CurrentUserSerializer
// manually? // manually? Or can we somehow inject this data into the ShowAction?
$document = $this->apiClient->send( $document = $this->apiClient->send(
$this->actor, $this->actor,
'Flarum\Api\Actions\Users\ShowAction', 'Flarum\Api\Actions\Users\ShowAction',
@ -109,6 +240,13 @@ class ClientView
return $document; return $document;
} }
/**
* Get an array of data by merging the 'data' and 'included' keys of a
* JSON-API document.
*
* @param object $document
* @return array
*/
protected function getDataFromDocument($document) protected function getDataFromDocument($document)
{ {
$data[] = $document->data; $data[] = $document->data;
@ -120,11 +258,16 @@ class ClientView
return $data; return $data;
} }
/**
* Get information about the current session.
*
* @return array
*/
protected function getSession() protected function getSession()
{ {
return [ return [
'userId' => $this->actor->id, 'userId' => $this->actor->id,
'token' => $this->request->getCookieParams()['flarum_remember'], 'token' => array_get($this->request->getCookieParams(), 'flarum_remember'),
]; ];
} }
} }

View File

@ -11,6 +11,8 @@
@foreach ($styles as $file) @foreach ($styles as $file)
<link rel="stylesheet" href="{{ str_replace(public_path(), '', $file) }}"> <link rel="stylesheet" href="{{ str_replace(public_path(), '', $file) }}">
@endforeach @endforeach
{!! $head !!}
</head> </head>
<body> <body>
@ -37,5 +39,7 @@
{!! $content !!} {!! $content !!}
</noscript> </noscript>
@endif @endif
{!! $foot !!}
</body> </body>
</html> </html>

View File

@ -1,21 +1,56 @@
<?php
/**
* Forum Client Template
*
* NOTE: You shouldn't edit this file directly. Your changes will be overwritten
* when you update Flarum. See flarum.org/docs/templates to learn how to
* customize your forum's layout.
*
* Flarum's JavaScript client mounts various components into key elements in
* this template. They are distinguished by their ID attributes:
*
* - #page
* - #page-back-button
* - #header
* - #header-back-button
* - #home-link
* - #header-primary-controls
* - #header-secondary-controls
* - #footer
* - #footer-primary-controls
* - #footer-secondary-controls
* - #content
* - #composer
*/
?>
<div class="global-page" id="page"> <div class="global-page" id="page">
<div id="back-control"></div> <div id="back-control"></div>
<div class="global-drawer"> <div class="global-drawer">
<header class="global-header" id="header"> <header class="global-header" id="header">
<div id="back-button"></div> <div id="back-button"></div>
<div class="container"> <div class="container">
<h1 class="header-title"><a href="{{ $forum->attributes->baseUrl }}" id="home-link">{{ $forum->attributes->title }}</a></h1> <h1 class="header-title">
<a href="{{ $forum->attributes->baseUrl }}" id="home-link">
{{ $forum->attributes->title }}
</a>
</h1>
<div id="header-primary" class="header-primary"></div> <div id="header-primary" class="header-primary"></div>
<div id="header-secondary" class="header-secondary"></div> <div id="header-secondary" class="header-secondary"></div>
</div> </div>
</header> </header>
<footer class="global-footer" id="footer"> <footer class="global-footer" id="footer">
<div class="container"> <div class="container">
<div id="footer-primary" class="footer-primary"></div> <div id="footer-primary" class="footer-primary"></div>
<div id="footer-secondary" class="footer-secondary"></div> <div id="footer-secondary" class="footer-secondary"></div>
</div> </div>
</footer> </footer>
</div> </div>
<main class="global-content"> <main class="global-content">
<div id="content"></div> <div id="content"></div>
<div class="composer-container"> <div class="composer-container">
@ -24,4 +59,5 @@
</div> </div>
</div> </div>
</main> </main>
</div> </div>