mirror of
https://github.com/openvk/chandler.git
synced 2025-03-31 21:43:59 +03:00
Removing the event system from the framework as nobody uses it.
Closes #8.
This commit is contained in:
parent
a4dd0c40ed
commit
19bf47d799
18 changed files with 124 additions and 377 deletions
6
.gitattributes
vendored
6
.gitattributes
vendored
|
@ -2,6 +2,12 @@
|
||||||
.gitattributes text eol=lf export-ignore
|
.gitattributes text eol=lf export-ignore
|
||||||
.gitignore text eol=lf export-ignore
|
.gitignore text eol=lf export-ignore
|
||||||
|
|
||||||
|
# PHP
|
||||||
|
*.php text eol=lf
|
||||||
|
|
||||||
# JSON
|
# JSON
|
||||||
*.json text eol=lf
|
*.json text eol=lf
|
||||||
*.lock text eol=lf
|
*.lock text eol=lf
|
||||||
|
|
||||||
|
# YAML
|
||||||
|
*.yml text eol=lf
|
||||||
|
|
16
.gitignore
vendored
16
.gitignore
vendored
|
@ -1,3 +1,8 @@
|
||||||
|
# Chandler
|
||||||
|
/extensions/available/
|
||||||
|
/extensions/enabled/
|
||||||
|
/chandler.yml
|
||||||
|
|
||||||
# Composer
|
# Composer
|
||||||
/vendor/
|
/vendor/
|
||||||
/composer.phar
|
/composer.phar
|
||||||
|
@ -8,13 +13,6 @@
|
||||||
# PHPUnit
|
# PHPUnit
|
||||||
/tests/cache/
|
/tests/cache/
|
||||||
|
|
||||||
/chandler.yml
|
|
||||||
|
|
||||||
/extensions/available/*
|
|
||||||
/extensions/enabled/*
|
|
||||||
!/extensions/available/.gitkeep
|
|
||||||
!/extensions/enabled/.gitkeep
|
|
||||||
|
|
||||||
/tmp/cache/di_*
|
/tmp/cache/di_*
|
||||||
/tmp/plugin_artifacts/*
|
/tmp/plugin_artifacts/*
|
||||||
/tmp/cache/database/*
|
/tmp/cache/database/*
|
||||||
|
@ -25,9 +23,5 @@
|
||||||
!/tmp/cache/templates/.gitkeep
|
!/tmp/cache/templates/.gitkeep
|
||||||
!/tmp/cache/yaml/.gitkeep
|
!/tmp/cache/yaml/.gitkeep
|
||||||
|
|
||||||
/htdocs/*
|
|
||||||
!/htdocs/.htaccess
|
|
||||||
!/htdocs/index.php
|
|
||||||
|
|
||||||
/logs/*
|
/logs/*
|
||||||
!/logs/.gitkeep
|
!/logs/.gitkeep
|
||||||
|
|
|
@ -42,7 +42,7 @@ class Bootstrap
|
||||||
*/
|
*/
|
||||||
private function igniteExtensions(): void
|
private function igniteExtensions(): void
|
||||||
{
|
{
|
||||||
Chandler\Extensions\ExtensionManager::i();
|
Chandler\Extensions\ExtensionManager::getInstance();
|
||||||
}
|
}
|
||||||
|
|
||||||
private function loadConfig(): void
|
private function loadConfig(): void
|
||||||
|
@ -103,7 +103,7 @@ class Bootstrap
|
||||||
private function route(string $url): void
|
private function route(string $url): void
|
||||||
{
|
{
|
||||||
ob_start();
|
ob_start();
|
||||||
$router = Chandler\MVC\Routing\Router::i();
|
$router = Chandler\MVC\Routing\Router::getInstance();
|
||||||
if (($output = $router->execute($url, null)) !== null)
|
if (($output = $router->execute($url, null)) !== null)
|
||||||
echo $output;
|
echo $output;
|
||||||
else
|
else
|
||||||
|
|
|
@ -3,9 +3,9 @@ use Chandler\Security\Authenticator;
|
||||||
|
|
||||||
return (function(): ?bool
|
return (function(): ?bool
|
||||||
{
|
{
|
||||||
$auth = Authenticator::i();
|
$auth = Authenticator::getInstance();
|
||||||
$user = $auth->getUser();
|
$user = $auth->getUser();
|
||||||
if(!$user) return NULL;
|
if(!$user) return NULL;
|
||||||
|
|
||||||
return $user->can("access")->model("admin")->whichBelongsTo(NULL);
|
return $user->can("access")->model("admin")->whichBelongsTo(NULL);
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,49 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types = 1);
|
|
||||||
|
|
||||||
namespace Chandler\Eventing;
|
|
||||||
|
|
||||||
use Chandler\Patterns\TSimpleSingleton;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @package Chandler\Eventing
|
|
||||||
*/
|
|
||||||
class EventDispatcher
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* @var array
|
|
||||||
*/
|
|
||||||
private $hooks = [];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param mixed $hook
|
|
||||||
*
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
function addListener($hook): bool
|
|
||||||
{
|
|
||||||
$this->hooks[] = $hook;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param \Chandler\Eventing\Events\Event $event
|
|
||||||
*
|
|
||||||
* @return \Chandler\Eventing\Events\Event
|
|
||||||
*/
|
|
||||||
function pushEvent(Events\Event $event): Events\Event
|
|
||||||
{
|
|
||||||
foreach ($this->hooks as $hook) {
|
|
||||||
if ($event instanceof Events\Cancelable)
|
|
||||||
if ($event->isCancelled())
|
|
||||||
break;
|
|
||||||
$method = "on" . str_replace("Event", "", get_class($event));
|
|
||||||
if (!method_exists($hook, $method)) continue;
|
|
||||||
$hook->$method($event);
|
|
||||||
}
|
|
||||||
return $event;
|
|
||||||
}
|
|
||||||
|
|
||||||
use TSimpleSingleton;
|
|
||||||
}
|
|
|
@ -1,15 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types = 1);
|
|
||||||
|
|
||||||
namespace Chandler\Eventing\Events;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @package Chandler\Eventing\Events
|
|
||||||
*/
|
|
||||||
interface Cancelable
|
|
||||||
{
|
|
||||||
public function cancel(): void;
|
|
||||||
|
|
||||||
public function isCancelled(): bool;
|
|
||||||
}
|
|
|
@ -1,74 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types = 1);
|
|
||||||
|
|
||||||
namespace Chandler\Eventing\Events;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @package Chandler\Eventing\Events
|
|
||||||
*/
|
|
||||||
class Event
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* @var float
|
|
||||||
*/
|
|
||||||
protected $code;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $data;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var bool
|
|
||||||
*/
|
|
||||||
protected $pristine = true;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var int
|
|
||||||
*/
|
|
||||||
protected $time;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return float
|
|
||||||
*/
|
|
||||||
public function getCode(): float
|
|
||||||
{
|
|
||||||
return $this->code;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
public function getData(): string
|
|
||||||
{
|
|
||||||
return $this->data;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return int
|
|
||||||
*/
|
|
||||||
public function getTime(): int
|
|
||||||
{
|
|
||||||
return $this->time;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
public function isTainted(): bool
|
|
||||||
{
|
|
||||||
return !$this->pristine;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param string $data
|
|
||||||
* @param float $code
|
|
||||||
*/
|
|
||||||
public function __construct(string $data = "", float $code = 0.0)
|
|
||||||
{
|
|
||||||
$this->data = $data;
|
|
||||||
$this->code = $code;
|
|
||||||
$this->time = time();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,6 +1,5 @@
|
||||||
<?php declare(strict_types=1);
|
<?php declare(strict_types=1);
|
||||||
namespace Chandler\Extensions;
|
namespace Chandler\Extensions;
|
||||||
use Chandler\Eventing\EventDispatcher;
|
|
||||||
use Chandler\Patterns\TSimpleSingleton;
|
use Chandler\Patterns\TSimpleSingleton;
|
||||||
use Chandler\MVC\Routing\Router;
|
use Chandler\MVC\Routing\Router;
|
||||||
use Nette\Utils\Finder;
|
use Nette\Utils\Finder;
|
||||||
|
@ -20,51 +19,50 @@ class ExtensionManager
|
||||||
private $router = NULL;
|
private $router = NULL;
|
||||||
private $rootApp = NULL;
|
private $rootApp = NULL;
|
||||||
private $eventLoop = NULL;
|
private $eventLoop = NULL;
|
||||||
|
|
||||||
private function __construct()
|
private function __construct()
|
||||||
{
|
{
|
||||||
foreach(Finder::findDirectories("*")->in(CHANDLER_EXTENSIONS_AVAILABLE) as $directory) {
|
foreach(Finder::findDirectories("*")->in(CHANDLER_EXTENSIONS_AVAILABLE) as $directory) {
|
||||||
$extensionName = $directory->getFilename();
|
$extensionName = $directory->getFilename();
|
||||||
$directory = $directory->getRealPath();
|
$directory = $directory->getRealPath();
|
||||||
$config = "$directory/manifest.yml";
|
$config = "$directory/manifest.yml";
|
||||||
|
|
||||||
if(!file_exists($config)) {
|
if(!file_exists($config)) {
|
||||||
trigger_error("Skipping $extensionName for not having a valid configuration file ($config is not found)", E_USER_WARNING);
|
trigger_error("Skipping $extensionName for not having a valid configuration file ($config is not found)", E_USER_WARNING);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->extensions[$extensionName] = (object) chandler_parse_yaml($config);
|
$this->extensions[$extensionName] = (object) chandler_parse_yaml($config);
|
||||||
$this->extensions[$extensionName]->id = $extensionName;
|
$this->extensions[$extensionName]->id = $extensionName;
|
||||||
$this->extensions[$extensionName]->rawName = $directory;
|
$this->extensions[$extensionName]->rawName = $directory;
|
||||||
$this->extensions[$extensionName]->enabled = CHANDLER_ROOT_CONF["extensions"]["allEnabled"];
|
$this->extensions[$extensionName]->enabled = CHANDLER_ROOT_CONF["extensions"]["allEnabled"];
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!CHANDLER_ROOT_CONF["extensions"]["allEnabled"]) {
|
if(!CHANDLER_ROOT_CONF["extensions"]["allEnabled"]) {
|
||||||
foreach(Finder::find("*")->in(CHANDLER_EXTENSIONS_ENABLED) as $directory) { #findDirectories doesn't work with symlinks
|
foreach(Finder::find("*")->in(CHANDLER_EXTENSIONS_ENABLED) as $directory) { #findDirectories doesn't work with symlinks
|
||||||
if(!is_dir($directory->getRealPath())) continue;
|
if(!is_dir($directory->getRealPath())) continue;
|
||||||
|
|
||||||
$extension = $directory->getFilename();
|
$extension = $directory->getFilename();
|
||||||
|
|
||||||
if(!array_key_exists($extension, $this->extensions)) {
|
if(!array_key_exists($extension, $this->extensions)) {
|
||||||
trigger_error("Extension $extension is enabled, but not available, skipping", E_USER_WARNING);
|
trigger_error("Extension $extension is enabled, but not available, skipping", E_USER_WARNING);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->extensions[$extension]->enabled = true;
|
$this->extensions[$extension]->enabled = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!array_key_exists(CHANDLER_ROOT_CONF["rootApp"], $this->extensions) || !$this->extensions[CHANDLER_ROOT_CONF["rootApp"]]->enabled) {
|
if(!array_key_exists(CHANDLER_ROOT_CONF["rootApp"], $this->extensions) || !$this->extensions[CHANDLER_ROOT_CONF["rootApp"]]->enabled) {
|
||||||
trigger_error("Selected root app is not available", E_USER_ERROR);
|
trigger_error("Selected root app is not available", E_USER_ERROR);
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->rootApp = CHANDLER_ROOT_CONF["rootApp"];
|
$this->rootApp = CHANDLER_ROOT_CONF["rootApp"];
|
||||||
$this->eventLoop = EventDispatcher::i();
|
$this->router = Router::getInstance();
|
||||||
$this->router = Router::i();
|
|
||||||
|
|
||||||
$this->init();
|
$this->init();
|
||||||
}
|
}
|
||||||
|
|
||||||
private function init(): void
|
private function init(): void
|
||||||
{
|
{
|
||||||
foreach($this->getExtensions(true) as $name => $configuration) {
|
foreach($this->getExtensions(true) as $name => $configuration) {
|
||||||
|
@ -73,60 +71,60 @@ class ExtensionManager
|
||||||
|
|
||||||
include_once CHANDLER_EXTENSIONS_ENABLED . \"/\" . str_replace(\"\\\\\", \"/\", \$class) . \".php\";
|
include_once CHANDLER_EXTENSIONS_ENABLED . \"/\" . str_replace(\"\\\\\", \"/\", \$class) . \".php\";
|
||||||
"));
|
"));
|
||||||
|
|
||||||
define(str_replace("-", "_", mb_strtoupper($name)) . "_ROOT", CHANDLER_EXTENSIONS_ENABLED . "/$name", false);
|
define(str_replace("-", "_", mb_strtoupper($name)) . "_ROOT", CHANDLER_EXTENSIONS_ENABLED . "/$name", false);
|
||||||
define(str_replace("-", "_", mb_strtoupper($name)) . "_ROOT_CONF", chandler_parse_yaml(CHANDLER_EXTENSIONS_ENABLED . "/$name/$name.yml"), false);
|
define(str_replace("-", "_", mb_strtoupper($name)) . "_ROOT_CONF", chandler_parse_yaml(CHANDLER_EXTENSIONS_ENABLED . "/$name/$name.yml"), false);
|
||||||
|
|
||||||
if(isset($configuration->init)) {
|
if(isset($configuration->init)) {
|
||||||
$init = require(CHANDLER_EXTENSIONS_ENABLED . "/$name/" . $configuration->init);
|
$init = require(CHANDLER_EXTENSIONS_ENABLED . "/$name/" . $configuration->init);
|
||||||
if(is_callable($init))
|
if(is_callable($init))
|
||||||
$init();
|
$init();
|
||||||
}
|
}
|
||||||
|
|
||||||
if(is_dir($hooks = CHANDLER_EXTENSIONS_ENABLED . "/$name/Hooks")) {
|
if(is_dir($hooks = CHANDLER_EXTENSIONS_ENABLED . "/$name/Hooks")) {
|
||||||
foreach(Finder::findFiles("*Hook.php")->in($hooks) as $hookFile) {
|
foreach(Finder::findFiles("*Hook.php")->in($hooks) as $hookFile) {
|
||||||
$hookClassName = "$name\\Hooks\\" . str_replace(".php", "", end(explode("/", $hookFile)));
|
$hookClassName = "$name\\Hooks\\" . str_replace(".php", "", end(explode("/", $hookFile)));
|
||||||
$hook = new $hookClassName;
|
$hook = new $hookClassName;
|
||||||
|
|
||||||
$this->eventLoop->addListener($hook);
|
$this->eventLoop->addListener($hook);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(is_dir($app = CHANDLER_EXTENSIONS_ENABLED . "/$name/Web")) #"app" means "web app", thus variable is called $app
|
if(is_dir($app = CHANDLER_EXTENSIONS_ENABLED . "/$name/Web")) #"app" means "web app", thus variable is called $app
|
||||||
$this->router->readRoutes("$app/routes.yml", $name, $this->rootApp !== $name);
|
$this->router->readRoutes("$app/routes.yml", $name, $this->rootApp !== $name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getExtensions(bool $onlyEnabled = false): array
|
function getExtensions(bool $onlyEnabled = false): array
|
||||||
{
|
{
|
||||||
return $onlyEnabled
|
return $onlyEnabled
|
||||||
? array_filter($this->extensions, function($e) { return $e->enabled; })
|
? array_filter($this->extensions, function($e) { return $e->enabled; })
|
||||||
: $this->extensions;
|
: $this->extensions;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getExtension(string $name): ?object
|
function getExtension(string $name): ?object
|
||||||
{
|
{
|
||||||
return @$this->extensions[$name];
|
return @$this->extensions[$name];
|
||||||
}
|
}
|
||||||
|
|
||||||
function disableExtension(string $name): void
|
function disableExtension(string $name): void
|
||||||
{
|
{
|
||||||
if(!array_key_exists($name, $this->getExtensions(true))) return;
|
if(!array_key_exists($name, $this->getExtensions(true))) return;
|
||||||
|
|
||||||
if(!unlink(CHANDLER_EXTENSIONS_ENABLED . "/$name")) throw new \Exception("Could not disable extension");
|
if(!unlink(CHANDLER_EXTENSIONS_ENABLED . "/$name")) throw new \Exception("Could not disable extension");
|
||||||
}
|
}
|
||||||
|
|
||||||
function enableExtension(string $name): void
|
function enableExtension(string $name): void
|
||||||
{
|
{
|
||||||
if(CHANDLER_ROOT_CONF["extensions"]["allEnabled"]) return;
|
if(CHANDLER_ROOT_CONF["extensions"]["allEnabled"]) return;
|
||||||
|
|
||||||
if(array_key_exists($name, $this->getExtensions(true))) return;
|
if(array_key_exists($name, $this->getExtensions(true))) return;
|
||||||
|
|
||||||
$path = CHANDLER_EXTENSIONS_AVAILABLE . "/$name";
|
$path = CHANDLER_EXTENSIONS_AVAILABLE . "/$name";
|
||||||
if(!is_dir($path)) throw new \Exception("Extension doesn't exist");
|
if(!is_dir($path)) throw new \Exception("Extension doesn't exist");
|
||||||
|
|
||||||
if(!symlink($path, str_replace("available", "enabled", $path))) throw new \Exception("Could not enable extension");
|
if(!symlink($path, str_replace("available", "enabled", $path))) throw new \Exception("Could not enable extension");
|
||||||
}
|
}
|
||||||
|
|
||||||
use TSimpleSingleton;
|
use TSimpleSingleton;
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,6 @@ declare(strict_types = 1);
|
||||||
|
|
||||||
namespace Chandler\MVC\Routing;
|
namespace Chandler\MVC\Routing;
|
||||||
|
|
||||||
use Chandler\Eventing\EventDispatcher;
|
|
||||||
use Chandler\MVC\Exceptions\InterruptedException;
|
use Chandler\MVC\Exceptions\InterruptedException;
|
||||||
use Chandler\MVC\IPresenter;
|
use Chandler\MVC\IPresenter;
|
||||||
use Chandler\Patterns\TSimpleSingleton;
|
use Chandler\Patterns\TSimpleSingleton;
|
||||||
|
@ -199,7 +198,7 @@ class Router
|
||||||
{
|
{
|
||||||
$key = hash("snefru", CHANDLER_ROOT_CONF["security"]["secret"] . bin2hex($nonce));
|
$key = hash("snefru", CHANDLER_ROOT_CONF["security"]["secret"] . bin2hex($nonce));
|
||||||
$data = $route->namespace;
|
$data = $route->namespace;
|
||||||
$data .= Session::i()->get("tok", -1);
|
$data .= Session::getInstance()->get("tok", -1);
|
||||||
return hash_hmac("snefru", $data, $key) . "#" . bin2hex($nonce);
|
return hash_hmac("snefru", $data, $key) . "#" . bin2hex($nonce);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -257,10 +256,5 @@ class Router
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function __construct()
|
|
||||||
{
|
|
||||||
$this->events = EventDispatcher::i();
|
|
||||||
}
|
|
||||||
|
|
||||||
use TSimpleSingleton;
|
use TSimpleSingleton;
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,22 +10,22 @@ abstract class SimplePresenter implements IPresenter
|
||||||
const REDIRECT_TEMPORARY = 2;
|
const REDIRECT_TEMPORARY = 2;
|
||||||
const REDIRECT_PERMAMENT_PRESISTENT = 8;
|
const REDIRECT_PERMAMENT_PRESISTENT = 8;
|
||||||
const REDIRECT_TEMPORARY_PRESISTENT = 7;
|
const REDIRECT_TEMPORARY_PRESISTENT = 7;
|
||||||
|
|
||||||
protected $mmReader;
|
protected $mmReader;
|
||||||
protected $template;
|
protected $template;
|
||||||
protected $errorTemplate = NULL;
|
protected $errorTemplate = NULL;
|
||||||
|
|
||||||
function __construct()
|
function __construct()
|
||||||
{
|
{
|
||||||
$this->template = (object) [];
|
$this->template = (object) [];
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTemplatingEngine(): TemplatingEngine
|
function getTemplatingEngine(): TemplatingEngine
|
||||||
{
|
{
|
||||||
$latte = new TemplatingEngine;
|
$latte = new TemplatingEngine;
|
||||||
$macros = new \Latte\Macros\MacroSet($latte->getCompiler());
|
$macros = new \Latte\Macros\MacroSet($latte->getCompiler());
|
||||||
$latte->setTempDirectory(CHANDLER_ROOT . "/tmp/cache/templates");
|
$latte->setTempDirectory(CHANDLER_ROOT . "/tmp/cache/templates");
|
||||||
|
|
||||||
$macros->addMacro("css", '
|
$macros->addMacro("css", '
|
||||||
$domain = "' . explode("\\", static::class)[0] . '";
|
$domain = "' . explode("\\", static::class)[0] . '";
|
||||||
$file = (%node.array)[0];
|
$file = (%node.array)[0];
|
||||||
|
@ -62,18 +62,18 @@ abstract class SimplePresenter implements IPresenter
|
||||||
echo "<!-- Inclusion complete -->";
|
echo "<!-- Inclusion complete -->";
|
||||||
'
|
'
|
||||||
);
|
);
|
||||||
|
|
||||||
return $latte;
|
return $latte;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function throwError(int $code = 400, string $desc = "Bad Request", string $message = ""): void
|
protected function throwError(int $code = 400, string $desc = "Bad Request", string $message = ""): void
|
||||||
{
|
{
|
||||||
if(!is_null($this->errorTemplate)) {
|
if(!is_null($this->errorTemplate)) {
|
||||||
header("HTTP/1.0 $code $desc");
|
header("HTTP/1.0 $code $desc");
|
||||||
|
|
||||||
$ext = explode("\\", get_class($this))[0];
|
$ext = explode("\\", get_class($this))[0];
|
||||||
$path = CHANDLER_EXTENSIONS_ENABLED . "/$ext/Web/Presenters/templates/" . $this->errorTemplate . ".xml";
|
$path = CHANDLER_EXTENSIONS_ENABLED . "/$ext/Web/Presenters/templates/" . $this->errorTemplate . ".xml";
|
||||||
|
|
||||||
$latte = $this->getTemplatingEngine();
|
$latte = $this->getTemplatingEngine();
|
||||||
$latte->render($path, array_merge_recursive([
|
$latte->render($path, array_merge_recursive([
|
||||||
"code" => $code,
|
"code" => $code,
|
||||||
|
@ -85,18 +85,18 @@ abstract class SimplePresenter implements IPresenter
|
||||||
chandler_http_panic($code, $desc, $message);
|
chandler_http_panic($code, $desc, $message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function assertNoCSRF(): void
|
protected function assertNoCSRF(): void
|
||||||
{
|
{
|
||||||
if(!$GLOBALS["csrfCheck"])
|
if(!$GLOBALS["csrfCheck"])
|
||||||
$this->throwError(400, "Bad Request", "CSRF token is missing or invalid.");
|
$this->throwError(400, "Bad Request", "CSRF token is missing or invalid.");
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function terminate(): void
|
protected function terminate(): void
|
||||||
{
|
{
|
||||||
throw new Exceptions\InterruptedException;
|
throw new Exceptions\InterruptedException;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function notFound(): void
|
protected function notFound(): void
|
||||||
{
|
{
|
||||||
$this->throwError(
|
$this->throwError(
|
||||||
|
@ -105,51 +105,51 @@ abstract class SimplePresenter implements IPresenter
|
||||||
"The resource you are looking for has been deleted, had its name changed or doesn't exist."
|
"The resource you are looking for has been deleted, had its name changed or doesn't exist."
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function getCaller(): string
|
protected function getCaller(): string
|
||||||
{
|
{
|
||||||
return $GLOBALS["parentModule"] ?? "libchandler:absolute.0";
|
return $GLOBALS["parentModule"] ?? "libchandler:absolute.0";
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function redirect(string $location, int $code = 2): void
|
protected function redirect(string $location, int $code = 2): void
|
||||||
{
|
{
|
||||||
$code = 300 + $code;
|
$code = 300 + $code;
|
||||||
if(($code <=> 300) !== 0 && $code > 399) return;
|
if(($code <=> 300) !== 0 && $code > 399) return;
|
||||||
|
|
||||||
header("HTTP/1.1 $code");
|
header("HTTP/1.1 $code");
|
||||||
header("Location: $location");
|
header("Location: $location");
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function pass(string $to, ...$args): void
|
protected function pass(string $to, ...$args): void
|
||||||
{
|
{
|
||||||
$args = array_merge([$to], $args);
|
$args = array_merge([$to], $args);
|
||||||
$router = \Chandler\MVC\Routing\Router::i();
|
$router = \Chandler\MVC\Routing\Router::getInstance();
|
||||||
$__out = $router->execute($router->reverse(...$args), "libchandler:absolute.0");
|
$__out = $router->execute($router->reverse(...$args), "libchandler:absolute.0");
|
||||||
exit($__out);
|
exit($__out);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function sendmail(string $to, string $template, array $params): void
|
protected function sendmail(string $to, string $template, array $params): void
|
||||||
{
|
{
|
||||||
$emailDir = pathinfo($template, PATHINFO_DIRNAME);
|
$emailDir = pathinfo($template, PATHINFO_DIRNAME);
|
||||||
$template .= ".eml.latte";
|
$template .= ".eml.latte";
|
||||||
|
|
||||||
$renderedHTML = (new TemplatingEngine)->renderToString($template, $params);
|
$renderedHTML = (new TemplatingEngine)->renderToString($template, $params);
|
||||||
$document = new \DOMDocument();
|
$document = new \DOMDocument();
|
||||||
$document->loadHTML($renderedHTML, LIBXML_NOEMPTYTAG);
|
$document->loadHTML($renderedHTML, LIBXML_NOEMPTYTAG);
|
||||||
$querySel = new \DOMXPath($document);
|
$querySel = new \DOMXPath($document);
|
||||||
|
|
||||||
$subject = $querySel->query("//title/text()")->item(0)->data;
|
$subject = $querySel->query("//title/text()")->item(0)->data;
|
||||||
|
|
||||||
foreach($querySel->query("//link[@rel='stylesheet']") as $link) {
|
foreach($querySel->query("//link[@rel='stylesheet']") as $link) {
|
||||||
$style = $document->createElement("style");
|
$style = $document->createElement("style");
|
||||||
$style->setAttribute("id", uniqid("mail", true));
|
$style->setAttribute("id", uniqid("mail", true));
|
||||||
$style->appendChild(new \DOMText(file_get_contents("$emailDir/assets/css/" . $link->getAttribute("href"))));
|
$style->appendChild(new \DOMText(file_get_contents("$emailDir/assets/css/" . $link->getAttribute("href"))));
|
||||||
|
|
||||||
$link->parentNode->appendChild($style);
|
$link->parentNode->appendChild($style);
|
||||||
$link->parentNode->removeChild($link);
|
$link->parentNode->removeChild($link);
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach($querySel->query("//img") as $image) {
|
foreach($querySel->query("//img") as $image) {
|
||||||
$imagePath = "$emailDir/assets/res/" . $image->getAttribute("src");
|
$imagePath = "$emailDir/assets/res/" . $image->getAttribute("src");
|
||||||
$type = pathinfo($imagePath, PATHINFO_EXTENSION);
|
$type = pathinfo($imagePath, PATHINFO_EXTENSION);
|
||||||
|
@ -157,36 +157,36 @@ abstract class SimplePresenter implements IPresenter
|
||||||
|
|
||||||
$image->setAttribute("src", "data:image/$type;base64,$contents");
|
$image->setAttribute("src", "data:image/$type;base64,$contents");
|
||||||
}
|
}
|
||||||
|
|
||||||
\Chandler\Email\Email::send($to, $subject, $document->saveHTML());
|
\Chandler\Email\Email::send($to, $subject, $document->saveHTML());
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function queryParam(string $index): ?string
|
protected function queryParam(string $index): ?string
|
||||||
{
|
{
|
||||||
return $_GET[$index] ?? NULL;
|
return $_GET[$index] ?? NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function postParam(string $index, bool $csrfCheck = true): ?string
|
protected function postParam(string $index, bool $csrfCheck = true): ?string
|
||||||
{
|
{
|
||||||
if($csrfCheck)
|
if($csrfCheck)
|
||||||
$this->assertNoCSRF();
|
$this->assertNoCSRF();
|
||||||
|
|
||||||
return $_POST[$index] ?? NULL;
|
return $_POST[$index] ?? NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function requestParam(string $index): ?string
|
protected function requestParam(string $index): ?string
|
||||||
{
|
{
|
||||||
return $_REQUEST[$index] ?? NULL;
|
return $_REQUEST[$index] ?? NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function jsonParam(string $index): ?string
|
protected function jsonParam(string $index): ?string
|
||||||
{
|
{
|
||||||
if(!isset($GLOBALS["jsonInputCache"]))
|
if(!isset($GLOBALS["jsonInputCache"]))
|
||||||
$GLOBALS["jsonInputCache"] = json_decode($this->queryParam("js") ?? file_get_contents("php://input"), true);
|
$GLOBALS["jsonInputCache"] = json_decode($this->queryParam("js") ?? file_get_contents("php://input"), true);
|
||||||
|
|
||||||
return $GLOBALS["jsonInputCache"][$index] ?? NULL;
|
return $GLOBALS["jsonInputCache"][$index] ?? NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function findParam(string $index, array $searchIn = ["json", "post", "query"]): ?string
|
protected function findParam(string $index, array $searchIn = ["json", "post", "query"]): ?string
|
||||||
{
|
{
|
||||||
$res = NULL;
|
$res = NULL;
|
||||||
|
@ -198,38 +198,38 @@ abstract class SimplePresenter implements IPresenter
|
||||||
else if($search === "query")
|
else if($search === "query")
|
||||||
$res = $this->queryParam($index) ?? $res;
|
$res = $this->queryParam($index) ?? $res;
|
||||||
}
|
}
|
||||||
|
|
||||||
return $res;
|
return $res;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function checkbox(string $name): bool
|
protected function checkbox(string $name): bool
|
||||||
{
|
{
|
||||||
return ($this->postParam($name) ?? "off") === "on";
|
return ($this->postParam($name) ?? "off") === "on";
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTemplateScope(): array
|
function getTemplateScope(): array
|
||||||
{
|
{
|
||||||
return (array) $this->template;
|
return (array) $this->template;
|
||||||
}
|
}
|
||||||
|
|
||||||
function onStartup(): void
|
function onStartup(): void
|
||||||
{
|
{
|
||||||
date_default_timezone_set("UTC");
|
date_default_timezone_set("UTC");
|
||||||
}
|
}
|
||||||
|
|
||||||
function onBeforeRender(): void
|
function onBeforeRender(): void
|
||||||
{
|
{
|
||||||
$this->template->csrfToken = $GLOBALS["csrfToken"];
|
$this->template->csrfToken = $GLOBALS["csrfToken"];
|
||||||
}
|
}
|
||||||
|
|
||||||
function onAfterRender(): void
|
function onAfterRender(): void
|
||||||
{}
|
{}
|
||||||
|
|
||||||
function onStop(): void
|
function onStop(): void
|
||||||
{}
|
{}
|
||||||
|
|
||||||
function onDestruction(): void
|
function onDestruction(): void
|
||||||
{}
|
{}
|
||||||
|
|
||||||
use SmartObject;
|
use SmartObject;
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,7 +17,7 @@ trait TSimpleSingleton
|
||||||
/**
|
/**
|
||||||
* @return static
|
* @return static
|
||||||
*/
|
*/
|
||||||
public static function i(): self
|
public static function getInstance(): self
|
||||||
{
|
{
|
||||||
if (is_null(static::$instance)) {
|
if (is_null(static::$instance)) {
|
||||||
return static::$instance = new static();
|
return static::$instance = new static();
|
||||||
|
|
|
@ -8,18 +8,18 @@ class Authenticator
|
||||||
{
|
{
|
||||||
private $db;
|
private $db;
|
||||||
private $session;
|
private $session;
|
||||||
|
|
||||||
private function __construct()
|
private function __construct()
|
||||||
{
|
{
|
||||||
$this->db = DatabaseConnection::i()->getContext();
|
$this->db = DatabaseConnection::i()->getContext();
|
||||||
$this->session = Session::i();
|
$this->session = Session::getInstance();
|
||||||
}
|
}
|
||||||
|
|
||||||
private function verifySuRights(string $uId): bool
|
private function verifySuRights(string $uId): bool
|
||||||
{
|
{
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function makeToken(string $user, string $ip, string $ua): string
|
private function makeToken(string $user, string $ip, string $ua): string
|
||||||
{
|
{
|
||||||
$data = ["user" => $user, "ip" => $ip, "ua" => $ua];
|
$data = ["user" => $user, "ip" => $ip, "ua" => $ua];
|
||||||
|
@ -27,15 +27,15 @@ class Authenticator
|
||||||
->table("ChandlerTokens")
|
->table("ChandlerTokens")
|
||||||
->where($data)
|
->where($data)
|
||||||
->fetch();
|
->fetch();
|
||||||
|
|
||||||
if(!$token) {
|
if(!$token) {
|
||||||
$this->db->table("ChandlerTokens")->insert($data);
|
$this->db->table("ChandlerTokens")->insert($data);
|
||||||
$token = $this->db->table("ChandlerTokens")->where($data)->fetch();
|
$token = $this->db->table("ChandlerTokens")->where($data)->fetch();
|
||||||
}
|
}
|
||||||
|
|
||||||
return $token->token;
|
return $token->token;
|
||||||
}
|
}
|
||||||
|
|
||||||
static function verifyHash(string $input, string $hash): bool
|
static function verifyHash(string $input, string $hash): bool
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
|
@ -54,46 +54,46 @@ class Authenticator
|
||||||
} catch(\SodiumException $ex) {
|
} catch(\SodiumException $ex) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getUser(): ?User
|
function getUser(): ?User
|
||||||
{
|
{
|
||||||
$token = $this->session->get("tok");
|
$token = $this->session->get("tok");
|
||||||
if(!$token) return null;
|
if(!$token) return null;
|
||||||
|
|
||||||
$token = $this->db
|
$token = $this->db
|
||||||
->table("ChandlerTokens")
|
->table("ChandlerTokens")
|
||||||
->where([
|
->where([
|
||||||
"token" => $token,
|
"token" => $token,
|
||||||
])
|
])
|
||||||
->fetch();
|
->fetch();
|
||||||
|
|
||||||
if(!$token) return null;
|
if(!$token) return null;
|
||||||
|
|
||||||
$checksPassed = false;
|
$checksPassed = false;
|
||||||
if(CHANDLER_ROOT_CONF["security"]["extendedValidation"])
|
if(CHANDLER_ROOT_CONF["security"]["extendedValidation"])
|
||||||
$checksPassed = $token->ip === CONNECTING_IP && $token->ua === $_SERVER["HTTP_USER_AGENT"];
|
$checksPassed = $token->ip === CONNECTING_IP && $token->ua === $_SERVER["HTTP_USER_AGENT"];
|
||||||
else
|
else
|
||||||
$checksPassed = true;
|
$checksPassed = true;
|
||||||
|
|
||||||
if($checksPassed) {
|
if($checksPassed) {
|
||||||
$su = $this->session->get("_su");
|
$su = $this->session->get("_su");
|
||||||
$user = $this->db->table("ChandlerUsers")->get($su ?? $token->user);
|
$user = $this->db->table("ChandlerUsers")->get($su ?? $token->user);
|
||||||
if(!$user) return null;
|
if(!$user) return null;
|
||||||
|
|
||||||
return new User($user, !is_null($su));
|
return new User($user, !is_null($su));
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function authenticate(string $user): void
|
function authenticate(string $user): void
|
||||||
{
|
{
|
||||||
$this->session->set("tok", $this->makeToken($user, CONNECTING_IP, $_SERVER["HTTP_USER_AGENT"]));
|
$this->session->set("tok", $this->makeToken($user, CONNECTING_IP, $_SERVER["HTTP_USER_AGENT"]));
|
||||||
}
|
}
|
||||||
|
|
||||||
function verifyCredentials(string $id, string $password): bool
|
function verifyCredentials(string $id, string $password): bool
|
||||||
{
|
{
|
||||||
$user = $this->db->table("ChandlerUsers")->get($id);
|
$user = $this->db->table("ChandlerUsers")->get($id);
|
||||||
|
@ -101,30 +101,30 @@ class Authenticator
|
||||||
return false;
|
return false;
|
||||||
else if(!$this->verifyHash($password, $user->passwordHash))
|
else if(!$this->verifyHash($password, $user->passwordHash))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function login(string $id, string $password): bool
|
function login(string $id, string $password): bool
|
||||||
{
|
{
|
||||||
if(!$this->verifyCredentials($id, $password))
|
if(!$this->verifyCredentials($id, $password))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
$this->authenticate($id);
|
$this->authenticate($id);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function logout(bool $revoke = false): bool
|
function logout(bool $revoke = false): bool
|
||||||
{
|
{
|
||||||
$token = $this->session->get("tok");
|
$token = $this->session->get("tok");
|
||||||
if(!$token) return false;
|
if(!$token) return false;
|
||||||
|
|
||||||
if($revoke) $this->db->table("ChandlerTokens")->where("id", $token)->delete();
|
if($revoke) $this->db->table("ChandlerTokens")->where("id", $token)->delete();
|
||||||
|
|
||||||
$this->session->set("tok", NULL);
|
$this->session->set("tok", NULL);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
use TSimpleSingleton;
|
use TSimpleSingleton;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
{
|
{
|
||||||
"autoload": {
|
"autoload": {
|
||||||
"classmap": [
|
"classmap": [
|
||||||
"chandler/Eventing/Events/Event.php"
|
"chandler/Patterns/TSimpleSingleton.php"
|
||||||
]
|
],
|
||||||
|
"psr-4": {
|
||||||
|
"Chandler\\": "src"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"autoload-dev": {
|
"autoload-dev": {
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
|
@ -14,6 +17,7 @@
|
||||||
"minimum-stability": "stable",
|
"minimum-stability": "stable",
|
||||||
"name": "openvk/chanlder",
|
"name": "openvk/chanlder",
|
||||||
"require": {
|
"require": {
|
||||||
|
"ext-yaml": "*",
|
||||||
"php": "^7.3",
|
"php": "^7.3",
|
||||||
"nette/utils": "^3.0",
|
"nette/utils": "^3.0",
|
||||||
"nette/di": "^3.0",
|
"nette/di": "^3.0",
|
||||||
|
|
|
@ -14,10 +14,10 @@ server {
|
||||||
listen [::]:443 ssl http2;
|
listen [::]:443 ssl http2;
|
||||||
server_name domain.tld;
|
server_name domain.tld;
|
||||||
|
|
||||||
root /opt/chandler/htdocs;
|
root /opt/chandler/public;
|
||||||
|
|
||||||
client_max_body_size 100m;
|
client_max_body_size 100m;
|
||||||
|
|
||||||
index index.php;
|
index index.php;
|
||||||
|
|
||||||
ssl_certificate /path/to/fullchain.pem;
|
ssl_certificate /path/to/fullchain.pem;
|
||||||
|
|
|
@ -3,6 +3,8 @@
|
||||||
<coverage cacheDirectory="tests/cache" processUncoveredFiles="true">
|
<coverage cacheDirectory="tests/cache" processUncoveredFiles="true">
|
||||||
<include>
|
<include>
|
||||||
<file>chandler/Eventing/Events/Event.php</file>
|
<file>chandler/Eventing/Events/Event.php</file>
|
||||||
|
<file>chandler/Eventing/EventDispatcher.php</file>
|
||||||
|
<file>chandler/Patterns/TSimpleSingleton.php</file>
|
||||||
</include>
|
</include>
|
||||||
</coverage>
|
</coverage>
|
||||||
<testsuites>
|
<testsuites>
|
||||||
|
|
|
@ -1,5 +1,9 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
declare(strict_types = 1);
|
declare(strict_types = 1);
|
||||||
|
|
||||||
|
require_once(dirname(__DIR__) . "/vendor/autoload.php");
|
||||||
|
|
||||||
$bootstrap = require("../chandler/Bootstrap.php");
|
$bootstrap = require("../chandler/Bootstrap.php");
|
||||||
|
|
||||||
$bootstrap->ignite();
|
$bootstrap->ignite();
|
||||||
|
|
|
@ -1,71 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types = 1);
|
|
||||||
|
|
||||||
namespace Chandler\Tests\Chandler\Eventing;
|
|
||||||
|
|
||||||
use Chandler\Eventing\EventDispatcher;
|
|
||||||
use Chandler\Eventing\Events\Event;
|
|
||||||
use PHPUnit\Framework\TestCase;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @package Chandler\Tests\Chandler\Eventing
|
|
||||||
*/
|
|
||||||
class EventDispatcherTest extends TestCase
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
public function provideMethodAddListener(): array // IMPROVE: Add more values.
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
[
|
|
||||||
null,
|
|
||||||
],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
public function provideMethodPushEvent(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
[
|
|
||||||
new Event(),
|
|
||||||
],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @dataProvider provideMethodAddListener
|
|
||||||
*
|
|
||||||
* @param mixed $hook
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function testMethodAddListener($hook): void
|
|
||||||
{
|
|
||||||
$this->assertTrue(EventDispatcher::i()->addListener($hook));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function testMethodI(): void
|
|
||||||
{
|
|
||||||
$this->assertSame(EventDispatcher::i(), EventDispatcher::i());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @dataProvider provideMethodPushEvent
|
|
||||||
*
|
|
||||||
* @param \Chandler\Eventing\Events\Event $event
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function testMethodPushEvent(Event $event): void
|
|
||||||
{
|
|
||||||
$this->assertSame($event, EventDispatcher::i()->pushEvent($event));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,46 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types = 1);
|
|
||||||
|
|
||||||
namespace Chandler\Tests\Chandler\Eventing\Events;
|
|
||||||
|
|
||||||
use Chandler\Eventing\Events\Event;
|
|
||||||
use PHPUnit\Framework\TestCase;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @package Chandler\Tests\Chandler\Eventing\Events
|
|
||||||
*/
|
|
||||||
class EventTest extends TestCase
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function testPropertyCode(): void
|
|
||||||
{
|
|
||||||
$this->assertClassHasAttribute("code", Event::class);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function testPropertyData(): void
|
|
||||||
{
|
|
||||||
$this->assertClassHasAttribute("data", Event::class);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function testPropertyPristine(): void
|
|
||||||
{
|
|
||||||
$this->assertClassHasAttribute("pristine", Event::class);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function testPropertyTime(): void
|
|
||||||
{
|
|
||||||
$this->assertClassHasAttribute("time", Event::class);
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in a new issue