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

Refactor the web app bootstrapping code

- All custom JS variables are now preloaded into the `app.data` object, rather than directly on the `app` object. This means that admin settings are available in `app.data.settings` rather than `app.settings`, etc.
- Cleaner route handler generation
- Renamed ConfigureClientView to ConfigureWebApp, though the former still exists and is deprecated
- Partial fix for #881 (strips ?nojs=1 from URL if possible, so that refreshing will attempt to load JS version again)
This commit is contained in:
Toby Zerner
2016-05-26 19:04:24 +09:30
parent 2525e3e7ad
commit 9bfb797fdc
49 changed files with 1575 additions and 1254 deletions

View File

@@ -1,316 +0,0 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Http\Controller;
use Flarum\Api\Client;
use Flarum\Asset\AssetManager;
use Flarum\Asset\JsCompiler;
use Flarum\Asset\LessCompiler;
use Flarum\Event\ConfigureClientView;
use Flarum\Foundation\Application;
use Flarum\Locale\JsCompiler as LocaleJsCompiler;
use Flarum\Locale\LocaleManager;
use Flarum\Settings\SettingsRepositoryInterface;
use Illuminate\Contracts\Cache\Repository;
use Illuminate\Contracts\Events\Dispatcher;
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 AbstractClientController extends AbstractHtmlController
{
/**
* 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;
/**
* A regex matching the keys of the translations that should be included in
* the compiled locale file.
*
* @var string
*/
protected $translations;
/**
* @var \Flarum\Foundation\Application
*/
protected $app;
/**
* @var Client
*/
protected $api;
/**
* @var LocaleManager
*/
protected $locales;
/**
* @var \Flarum\Settings\SettingsRepositoryInterface
*/
protected $settings;
/**
* @var Dispatcher
*/
protected $events;
/**
* @var Repository
*/
protected $cache;
/**
* @param \Flarum\Foundation\Application $app
* @param Client $api
* @param LocaleManager $locales
* @param \Flarum\Settings\SettingsRepositoryInterface $settings
* @param Dispatcher $events
* @param Repository $cache
*/
public function __construct(
Application $app,
Client $api,
LocaleManager $locales,
SettingsRepositoryInterface $settings,
Dispatcher $events,
Repository $cache
) {
$this->app = $app;
$this->api = $api;
$this->locales = $locales;
$this->settings = $settings;
$this->events = $events;
$this->cache = $cache;
}
/**
* {@inheritdoc}
*
* @return ClientView
*/
public function render(Request $request)
{
$actor = $request->getAttribute('actor');
$assets = $this->getAssets();
$locale = $this->locales->getLocale();
$localeCompiler = $locale ? $this->getLocaleCompiler($locale) : null;
$view = new ClientView(
$this->api,
$request,
$actor,
$assets,
$this->layout,
$localeCompiler
);
$view->setVariable('locales', $this->locales->getLocales());
$view->setVariable('locale', $locale);
$this->events->fire(
new ConfigureClientView($this, $view)
);
if ($localeCompiler) {
$translations = array_get($this->locales->getTranslator()->getMessages(), 'messages', []);
$translations = $this->filterTranslations($translations);
$localeCompiler->setTranslations($translations);
}
app('view')->share('translator', $this->locales->getTranslator());
return $view;
}
/**
* Flush the client's assets so that they will be regenerated from scratch
* on the next render.
*/
public function flushAssets()
{
$this->flushCss();
$this->flushJs();
}
public function flushCss()
{
$this->getAssets()->flushCss();
}
public function flushJs()
{
$this->getAssets()->flushJs();
$this->flushLocales();
}
public function flushLocales()
{
$locales = array_keys($this->locales->getLocales());
foreach ($locales as $locale) {
$this->getLocaleCompiler($locale)->flush();
}
}
/**
* 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();
$watch = $this->app->config('debug');
$assets = new AssetManager(
new JsCompiler($public, "$this->clientName.js", $watch, $this->cache),
new LessCompiler($public, "$this->clientName.css", $watch, $this->app->storagePath().'/less')
);
$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)
{
$assets->addLess(function () {
$less = '';
foreach ($this->getLessVariables() as $name => $value) {
$less .= "@$name: $value;";
}
$less .= $this->settings->get('custom_less');
return $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 [
'config-primary-color' => $this->settings->get('theme_primary_color') ?: '#000',
'config-secondary-color' => $this->settings->get('theme_secondary_color') ?: '#000',
'config-dark-mode' => $this->settings->get('theme_dark_mode') ? 'true' : 'false',
'config-colored-header' => $this->settings->get('theme_colored_header') ? 'true' : 'false'
];
}
/**
* Set up the locale compiler for the given locale.
*
* @param string $locale
* @return LocaleJsCompiler
*/
protected function getLocaleCompiler($locale)
{
$compiler = new LocaleJsCompiler(
$this->getAssetDirectory(),
"$this->clientName-$locale.js",
$this->app->config('debug'),
$this->cache
);
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
* @return array
*/
protected function filterTranslations(array $translations)
{
if (! $this->translations) {
return [];
}
$filtered = array_filter(array_keys($translations), function ($id) {
return preg_match($this->translations, $id);
});
return array_only($translations, $filtered);
}
}

View File

@@ -24,7 +24,7 @@ abstract class AbstractHtmlController implements ControllerInterface
$view = $this->render($request);
$response = new Response;
$response->getBody()->write($view->render());
$response->getBody()->write($view);
return $response;
}

