This commit is contained in:
Florian Bouillon 2019-04-12 16:56:36 +02:00
parent bf4e1d44c0
commit b80210ed64
16 changed files with 448 additions and 123 deletions

11
.editorconfig Normal file
View File

@ -0,0 +1,11 @@
root = true
[*]
indent_style = tab
indent_size = 4
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.php]
indent_style = space

7
.gitignore vendored
View File

@ -3,7 +3,12 @@ cache/
/vendor/ /vendor/
composer.lock composer.lock
node_modules /node_modules/
/logs/*
!/logs/.gitkeep
/src/Modules/*
!/src/Modules/ModuleName/
/tmp/
# OS Related (used: https://gitignore.io/) # OS Related (used: https://gitignore.io/)

View File

@ -1,6 +1,6 @@
<ruleset xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" name="PSR2" xsi:noNamespaceSchemaLocation="../../../phpcs.xsd"> <ruleset xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" name="PSR12" xsi:noNamespaceSchemaLocation="../../../phpcs.xsd">
<description>The PSR-2 coding standard.</description> <description>The PSR-12 coding standard.</description>
<arg name="tab-width" value="4"/> <rule ref="PSR12"/>
<rule ref="PSR1"/> <file>src</file>
<rule ref="PSR2"/> <exclude-pattern>Modules</exclude-pattern>
</ruleset> </ruleset>

View File

@ -22,10 +22,14 @@
] ]
}, },
"require": { "require": {
"twig/twig": "^2.7" "twig/twig": "^2.7",
"psr/simple-cache": "^1.0",
"psr/log": "^1.1"
}, },
"require-dev": { "require-dev": {
"squizlabs/php_codesniffer": "^3.4", "squizlabs/php_codesniffer": "^3.4",
"symfony/var-dumper": "^4.2" "symfony/var-dumper": "^4.2",
"phpunit/phpunit": "^8.1",
"codacy/coverage": "^1.4"
} }
} }

0
logs/.gitkeep Normal file
View File

39
phpunit.xml Normal file
View File

@ -0,0 +1,39 @@
<phpunit
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/|version|/phpunit.xsd"
backupGlobals="true"
backupStaticAttributes="false"
bootstrap="vendor/autoload.php"
cacheResult="false"
cacheTokens="false"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
forceCoversAnnotation="false"
printerClass="PHPUnit\TextUI\ResultPrinter"
processIsolation="false"
stopOnError="false"
stopOnFailure="false"
stopOnIncomplete="false"
stopOnSkipped="false"
stopOnRisky="false"
testSuiteLoaderClass="PHPUnit\Runner\StandardTestSuiteLoader"
timeoutForSmallTests="1"
timeoutForMediumTests="10"
timeoutForLargeTests="60"
verbose="false">
<filter>
<whitelist>
<directory>src/AdminPanel</directory>
</whitelist>
</filter>
<testsuites>
<testsuite name="Tests">
<directory>tests/</directory>
</testsuite>
</testsuites>
<logging>
<log type="coverage-clover" target="tmp/code-coverage.xml" />
</logging>
</phpunit>

View File

@ -0,0 +1,9 @@
<?php
namespace AdminPanel\Cache;
use Exception;
class CacheException extends Exception implements \Psr\SimpleCache\CacheException
{
}

View File

@ -0,0 +1,131 @@
<?php
namespace AdminPanel\Cache;
use Psr\SimpleCache\CacheInterface;
class FileCache implements CacheInterface
{
private $folder;
private $ttl;
private function checkKey($key)
{
return preg_match('/^[A-Za-z0-9_.]{1,64}$/', $key);
}
private function getTTL($ttl)
{
if (is_int($ttl)) {
return $ttl;
} else {
return
((($ttl->y * 365 + $ttl->m * 30 + $ttl->d
) * 24 + $ttl->h
) * 60 + $ttl->i
) * 60 + $ttl->s;
}
}
/**
* Cache Constructor
*
* @param string $folder
* @param integer|\DateInterval $ttl
*/
public function __construct(string $folder = "./cache", $ttl = 86400)
{
$this->folder = $folder;
if (!file_exists($this->folder)) {
mkdir($this->folder, 0777, true);
}
$this->ttl = $this->getTTl($ttl);
}
public function get($key, $default = null)
{
if (!$this->checkKey($key)) {
throw new InvalidArgumentException("key is not correct");
}
$file = $this->folder . DIRECTORY_SEPARATOR . $key;
if (is_file($file)) {
$res = unserialize(file_get_contents($file));
if ($res["ttl"] > time() && $res['value'] !== null) {
return $res["value"];
} else {
$this->delete($key);
}
}
return $default;
}
public function set($key, $value, $ttl = null)
{
if (!$this->checkKey($key)) {
throw new InvalidArgumentException("key is not valid");
}
$tl = $ttl != null ? $this->getTTL($ttl) : $this->ttl;
$arr = array(
"value" => $value,
'ttl' => time() + $tl
);
return file_put_contents($this->folder . DIRECTORY_SEPARATOR . $key, serialize($arr)) ? true : false;
}
public function delete($key)
{
if (!$this->checkKey($key)) {
throw new InvalidArgumentException("key is not valid");
}
return unlink($this->folder . DIRECTORY_SEPARATOR . $key);
}
public function clear()
{
$keys = array_diff(scandir($this->folder), array("..", "."));
foreach ($keys as $key) {
$this->delete($key);
}
}
public function getMultiple($keys, $default = null)
{
if (!is_iterable($keys)) {
throw new InvalidArgumentException('$keys isn\'t traversable');
}
$result = array();
foreach ($keys as $key) {
if (!$this->checkKey($key)) {
throw new InvalidArgumentException("a key in the array is invalid");
}
$result[$key] = $this->get($key, $default);
}
return $result;
}
public function setMultiple($values, $ttl = null)
{
if (!is_iterable($values)) {
throw new InvalidArgumentException('$values isn\'t traversable');
}
foreach ($values as $key => $value) {
$tmp = $this->set($key, $value, $ttl);
if (!$tmp) {
return false;
}
}
return true;
}
public function deleteMultiple($keys)
{
foreach ($keys as $key) {
$this->delete($key);
}
}
public function has($key)
{
return is_file($this->folder . DIRECTORY_SEPARATOR . $key);
}
}

