Merge #16028 - Ref #16005 - Use caching to speed up route parsing

Ref: #16005
Pull-request: #16028
Signed-off-by: William Desportes <williamdes@wdes.fr>
This commit is contained in:
William Desportes 2020-03-28 11:39:01 +01:00
commit 94760f43e7
No known key found for this signature in database
GPG Key ID: 90A0EF1B8251A889
14 changed files with 268 additions and 62 deletions

View File

@ -4,10 +4,7 @@
*/
declare(strict_types=1);
use FastRoute\Dispatcher;
use PhpMyAdmin\Message;
use PhpMyAdmin\Response;
use function FastRoute\simpleDispatcher;
use PhpMyAdmin\Routing;
if (! defined('ROOT_PATH')) {
// phpcs:disable PSR1.Files.SideEffects
@ -15,52 +12,9 @@ if (! defined('ROOT_PATH')) {
// phpcs:enable
}
global $containerBuilder, $route;
/** @var string $route */
$route = $_GET['route'] ?? $_POST['route'] ?? '/';
/**
* See FAQ 1.34.
*
* @see https://docs.phpmyadmin.net/en/latest/faq.html#faq1-34
*/
if (($route === '/' || $route === '') && isset($_GET['db']) && mb_strlen($_GET['db']) !== 0) {
$route = '/database/structure';
if (isset($_GET['table']) && mb_strlen($_GET['table']) !== 0) {
$route = '/sql';
}
}
if ($route === '/import-status') {
// phpcs:disable PSR1.Files.SideEffects
define('PMA_MINIMUM_COMMON', true);
// phpcs:enable
}
global $route;
require_once ROOT_PATH . 'libraries/common.inc.php';
$routes = require ROOT_PATH . 'libraries/routes.php';
$dispatcher = simpleDispatcher($routes);
$routeInfo = $dispatcher->dispatch(
$_SERVER['REQUEST_METHOD'],
rawurldecode($route)
);
if ($routeInfo[0] === Dispatcher::NOT_FOUND) {
/** @var Response $response */
$response = $containerBuilder->get(Response::class);
$response->setHttpResponseCode(404);
Message::error(sprintf(
__('Error 404! The page %s was not found.'),
'<code>' . $route . '</code>'
))->display();
} elseif ($routeInfo[0] === Dispatcher::METHOD_NOT_ALLOWED) {
/** @var Response $response */
$response = $containerBuilder->get(Response::class);
$response->setHttpResponseCode(405);
Message::error(__('Error 405! Request method not allowed.'))->display();
} elseif ($routeInfo[0] === Dispatcher::FOUND) {
[$controllerName, $action] = $routeInfo[1];
$controller = $containerBuilder->get($controllerName);
$controller->$action($routeInfo[2]);
}
$dispatcher = Routing::getDispatcher();
Routing::callControllerForRoute($route, $dispatcher);

2
libraries/cache/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/*
!/.gitignore

View File

@ -6,6 +6,7 @@ namespace PhpMyAdmin\Command;
use PhpMyAdmin\Config;
use PhpMyAdmin\DatabaseInterface;
use PhpMyAdmin\Routing;
use PhpMyAdmin\Tests\Stubs\DbiDummy;
use PhpMyAdmin\Twig\CoreExtension;
use PhpMyAdmin\Twig\I18nExtension;
@ -41,13 +42,50 @@ final class CacheWarmupCommand extends Command
protected function configure(): void
{
$this->setDescription('Warms up the Twig templates cache');
$this->addOption('twig', null, null, 'Warm up twig templates cache.');
$this->addOption('routing', null, null, 'Warm up routing cache.');
$this->setHelp('The <info>%command.name%</info> command warms up the cache of the Twig templates.');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
if ($input->getOption('twig') === true && $input->getOption('routing') === true) {
$output->writeln('Please specify --twig or --routing');
return 1;
} elseif ($input->getOption('twig') === true) {
return $this->warmUpTwigCache($output);
} elseif ($input->getOption('routing') === true) {
return $this->warmUpRoutingCache($output);
} else {
$output->writeln('Warming up all caches.', OutputInterface::VERBOSITY_VERBOSE);
$twigCode = $this->warmUptwigCache($output);
if ($twigCode !== 0) {
$output->writeln('Twig cache generation had an error.');
return $twigCode;
}
$routingCode = $this->warmUpTwigCache($output);
if ($routingCode !== 0) {
$output->writeln('Routing cache generation had an error.');
return $twigCode;
}
$output->writeln('Warm up of all caches done.', OutputInterface::VERBOSITY_VERBOSE);
return 0;
}
}
private function warmUpRoutingCache(OutputInterface $output): int
{
$output->writeln('Warming up the routing cache', OutputInterface::VERBOSITY_VERBOSE);
Routing::getDispatcher();
$output->writeln('Warm up done.', OutputInterface::VERBOSITY_VERBOSE);
return 0;
}
private function warmUpTwigCache(OutputInterface $output): int
{
global $cfg, $PMA_Config, $dbi;
$output->writeln('Warming up the twig cache', OutputInterface::VERBOSITY_VERBOSE);
$cfg['environment'] = 'production';
$PMA_Config = new Config(CONFIG_FILE);
$PMA_Config->set('environment', $cfg['environment']);
@ -77,12 +115,15 @@ final class CacheWarmupCommand extends Command
/** @var CacheInterface $twigCache */
$twigCache = $twig->getCache(false);
$output->writeln('Searching for files...', OutputInterface::VERBOSITY_VERY_VERBOSE);
$replacements = [];
$templates = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($tplDir),
RecursiveIteratorIterator::LEAVES_ONLY
);
$output->writeln('Warming templates', OutputInterface::VERBOSITY_VERY_VERBOSE);
foreach ($templates as $file) {
// Skip test files
if (strpos($file->getPathname(), '/test/') !== false) {
@ -91,6 +132,7 @@ final class CacheWarmupCommand extends Command
// force compilation
if ($file->isFile() && $file->getExtension() === 'twig') {
$name = str_replace($tplDir . '/', '', $file->getPathname());
$output->writeln('Loading: ' . $name, OutputInterface::VERBOSITY_DEBUG);
$template = $twig->loadTemplate($name);
// Generate line map
@ -101,6 +143,7 @@ final class CacheWarmupCommand extends Command
}
}
$output->writeln('Writing replacements...', OutputInterface::VERBOSITY_VERY_VERBOSE);
// Store replacements in JSON
$handle = fopen($tmpDir . '/replace.json', 'w');
if ($handle === false) {
@ -109,6 +152,7 @@ final class CacheWarmupCommand extends Command
fwrite($handle, (string) json_encode($replacements));
fclose($handle);
$output->writeln('Warm up done.', OutputInterface::VERBOSITY_VERBOSE);
return 0;
}

View File

@ -45,7 +45,7 @@ class ImportAjax
* list of available plugins
*/
$plugins = [
// PHP 5.4 session-based upload progress is problematic, see bug 3964
// in PHP 5.4 session-based upload progress was problematic, see closed bug 3964
//"session",
'progress',
'apc',

View File

@ -0,0 +1,80 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin;
use PhpMyAdmin\Message;
use PhpMyAdmin\Response;
use FastRoute\Dispatcher;
use function FastRoute\cachedDispatcher;
use function mb_strlen;
use function rawurldecode;
/**
* Class used to warm up the routing cache and manage routing.
*/
class Routing
{
public static function getDispatcher(): Dispatcher
{
global $cfg;
$routes = require ROOT_PATH . 'libraries/routes.php';
return cachedDispatcher($routes, [
'cacheFile' => CACHE_DIR . 'routes.cache',
'cacheDisabled' => ($cfg['environment'] ?? '') === 'development',
]);
}
public static function getCurrentRoute(): string
{
/** @var string $route */
$route = $_GET['route'] ?? $_POST['route'] ?? '/';
/**
* See FAQ 1.34.
*
* @see https://docs.phpmyadmin.net/en/latest/faq.html#faq1-34
*/
if (($route === '/' || $route === '') && isset($_GET['db']) && mb_strlen($_GET['db']) !== 0) {
$route = '/database/structure';
if (isset($_GET['table']) && mb_strlen($_GET['table']) !== 0) {
$route = '/sql';
}
}
return $route;
}
/**
* Call associated controller for a route using the dispatcher
* @param string $route The current route
* @param Dispatcher $dispatcher The dispatcher
*/
public static function callControllerForRoute(string $route, Dispatcher $dispatcher): void
{
global $containerBuilder;
$routeInfo = $dispatcher->dispatch(
$_SERVER['REQUEST_METHOD'],
rawurldecode($route)
);
if ($routeInfo[0] === Dispatcher::NOT_FOUND) {
/** @var Response $response */
$response = $containerBuilder->get(Response::class);
$response->setHttpResponseCode(404);
Message::error(sprintf(
__('Error 404! The page %s was not found.'),
'<code>' . $route . '</code>'
))->display();
} elseif ($routeInfo[0] === Dispatcher::METHOD_NOT_ALLOWED) {
/** @var Response $response */
$response = $containerBuilder->get(Response::class);
$response->setHttpResponseCode(405);
Message::error(__('Error 405! Request method not allowed.'))->display();
} elseif ($routeInfo[0] === Dispatcher::FOUND) {
[$controllerName, $action] = $routeInfo[1];
$controller = $containerBuilder->get($controllerName);
$controller->$action($routeInfo[2]);
}
}
}

View File

@ -40,6 +40,7 @@ use PhpMyAdmin\Message;
use PhpMyAdmin\MoTranslator\Loader;
use PhpMyAdmin\Plugins\AuthenticationPlugin;
use PhpMyAdmin\Response;
use PhpMyAdmin\Routing;
use PhpMyAdmin\Sanitize;
use PhpMyAdmin\Session;
use PhpMyAdmin\SqlParser\Lexer;
@ -50,7 +51,7 @@ use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader\PhpFileLoader;
global $containerBuilder, $error_handler, $PMA_Config, $server, $dbi, $lang, $cfg, $isConfigLoading, $auth_plugin;
global $containerBuilder, $error_handler, $PMA_Config, $server, $dbi, $lang, $cfg, $isConfigLoading, $auth_plugin, $route;
/**
* block attempts to directly run this script
@ -95,6 +96,14 @@ if (! @is_readable(AUTOLOAD_FILE)) {
}
require_once AUTOLOAD_FILE;
$route = Routing::getCurrentRoute();
if ($route === '/import-status') {
// phpcs:disable PSR1.Files.SideEffects
define('PMA_MINIMUM_COMMON', true);
// phpcs:enable
}
$containerBuilder = new ContainerBuilder();
$loader = new PhpFileLoader($containerBuilder, new FileLocator(__DIR__));
$loader->load('services_loader.php');

View File

@ -79,3 +79,8 @@ define('LOCALE_PATH', ROOT_PATH . 'locale/');
* is used)
*/
define('K_PATH_IMAGES', ROOT_PATH);
/**
* Define the cache directory for routing cache an other cache files
*/
define('CACHE_DIR', ROOT_PATH . 'libraries/cache/');

View File

@ -74,6 +74,7 @@
<arg name="extensions" value="php"/>
<exclude-pattern>*/node_modules/*</exclude-pattern>
<exclude-pattern>*/libraries/cache/*</exclude-pattern>
<exclude-pattern>*/test/sami-config.php</exclude-pattern>
<exclude-pattern>*.twig</exclude-pattern>
<exclude-pattern>*/twig-templates/*</exclude-pattern>

View File

@ -12,6 +12,7 @@ parameters:
excludes_analyse:
- examples/openid.php
- node_modules/*
- libraries/cache/*
- test/sami-config.php
- tmp/*
- twig-templates/*

View File

@ -25,6 +25,7 @@
<exclude>
<directory>examples</directory>
<directory>node_modules</directory>
<directory>libraries/cache</directory>
<directory>test</directory>
<directory>tmp</directory>
<directory>vendor</directory>

View File

@ -13,6 +13,12 @@ define('PHPMYADMIN', true);
require_once ROOT_PATH . 'libraries/vendor_config.php';
require_once AUTOLOAD_FILE;
if (! class_exists(Application::class)) {
echo 'Be sure to have dev-dependencies installed.' . PHP_EOL;
echo 'Command aborted.' . PHP_EOL;
exit(1);
}
$application = new Application('phpMyAdmin Console Tool');
$application->add(new AdvisoryRulesCommand());

View File

@ -80,7 +80,7 @@ while [ $# -gt 0 ] ; do
exit 1
fi
elif [ -z "$branch" ] ; then
branch=`echo $1 | tr -d -c '0-9A-Za-z_-'`
branch=`echo $1 | tr -d -c '/0-9A-Za-z_-'`
if [ "x$branch" != "x$1" ] ; then
echo "Invalid branch: $1"
exit 1
@ -218,11 +218,8 @@ echo "* Removing unneeded files"
# Remove developer information
rm -rf .github
# Remove phpcs coding standard definition
rm -rf PMAStandard
# Testsuite setup
rm -f .travis.yml .coveralls.yml .scrutinizer.yml .jshintrc .weblate codecov.yml
rm -f .travis.yml .scrutinizer.yml .jshintrc .weblate codecov.yml
# Remove readme for github
rm -f README.rst
@ -243,7 +240,7 @@ if [ ! -d libraries/tcpdf ] ; then
cp composer.json composer.json.backup
echo "* Running composer"
composer config platform.php "$PHP_REQ"
composer update --no-dev
composer update --no-dev --optimize-autoloader
# Parse the required versions from composer.json
PACKAGES_VERSIONS=''
@ -256,7 +253,7 @@ if [ ! -d libraries/tcpdf ] ; then
do
PACKAGES_VERSIONS="$PACKAGES_VERSIONS $PACKAGES:`awk "/require-dev/ {printline = 1; print; next } printline" composer.json | grep "$PACKAGES" | awk -F [\\"] '{print $4}'`"
done
composer require --update-no-dev $PACKAGES_VERSIONS
composer require --optimize-autoloader --update-no-dev $PACKAGES_VERSIONS
mv composer.json.backup composer.json
echo "* Cleanup of composer packages"
@ -304,6 +301,11 @@ fi
# Remove Bootstrap theme
rm -rf themes/bootstrap
composer update
# Warm up the routing cache
./scripts/console cache:warmup --routing
composer update --no-dev --optimize-autoloader
# Remove git metadata
rm .git
find . -name .gitignore -print0 | xargs -0 -r rm -f
@ -329,7 +331,7 @@ if [ $do_test -eq 1 ] ; then
rm -f .phpunit.result.cache
# Remove libs installed for testing
rm -rf build
composer update --no-dev
composer update --no-dev --optimize-autoloader
fi
@ -351,7 +353,7 @@ for kit in $KITS ; do
# Testsuite
rm -rf test/
rm phpunit.xml.* build.xml
rm -f .editorconfig .eslintignore .eslintrc.json .stylelintrc.json phpstan.neon.dist phpcs.xml.dist
rm -f .editorconfig .eslintignore .jshintrc .eslintrc.json .stylelintrc.json phpstan.neon.dist phpstan-baseline.neon phpcs.xml.dist
# Gettext po files
rm -rf po/
# Documentation source code

View File

@ -13,7 +13,7 @@ set -e
# Generate Twig template cache in clean dir
rm -rf twig-templates/
php scripts/console cache:warmup
php scripts/console cache:warmup --twig
# Update pot (template), ensure that advisor is at the end
LOCS=`ls po/*.po | sed 's@.*/\(.*\)\.po@\1@'`

View File

@ -0,0 +1,101 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Tests;
use PhpMyAdmin\Routing;
use FastRoute\Dispatcher;
use PhpMyAdmin\Controllers\HomeController;
/**
* Tests for PhpMyAdmin\Routing
*/
class RoutingTest extends PmaTestCase
{
/**
* Test for Routing::getDispatcher
*
* @return void
*/
public function testGetDispatcher(): void
{
$dispatcher = Routing::getDispatcher();
$this->assertInstanceOf(Dispatcher::class, $dispatcher);
$this->assertSame([
Dispatcher::FOUND,
[
HomeController::class,
'index',
],
[],
], $dispatcher->dispatch('GET', '/'));
}
/**
* Test for Routing::getCurrentRoute
*
* @return void
*/
public function testGetCurrentRouteNoParams(): void
{
$this->assertSame('/', Routing::getCurrentRoute());
}
/**
* Test for Routing::getCurrentRoute
*
* @return void
*/
public function testGetCurrentRouteGet(): void
{
$_GET['route'] = '/test';
$this->assertSame('/test', Routing::getCurrentRoute());
}
/**
* Test for Routing::getCurrentRoute
*
* @return void
*/
public function testGetCurrentRoutePost(): void
{
$_POST['route'] = '/testpost';
$this->assertSame('/testpost', Routing::getCurrentRoute());
}
/**
* Test for Routing::getCurrentRoute
*
* @return void
*/
public function testGetCurrentRouteGetIsOverPost(): void
{
$_GET['route'] = '/testget';
$_POST['route'] = '/testpost';
$this->assertSame('/testget', Routing::getCurrentRoute());
}
/**
* Test for Routing::getCurrentRoute
*
* @return void
*/
public function testGetCurrentRouteRedirectDbStructure(): void
{
$_GET['db'] = 'testDB';
$this->assertSame('/database/structure', Routing::getCurrentRoute());
}
/**
* Test for Routing::getCurrentRoute
*
* @return void
*/
public function testGetCurrentRouteRedirectSql(): void
{
$_GET['db'] = 'testDB';
$_GET['table'] = 'tableTest';
$this->assertSame('/sql', Routing::getCurrentRoute());
}
}