View File

@@ -0,0 +1,52 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Http\Controller;
use Flarum\Event\ConfigureWebApp;
use Flarum\Http\WebApp\AbstractWebApp;
use Illuminate\Contracts\Events\Dispatcher;
use Psr\Http\Message\ServerRequestInterface as Request;
abstract class AbstractWebAppController extends AbstractHtmlController
{
/**
* @var AbstractWebApp
*/
protected $webApp;
/**
* @var Dispatcher
*/
protected $events;
/**
* {@inheritdoc}
*/
public function render(Request $request)
{
$view = $this->getView($request);
$this->events->fire(
new ConfigureWebApp($this, $view, $request)
);
return $view->render($request);
}
/**
* @param Request $request
* @return \Flarum\Http\WebApp\WebAppView
*/
protected function getView(Request $request)
{
return $this->webApp->getView();
}
}

View File

@@ -1,371 +0,0 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Http\Controller;
use Flarum\Api\Client;
use Flarum\Asset\AssetManager;
use Flarum\Core\User;
use Flarum\Locale\JsCompiler;
use Illuminate\Contracts\Support\Renderable;
use Psr\Http\Message\ServerRequestInterface as Request;
/**
* 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;
/**
* The title of the document, displayed in the <title> tag.
*
* @var null|string
*/
protected $title;
/**
* The SEO content of the page, displayed in <noscript> tags.
*
* @var string
*/
protected $content;
/**
* The path to the client layout view to display.
*
* @var string
*/
protected $layout;
/**
* An API response that should be preloaded into the page.
*
* @var null|array|object
*/
protected $document;
/**
* Other variables to preload into the page.
*
* @var array
*/
protected $variables = [];
/**
* An array of JS modules to import before booting the app.
*
* @var array
*/
protected $bootstrappers = ['locale'];
/**
* 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 $api;
/**
* @var Request
*/
protected $request;
/**
* @var AssetManager
*/
protected $assets;
/**
* @var JsCompiler
*/
protected $localeJs;
/**
* @param Client $api
* @param Request $request
* @param User $actor
* @param AssetManager $assets
* @param string $layout
* @param JsCompiler $localeJs
*/
public function __construct(
Client $api,
Request $request,
User $actor,
AssetManager $assets,
$layout,
JsCompiler $localeJs = null
) {
$this->api = $api;
$this->request = $request;
$this->actor = $actor;
$this->assets = $assets;
$this->layout = $layout;
$this->localeJs = $localeJs;
$this->addHeadString('<link rel="stylesheet" href="//fonts.googleapis.com/css?family=Open+Sans:400italic,700italic,400,700,600">', 'font');
}
/**
* The title of the document, to be displayed in the <title> tag.
*
* @param null|string $title
*/
public function setTitle($title)
{
$this->title = $title;
}
/**
* Set the SEO content of the page, to be displayed in <noscript> tags.
*
* @param null|string $content
*/
public function setContent($content)
{
$this->content = $content;
}
/**
* Set the name of the client layout view to display.
*
* @param string $layout
*/
public function setLayout($layout)
{
$this->layout = $layout;
}
/**
* Add a string to be appended to the page's <head>.
*
* @param string $string
*/
public function addHeadString($string, $name = null)
{
if ($name) {
$this->headStrings[$name] = $string;
} else {
$this->headStrings[] = $string;
}
}
/**
* Add a string to be prepended before the page's </body>.
*
* @param string $string
*/
public function addFootString($string)
{
$this->footStrings[] = $string;
}
/**
* 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)
{
$this->document = $document;
}
/**
* Set a variable to be preloaded into the app.
*
* @param string $name
* @param mixed $value
*/
public function setVariable($name, $value)
{
$this->variables[$name] = $value;
}
/**
* Add a JavaScript module to be imported before the app is booted.
*
* @param string $string
*/
public function addBootstrapper($string)
{
$this->bootstrappers[] = $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()
{
$view = app('view')->file(__DIR__.'/../../../views/app.blade.php');
$forum = $this->getForumDocument();
$data = $this->getDataFromDocument($forum);
if ($this->actor->exists) {
$user = $this->getUserDocument();
$data = array_merge($data, $this->getDataFromDocument($user));
}
$view->app = [
'preload' => [
'data' => $data,
'session' => $this->getSession(),
'document' => $this->document
]
] + $this->variables;
$view->bootstrappers = $this->bootstrappers;
$noJs = array_get($this->request->getQueryParams(), 'nojs');
$view->title = ($this->title ? $this->title.' - ' : '').$forum->data->attributes->title;
$view->forum = $forum->data;
$view->layout = app('view')->file($this->layout, [
'forum' => $forum->data,
'content' => app('view')->file(__DIR__.'/../../../views/content.blade.php', [
'content' => $this->content,
'noJs' => $noJs,
'forum' => $forum->data
])
]);
$view->noJs = $noJs;
$view->styles = [$this->assets->getCssFile()];
$view->scripts = [$this->assets->getJsFile()];
if ($this->localeJs) {
$view->scripts[] = $this->localeJs->getFile();
}
$view->head = implode("\n", $this->headStrings);
$view->foot = implode("\n", $this->footStrings);
return $view->render();
}
/**
* Get the string contents of the view.
*
* @return string
*/
public function __toString()
{
return $this->render();
}
/**
* Get the result of an API request to show the forum.
*
* @return object
*/
protected function getForumDocument()
{
return json_decode($this->api->send('Flarum\Api\Controller\ShowForumController', $this->actor)->getBody());
}
/**
* Get the result of an API request to show the current user.
*
* @return object
*/
protected function getUserDocument()
{
// TODO: calling on the API here results in an extra query to get
// the user + their groups, when we already have this information on
// $this->actor. Can we simply run the CurrentUserSerializer
// manually? Or can we somehow inject this data into the ShowDiscussionController?
$document = json_decode($this->api->send(
'Flarum\Api\Controller\ShowUserController',
$this->actor,
['id' => $this->actor->id]
)->getBody());
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)
{
$data[] = $document->data;
if (isset($document->included)) {
$data = array_merge($data, $document->included);
}
return $data;
}
/**
* Get information about the current session.
*
* @return array
*/
protected function getSession()
{
$session = $this->request->getAttribute('session');
return [
'userId' => $this->actor->id,
'csrfToken' => $session->get('csrf_token')
];
}
/**
* @return User
*/
public function getActor()
{
return $this->actor;
}
/**
* @return Request
*/
public function getRequest()
{
return $this->request;
}
}