View File

@ -0,0 +1,7 @@
<?php
namespace AdminPanel\Cache;
class InvalidArgumentException extends CacheException implements \Psr\SimpleCache\InvalidArgumentException
{
}

View File

@ -1,30 +1,52 @@
<?php <?php
/**
* @author Avior <florian.bouillon@delta-wings.net>
* @since 1.0.0
*/
namespace AdminPanel\Classes; namespace AdminPanel\Classes;
use Twig\Loader\FilesystemLoader; use Twig\Loader\FilesystemLoader;
use Twig\Environment; use Twig\Environment;
use AdminPanel\Cache\FileCache;
use AdminPanel\Logger\Logger;
class AdminPanel class AdminPanel
{ {
/** @var AdminPanel $instance */ /** @var AdminPanel $instance */
private static $instance = null; private static $instance = null;
/**
* Undocumented function
*
* @return AdminPanel
*/
public static function getInstance() public static function getInstance()
{ {
if (!isset(AdminPanel::$instance)) { if (!isset(AdminPanel::$instance)) {
define("ROOT", dirname(dirname(__DIR__)));
AdminPanel::$instance = new self(); AdminPanel::$instance = new self();
Cache::getInstance()->addTemplateFolder("/AdminPanel/Twig");
AdminPanel::$instance->addLoaderFolder(ROOT . "/AdminPanel/Twig"); AdminPanel::$instance->addLoaderFolder(ROOT . "/AdminPanel/Twig");
} }
return AdminPanel::$instance; return AdminPanel::$instance;
} }
private $loader; /** @var Logger $logger */
public function getLoader() private $logger;
public function setLogger(Logger $logger)
{ {
return $this->loader; $this->logger = $logger;
} }
public function addLoaderFolder(String $path, String $prefix = "AdminPanel") public function getLogger(): Logger
{
return $this->logger;
}
/** @var \Twig\Loader\FileSystemLoader $loader */
private $loader;
public function addLoaderFolder(string $path, string $prefix = "AdminPanel")
{ {
$this->loader->addPath($path, $prefix); $this->loader->addPath($path, $prefix);
} }
@ -33,7 +55,15 @@ class AdminPanel
private $twig; private $twig;
public function getTwig() public function getTwig()
{ {
return isset($this->twig) ? $this->twig : $this->twig = new Environment($this->loader); return isset($this->twig) ? $this->twig : $this->twig = new Environment($this->loader, [
'cache' => false //dirname(ROOT) . '/cache/twig/'
]);
}
private $cache;
public function getCache()
{
return $this->cache ? $this->cache : $this->cache = new FileCache(dirname(ROOT) . "/cache/fs");
} }
public function __construct() public function __construct()

View File

@ -1,48 +0,0 @@
<?php
namespace AdminPanel\Classes;
class Cache
{
/** @var Cache $instance */
private static $instance = null;
private static $folder = "../cache/";
private static $tpFileName = "templates.json";
private static $templates;
public static function getInstance()
{
if (!isset(Cache::$instance)) {
Cache::$instance = new self();
}
return Cache::$instance;
}
public function __construct()
{
if (!is_dir(Cache::$folder)) {
mkdir(Cache::$folder);
}
Cache::$templates = Cache::$folder . Cache::$tpFileName;
if (!is_file(Cache::$templates)) {
$fp = fopen(Cache::$templates, "wb");
fwrite($fp, "{}");
fclose($fp);
}
}
public function addTemplateFolder(string $folder, string $namespace = "AdminPanel")
{
$fp = fopen(Cache::$templates, "wb");
$json = file_get_contents(Cache::$templates);
// $json = json_decode(fread($fp, filesize(Cache::$templates)));
$json[$folder] = $namespace;
fwrite($fp, json_encode($json));
fclose($fp);
}
public function addRoute(string $name, string $path, string $controller, string $function, $options = array())
{
//TODO
}
}

View File

@ -2,15 +2,21 @@
namespace AdminPanel\Classes; namespace AdminPanel\Classes;
use Twig\Loader\FilesystemLoader;
use Twig\Environment;
class Controller class Controller
{ {
protected $urlArguments = array(); protected $urlArguments = array();
protected $moduleRoot = null; protected $moduleRoot = null;
/** @var \AdminPanel\Cache\FileCache */
protected $cache;
public function __construct()
{
$this->cache = AdminPanel::getInstance()->getCache();
}
public function setUrlArguments($args) public function setUrlArguments($args)
{ {
$this->urlArguments = $args; $this->urlArguments = $args;
@ -33,5 +39,21 @@ class Controller
return $this->moduleRoot; return $this->moduleRoot;
} }
//TODO implements functions and variables to add functionnalities to controllers protected function getHTTPGet(string $element, $default = null, $emptyAllowed = false)
{
return isset($_GET[$element]) && (!empty($_GET[$element]) || $emptyAllowed) ? $_GET[$element] : $default;
}
protected function getHTTPPost(string $element, $default = null, $emptyAllowed = false)
{
return isset($_POST[$element]) && (!empty($_POST[$element]) || $emptyAllowed) ? $_POST[$element] : $default;
}
protected function render($template, $args)
{
return AdminPanel::getInstance()->getTwig()->render($template, $args);
}
//TODO: implements functions and variables to add functionnalities to controllers
} }

View File

@ -33,14 +33,19 @@ function slugEqualToURI($slug, $uri, $options)
foreach ($slug as $key => $value) { foreach ($slug as $key => $value) {
if (preg_match("/{.+}/", $value)) { if (preg_match("/{.+}/", $value)) {
$elemnt = preg_replace("/{|}/", "", $value); $elemnt = preg_replace("/{|}/", "", $value);
// dd($options);
if (!isset($options->$elemnt)) {
$return[$elemnt] = explode("?", $uri[$key])[0];
continue;
}
$elOptions = $options->$elemnt; $elOptions = $options->$elemnt;
if ($elOptions->regex != null && preg_match($elOptions->regex, $uri[$key])) { if (!isset($elOptions->regex) || ($elOptions->regex != null && preg_match($elOptions->regex, $uri[$key]))) {
$return[$elemnt] = $uri[$key]; $return[$elemnt] = explode("?", $uri[$key])[0];
continue; continue;
} else { } else {
return false; return false;
} }
//TODO correspond with module settings //TODO: correspond with module settings
} else { } else {
if ($value == $uri[$key]) { if ($value == $uri[$key]) {
continue; continue;
@ -76,24 +81,15 @@ function initCache()
$json = getModulesJSON(); $json = getModulesJSON();
foreach ($json as $moduleName => $moduleValues) { foreach ($json as $moduleName => $moduleValues) {
if (isset($moduleValues["routes"])) { if (isset($moduleValues["routes"])) {
//TODO //TODO:
} }
if (isset($moduleValues["templateFolder"])) { if (isset($moduleValues["templateFolder"])) {
Cache::getInstance()->addTemplateFolder(ROOT . "/Modules/" . $moduleName . $moduleValues["templateFolder"], $moduleName); Cache::getInstance()->addTemplateFolder(
"/Modules/" . $moduleName . $moduleValues["templateFolder"],
$moduleName
);
} }
// return $moduleValues; // return $moduleValues;
} }
return $json; return $json;
} }
/**
* Define constant.
* (well really it's just an hack for my linter I really don't why we can't define constant in the main process)
*
* @param string $name constant name
* @param mixed $value contant value
*/
function dconst(string $name, $value)
{
define($name, $value);
}

View File

@ -0,0 +1,65 @@
<?php
namespace AdminPanel\Logger;
use Psr\Log\AbstractLogger;
use Psr\Log\InvalidArgumentException;
class Logger extends AbstractLogger
{
private $file;
private $exception = "{code} {file}[{line}] {type} {message}\n";
private $start = "[{logger_logger}]: {logger_time} ";
private $levels = array(
'emergency',
'alert',
'critical',
'error',
'warning',
'notice',
'info',
'debug'
);
public function __construct($file = "./logs.log")
{
if (!file_exists($file)) {
fclose(fopen($file, 'w'));
}
$this->file = $file;
}
public static function fillMessage($message, $context)
{
foreach ($context as $key => $value) {
$message = preg_replace("/\{$key\}/", $value, $message);
}
return $message;
}
public function log($level, $message = null, array $context = array())
{
if (!in_array($level, $this->levels)) {
throw new InvalidArgumentException('Level not supported');
}
$context["logger_logger"] = ucfirst($level);
$context["logger_time"] = (new \DateTime())->format("Y-m-d H:i:s");
if ($message != null) {
file_put_contents($this->file, $this->fillMessage($this->start . $message . "\n", $context), FILE_APPEND);
}
if (isset($context["exception"]) && $context["exception"] instanceof \Exception) {
$context['code'] = $context["exception"]->getCode();
$context['file'] = $context["exception"]->getFile();
$context['line'] = get_class($context["exception"]);
$context['type'] = $context["exception"]->getCode();
$context['message'] = $context["exception"]->getMessage();
file_put_contents($this->file, $this->fillMessage($this->start . $this->exception, $context), FILE_APPEND);
}
if ($message === null && !isset($context["exception"])) {
throw new InvalidArgumentException("no exception nor message was found");
}
}
}

View File

@ -1,63 +1,95 @@
<?php <?php
use AdminPanel\Classes\AdminPanel; use AdminPanel\Classes\AdminPanel;
use AdminPanel\Classes\Cache; use AdminPanel\Logger\Logger;
use Symfony\Component\VarDumper\Caster\ExceptionCaster;
use Psr\Log\InvalidArgumentException;
session_start(); session_start();
ini_set('display_errors', 'On'); ini_set('display_errors', 'On');
/** @var Composer\Autoload\ClassLoader $loader */ /** @var Composer\Autoload\ClassLoader $loader */
$loader = require_once __DIR__ . "/../vendor/autoload.php"; $loader = require_once __DIR__ . "/../vendor/autoload.php";
// var_dump($_SERVER["REQUEST_URI"] . "/"); $logger = new Logger(dirname(__DIR__) . '/logs/logs.log');
// $_SERVER["REQUEST_URI"] = "/test/";
//get all dirs
$modulesDIR = __DIR__ . "/Modules";
$modules = array_diff(scandir($modulesDIR), array('..', '.'));
dconst("ROOT", __DIR__);
initCache(); $ap = AdminPanel::getInstance();
/* /*
1: get all the template folders 1: get all the template folders
2: match routes directly with modules 2: match routes directly with modules
*/ */
$cache = $ap->getCache();
$caches = $cache->getMultiple(array(
'routes',
'templates'
));
//if cache don't exist create it!
if ($caches["routes"] === null || $caches['templates'] === null) {
/** @var string $module */ $modulesDIR = __DIR__ . "/Modules";
foreach ($modules as $module) { $modules = array_diff(scandir($modulesDIR), array('..', '.'));
$moduleDIR = $modulesDIR . "/" . $module; /** @var string $module */
if (is_dir($moduleDIR)) { foreach ($modules as $module) {
$json = json_decode(file_get_contents($moduleDIR . "/" . strtolower($module) . ".json")); $moduleDIR = $modulesDIR . "/" . $module;
foreach ($json->routes as $routeName => $routeArgs) { if (is_dir($moduleDIR) && is_file($moduleDIR . "/" . strtolower($module) . ".json")) {
$args = isset($routeArgs->args) ? $routeArgs->args : new stdClass(); $json = json_decode(file_get_contents($moduleDIR . "/" . strtolower($module) . ".json"));
$composants = slugEqualToURI($routeArgs->path, $_SERVER["REQUEST_URI"], $args); if (isset($json->templateFolder)) {
// dump($composants !== false); $cache->set(
if ($composants !== false) { 'templates',
if (isset($json->templateFolder)) { array_merge($cache->get('templates', array()), array(
AdminPanel::getInstance()->addLoaderFolder($moduleDIR . $json->templateFolder, $module); $module => $moduleDIR . $json->templateFolder
} ))
if (isset($routeArgs->file)) { );
if (isset($routeArgs->type)) { }
header("Content-Type: " . $routeArgs->type . "; charset=UTF-8"); foreach ($json->routes as $routeName => $routeArgs) {
} $cache->set('routes', array_merge(
echo file_get_contents($moduleDIR . $routeArgs->file); $cache->get('routes', array()),
die; array(
} $routeName => $routeArgs
$loader->loadClass($routeArgs->controller); )
$function = $routeArgs->function; ));
// dump($function);
/** @var AdminPanel\Classes\Controller $controller */
$controller = new $routeArgs->controller;
$controller->setUrlArguments($composants);
$controller->setModuleRoot($moduleDIR);
// if(isset($json->templateFolder)) $controller->loadTwig($json->templateFolder);
echo $controller->$function();
die;
} }
} }
} }
$caches = $cache->getMultiple(array(
'routes',
'templates'
));
} }
//load each templates
foreach ($caches['templates'] as $key => $value) {
$ap->addLoaderFolder($value, $key);
}
foreach ($caches['routes'] as $key => $value) {
$args = isset($value->args) ? $value->args : new stdClass();
$composants = slugEqualToURI($value->path, $_SERVER["REQUEST_URI"], $args);
// dump($composants !== false);
if ($composants !== false) {
if (isset($value->file)) {
if (isset($value->type)) {
header("Content-Type: " . $value->type . "; charset=UTF-8");
}
echo file_get_contents($moduleDIR . $value->file);
die;
}
$loader->loadClass($value->controller);
$function = $value->function;
// dump($function);
/** @var AdminPanel\Classes\Controller $controller */
$controller = new $value->controller();
// dd(new $routeArgs->controller());
if ($composants) {
$controller->setUrlArguments($composants);
}
// if(isset($json->templateFolder)) $controller->loadTwig($json->templateFolder);
echo $controller->$function();
die;
}
}
http_response_code(404); http_response_code(404);
// dd(); // dd();

22
tests/LoggerTest.php Normal file
View File

@ -0,0 +1,22 @@
<?php
use PHPUnit\Framework\TestCase;
use AdminPanel\Cache\FileCache;
use AdminPanel\Logger\Logger;
final class LoggerTest extends TestCase
{
public function testCanLog()
{
$file = "tests/logs.log";
$logger = new Logger($file);
$this->assertFileExists($file);
$logger->info("test");
$this->assertStringEqualsFile(
$file,
"[Info]: " . (new \DateTime())->format("Y-m-d H:i:s") . " test\n"
);
unlink($file);
}
}