commit
c8c58fc72b
26 changed files with 2403 additions and 0 deletions
@ -0,0 +1,5 @@ |
|||||||
|
.idea |
||||||
|
.vscode |
||||||
|
.DS_Store |
||||||
|
|
||||||
|
vendor |
@ -0,0 +1,21 @@ |
|||||||
|
# The MIT License |
||||||
|
|
||||||
|
Copyright (C) 2024, MikO <miko@massivedynamic.eu> |
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of |
||||||
|
this software and associated documentation files (the “Software”), to deal in |
||||||
|
the Software without restriction, including without limitation the rights to |
||||||
|
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies |
||||||
|
of the Software, and to permit persons to whom the Software is furnished to do |
||||||
|
so, subject to the following conditions: |
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all |
||||||
|
copies or substantial portions of the Software. |
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
||||||
|
SOFTWARE. |
@ -0,0 +1,26 @@ |
|||||||
|
{ |
||||||
|
"name": "massivedynamic/lucidity", |
||||||
|
"description": "a lightweight php framework", |
||||||
|
"type": "library", |
||||||
|
"version" : "0.0.1", |
||||||
|
"license": "MIT", |
||||||
|
"autoload": { |
||||||
|
"psr-4": { |
||||||
|
"massivedynamic\\lucidity\\": "src/" |
||||||
|
} |
||||||
|
}, |
||||||
|
"authors": [ |
||||||
|
{ |
||||||
|
"name": "MikO", |
||||||
|
"email": "miko@massivedymamic.eu" |
||||||
|
} |
||||||
|
], |
||||||
|
"require": { |
||||||
|
"robmorgan/phinx": "^0.16.2", |
||||||
|
"mustache/mustache": "^2.14", |
||||||
|
"psr/container": "^2.0" |
||||||
|
}, |
||||||
|
"require-dev": { |
||||||
|
"phpstan/phpstan": "^1.11" |
||||||
|
} |
||||||
|
} |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,103 @@ |
|||||||
|
<?php declare(strict_types=1); |
||||||
|
|
||||||
|
namespace massivedynamic\lucidity { |
||||||
|
|
||||||
|
use Exception; |
||||||
|
use Throwable; |
||||||
|
|
||||||
|
use massivedynamic\lucidity\TemplateEngine; |
||||||
|
use massivedynamic\lucidity\Utils; |
||||||
|
use massivedynamic\lucidity\containers\ServiceContainer; |
||||||
|
use massivedynamic\lucidity\services\Router; |
||||||
|
use massivedynamic\lucidity\services\Service; |
||||||
|
|
||||||
|
abstract class Application { |
||||||
|
protected static ?Application $App = null; |
||||||
|
|
||||||
|
/** @var array<string, mixed> */ |
||||||
|
protected array $globals; |
||||||
|
protected TemplateEngine $templateEngine; |
||||||
|
protected ServiceContainer $serviceContainer; |
||||||
|
protected Router|Service $router; |
||||||
|
|
||||||
|
public function __construct(private string $templatesPath, string $partialsPath, string $cachePath) { |
||||||
|
set_exception_handler(fn(Throwable $exception) => $this->handleException($exception)); |
||||||
|
|
||||||
|
if (self::$App instanceof Application) |
||||||
|
throw new Exception("Only one instance of `Application` is possible"); |
||||||
|
session_start(); |
||||||
|
self::$App = $this; |
||||||
|
$this->globals = [ |
||||||
|
"errors" => [] |
||||||
|
]; |
||||||
|
$this->templateEngine = new TemplateEngine($this, $templatesPath, $partialsPath, $cachePath); |
||||||
|
$this->serviceContainer = new ServiceContainer($this); |
||||||
|
$this->router = $this->serviceContainer->get(Router::class); |
||||||
|
} |
||||||
|
|
||||||
|
public function fetchService(string $id) : Service | Application { |
||||||
|
return $this->serviceContainer->get($id); |
||||||
|
} |
||||||
|
|
||||||
|
public function publishGlobal(string $key, mixed $value) : void { |
||||||
|
if ($key === "errors") |
||||||
|
throw new Exception("The 'errors' global can not be accessed directly. Use `Application::addError()` instead."); |
||||||
|
$this->globals[$key] = $value; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @param array<string> | string | null $errors |
||||||
|
*/ |
||||||
|
public function addError(array | string | null $errors) : void { |
||||||
|
if (!$errors) |
||||||
|
return; |
||||||
|
if (!is_array($errors)) |
||||||
|
$errors = [$errors]; |
||||||
|
$this->globals["errors"] = array_merge((array)$this->globals["errors"], $errors); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @return array<string, mixed> |
||||||
|
*/ |
||||||
|
public function globals() : array { |
||||||
|
return $this->globals; |
||||||
|
} |
||||||
|
|
||||||
|
private function handleException(Throwable $exception) : void { |
||||||
|
//Utils::Dump($exception->getTrace()); |
||||||
|
|
||||||
|
self::View("LucidityException", [ |
||||||
|
"exception" => $exception, |
||||||
|
"trace" => $exception->getTrace(), |
||||||
|
"exceptionType" => $exception::class, |
||||||
|
"lucidityVersion" => \Composer\InstalledVersions::getVersion("massivedynamic/lucidity"), |
||||||
|
"phpVersion" => phpversion() |
||||||
|
]); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @param string $template |
||||||
|
* @param array<string, mixed> $context |
||||||
|
*/ |
||||||
|
public static function View(string $template, array $context = []) : void { |
||||||
|
self::$App->templateEngine->view($template, $context); |
||||||
|
} |
||||||
|
|
||||||
|
public static function Instance() : Application { |
||||||
|
return self::$App; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
namespace { |
||||||
|
|
||||||
|
/** |
||||||
|
* @param string $template |
||||||
|
* @param array<string, mixed> $context |
||||||
|
*/ |
||||||
|
function view(string $template, array $context = []) : void { |
||||||
|
massivedynamic\lucidity\Application::View($template, $context); |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,10 @@ |
|||||||
|
<?php declare(strict_types=1); |
||||||
|
|
||||||
|
namespace massivedynamic\lucidity; |
||||||
|
|
||||||
|
enum RequestType : string { |
||||||
|
case GET = "GET"; |
||||||
|
case POST = "POST"; |
||||||
|
case PUT = "PUT"; |
||||||
|
case DELETE = "DELETE"; |
||||||
|
} |
@ -0,0 +1,31 @@ |
|||||||
|
<?php declare(strict_types=1); |
||||||
|
|
||||||
|
namespace massivedynamic\lucidity; |
||||||
|
|
||||||
|
use Mustache_Engine; |
||||||
|
use Mustache_Loader_FilesystemLoader; |
||||||
|
use Mustache_Loader_CascadingLoader; |
||||||
|
|
||||||
|
class TemplateEngine extends Mustache_Engine { |
||||||
|
public function __construct(private Application $app, private string $templatesPath, string $partialsPath, string $cachePath) { |
||||||
|
parent::__construct([ |
||||||
|
"cache" => $cachePath, |
||||||
|
"loader" => new Mustache_Loader_CascadingLoader([ |
||||||
|
new Mustache_Loader_FilesystemLoader($templatesPath), |
||||||
|
new Mustache_Loader_FilesystemLoader(dirname(__FILE__)."/../views") |
||||||
|
]), |
||||||
|
"partials_loader" => new Mustache_Loader_CascadingLoader([ |
||||||
|
new Mustache_Loader_FilesystemLoader($partialsPath), |
||||||
|
new Mustache_Loader_FilesystemLoader(dirname(__FILE__)."/../views/partials") |
||||||
|
]), |
||||||
|
"helpers" => [ |
||||||
|
"notEmpty" => fn(array $array) : bool => count($array) > 0 |
||||||
|
], |
||||||
|
"pragmas" => [Mustache_Engine::PRAGMA_BLOCKS, Mustache_Engine::PRAGMA_FILTERS] |
||||||
|
]); |
||||||
|
} |
||||||
|
|
||||||
|
public function view(string $template, array $context = []) : void { |
||||||
|
echo $this->render("$template.mustache", array_merge($this->app->globals(), $context)); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,16 @@ |
|||||||
|
<?php declare(strict_types=1); |
||||||
|
|
||||||
|
namespace massivedynamic\lucidity; |
||||||
|
|
||||||
|
class Utils { |
||||||
|
public static function Dump(mixed $content) : void { |
||||||
|
echo "<pre>"; |
||||||
|
print_r($content); |
||||||
|
echo "</pre>"; |
||||||
|
} |
||||||
|
|
||||||
|
public static function DD(mixed $content) : void { |
||||||
|
self::Dump($content); |
||||||
|
die(); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,57 @@ |
|||||||
|
<?php declare(strict_types=1); |
||||||
|
|
||||||
|
namespace massivedynamic\lucidity\containers; |
||||||
|
|
||||||
|
use Exception; |
||||||
|
use ReflectionClass; |
||||||
|
|
||||||
|
use massivedynamic\lucidity\Application; |
||||||
|
use Psr\Container\ContainerInterface; |
||||||
|
|
||||||
|
abstract class Container implements ContainerInterface { |
||||||
|
protected array $instances; |
||||||
|
|
||||||
|
public function __construct(protected Application $app) { |
||||||
|
$this->instances = []; |
||||||
|
} |
||||||
|
|
||||||
|
public function get(string $id) : mixed { |
||||||
|
if ($id === Application::class || is_subclass_of($id, Application::class)) |
||||||
|
return $this->app; |
||||||
|
if (!$this->has($id)) |
||||||
|
$this->instantiate($id); |
||||||
|
|
||||||
|
return $this->instances[$id]; |
||||||
|
} |
||||||
|
|
||||||
|
public function has(string $id) : bool { |
||||||
|
return array_key_exists($id, $this->instances); |
||||||
|
} |
||||||
|
|
||||||
|
public function emplace(string $type, ...$args) : bool { |
||||||
|
if (array_key_exists($type, $this->instances)) |
||||||
|
return false; |
||||||
|
$this->instances[$type] = new $type(...$args); |
||||||
|
|
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
protected function instantiate(string $id) : void { |
||||||
|
$classInfo = new ReflectionClass($id); |
||||||
|
$constructor = $classInfo->getConstructor(); |
||||||
|
$arguments = []; |
||||||
|
|
||||||
|
if ($constructor === null) { |
||||||
|
$this->instances[$id] = $classInfo->newInstance(); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
foreach ($constructor->getParameters() as $parameter) { |
||||||
|
$type = $parameter->getType(); |
||||||
|
if ($type === null) |
||||||
|
throw new Exception("constructor arguments need to name a type"); |
||||||
|
array_push($arguments, $this->get($type->getName())); |
||||||
|
} |
||||||
|
$this->instances[$id] = $classInfo->newInstanceArgs($arguments); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,12 @@ |
|||||||
|
<?php declare(strict_types=1); |
||||||
|
|
||||||
|
namespace massivedynamic\lucidity\containers; |
||||||
|
|
||||||
|
use massivedynamic\lucidity\containers\Container; |
||||||
|
use massivedynamic\lucidity\controllers\Controller; |
||||||
|
|
||||||
|
class ControllerContainer extends Container { |
||||||
|
public function get(string $id) : Controller { |
||||||
|
return parent::get($id); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,13 @@ |
|||||||
|
<?php declare(strict_types=1); |
||||||
|
|
||||||
|
namespace massivedynamic\lucidity\containers; |
||||||
|
|
||||||
|
use massivedynamic\lucidity\Application; |
||||||
|
use massivedynamic\lucidity\containers\Container; |
||||||
|
use massivedynamic\lucidity\services\Service; |
||||||
|
|
||||||
|
class ServiceContainer extends Container { |
||||||
|
public function get(string $id) : Service | Application { |
||||||
|
return parent::get($id); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,8 @@ |
|||||||
|
<?php declare(strict_types=1); |
||||||
|
|
||||||
|
namespace massivedynamic\lucidity\controllers; |
||||||
|
|
||||||
|
class Controller { |
||||||
|
public function __construct() { |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,12 @@ |
|||||||
|
<?php declare(strict_types=1); |
||||||
|
|
||||||
|
namespace massivedynamic\lucidity\exceptions; |
||||||
|
|
||||||
|
use Exception; |
||||||
|
use Throwable; |
||||||
|
|
||||||
|
class UnknownValidatorException extends Exception { |
||||||
|
public function __construct(string $validator, ?Throwable $previous = null) { |
||||||
|
parent::__construct("Unknown validator '$validator'", 1000, $previous); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,66 @@ |
|||||||
|
<?php declare(strict_types=1); |
||||||
|
|
||||||
|
namespace massivedynamic\lucidity\models; |
||||||
|
|
||||||
|
use ReflectionClass; |
||||||
|
use ReflectionProperty; |
||||||
|
use ReflectionUnionType; |
||||||
|
|
||||||
|
use massivedynamic\lucidity\Application; |
||||||
|
use massivedynamic\lucidity\Utils; |
||||||
|
use massivedynamic\lucidity\models\Model; |
||||||
|
use massivedynamic\lucidity\services\Database; |
||||||
|
use massivedynamic\lucidity\services\Service; |
||||||
|
|
||||||
|
abstract class DatabaseModel extends Model { |
||||||
|
protected static ?string $TableName = null; |
||||||
|
|
||||||
|
protected Service | Database $db; |
||||||
|
|
||||||
|
public function __construct() { |
||||||
|
$this->db = Application::Instance()->fetchService(Database::class); |
||||||
|
} |
||||||
|
|
||||||
|
public function save() { |
||||||
|
$classInfo = new ReflectionClass($this); |
||||||
|
$properties = $classInfo->getProperties(ReflectionProperty::IS_PROTECTED); |
||||||
|
$columns = []; |
||||||
|
$values = []; |
||||||
|
$types = []; |
||||||
|
|
||||||
|
foreach ($properties as $property) { |
||||||
|
if ($property->getType() instanceof ReflectionUnionType || $property->isStatic() || !$property->getType()->isBuiltin()) |
||||||
|
continue; |
||||||
|
|
||||||
|
array_push($columns, $property->name); |
||||||
|
array_push($values, $this->{$property->name}); |
||||||
|
array_push($types, Database::PrimitiveTypeToBindParam($property->getType()->getName())); |
||||||
|
} |
||||||
|
$placeholders = array_fill(0, count($columns), '?'); |
||||||
|
|
||||||
|
$qs = "INSERT INTO ".self::TableName()." (".implode(", ", $columns).") VALUES (".implode(", ", $placeholders).")"; |
||||||
|
$query = $this->db->prepare($qs); |
||||||
|
$query->bind_param(implode("", $types), ...$values); |
||||||
|
} |
||||||
|
|
||||||
|
public function all() : array { |
||||||
|
$result = $this->db->query("SELECT * FROM ".self::TableName()); |
||||||
|
|
||||||
|
$out = []; |
||||||
|
while ($row = $result->fetch_object()) |
||||||
|
array_push($out, $row); |
||||||
|
|
||||||
|
return $out; |
||||||
|
} |
||||||
|
|
||||||
|
private static function TableName() : string { |
||||||
|
return self::$TableName ?? self::Type2TableName(static::class); |
||||||
|
} |
||||||
|
|
||||||
|
private static function Type2TableName(string $type) : string { |
||||||
|
$parts = explode("\\", $type); |
||||||
|
$basename = strtolower(array_pop($parts)); |
||||||
|
|
||||||
|
return substr($basename, -1) === "s" ? $basename : $basename."s"; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,5 @@ |
|||||||
|
<?php declare(strict_types=1); |
||||||
|
|
||||||
|
namespace massivedynamic\lucidity\models; |
||||||
|
|
||||||
|
abstract class Model {} |
@ -0,0 +1,25 @@ |
|||||||
|
<?php declare(strict_types=1); |
||||||
|
|
||||||
|
namespace massivedynamic\lucidity\services; |
||||||
|
|
||||||
|
use mysqli; |
||||||
|
|
||||||
|
use massivedynamic\lucidity\Utils; |
||||||
|
use massivedynamic\lucidity\services\Service; |
||||||
|
|
||||||
|
class Database extends mysqli implements Service { |
||||||
|
public static function PrimitiveTypeToBindParam(string $type) : string { |
||||||
|
switch($type) { |
||||||
|
case "bool": |
||||||
|
return "i"; |
||||||
|
case "string": |
||||||
|
return "s"; |
||||||
|
case "int": |
||||||
|
return "i"; |
||||||
|
case "float": |
||||||
|
return "d"; |
||||||
|
default: |
||||||
|
return "X"; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,57 @@ |
|||||||
|
<?php declare(strict_types=1); |
||||||
|
|
||||||
|
namespace massivedynamic\lucidity\services; |
||||||
|
|
||||||
|
use massivedynamic\lucidity\RequestType; |
||||||
|
use massivedynamic\lucidity\Utils; |
||||||
|
use massivedynamic\lucidity\services\Service; |
||||||
|
use massivedynamic\lucidity\validators\Validator; |
||||||
|
|
||||||
|
class Request implements Service { |
||||||
|
public readonly RequestType $type; |
||||||
|
public readonly string $query; |
||||||
|
/** @var array<string, string> */ |
||||||
|
private readonly array $get; |
||||||
|
/** @var array<string, string> */ |
||||||
|
private readonly array $post; |
||||||
|
|
||||||
|
public function __construct() { |
||||||
|
$queryString = parse_url("/".ltrim($_SERVER["REQUEST_URI"], '/'), PHP_URL_PATH); |
||||||
|
assert($queryString !== false, "unreachable"); |
||||||
|
|
||||||
|
$this->type = RequestType::tryFrom($_SERVER["REQUEST_METHOD"]) ?? RequestType::GET; |
||||||
|
$this->query = !$queryString ? "" : $queryString; |
||||||
|
$this->get = $_GET; |
||||||
|
$this->post = $_POST; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @param array<string, array<string>> $attributes |
||||||
|
*/ |
||||||
|
public function validate(array $attributes) : bool { |
||||||
|
$validator = new Validator($this->all(), $attributes); |
||||||
|
|
||||||
|
return $validator->validate() === null; |
||||||
|
} |
||||||
|
|
||||||
|
public function match(string $pattern) : bool { |
||||||
|
$pattern = "/^".preg_replace("/\//", "\/", $pattern)."$/"; |
||||||
|
|
||||||
|
return preg_match($pattern, $this->query) === 1; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @return array<string, string> |
||||||
|
*/ |
||||||
|
public function all() : array { |
||||||
|
return $this->type === RequestType::POST ? $this->post : $this->get; |
||||||
|
} |
||||||
|
|
||||||
|
public function get(string $key, ?string $defaultValue = null) : ?string { |
||||||
|
return isset($this->get[$key]) ? $this->get[$key] : $defaultValue; |
||||||
|
} |
||||||
|
|
||||||
|
public function post(string $key, ?string $defaultValue = null) : ?string { |
||||||
|
return isset($this->post[$key]) ? $this->post[$key] : $defaultValue; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,108 @@ |
|||||||
|
<?php declare(strict_types=1); |
||||||
|
|
||||||
|
namespace massivedynamic\lucidity\services { |
||||||
|
|
||||||
|
use Exception; |
||||||
|
use ReflectionFunction; |
||||||
|
use ReflectionMethod; |
||||||
|
|
||||||
|
use massivedynamic\lucidity\Application; |
||||||
|
use massivedynamic\lucidity\Utils; |
||||||
|
use massivedynamic\lucidity\RequestType; |
||||||
|
use massivedynamic\lucidity\containers\ControllerContainer; |
||||||
|
use massivedynamic\lucidity\services\Request; |
||||||
|
use massivedynamic\lucidity\services\Service; |
||||||
|
|
||||||
|
class Router implements Service { |
||||||
|
private ControllerContainer $controllerContainer; |
||||||
|
private array $routes; |
||||||
|
|
||||||
|
public function __construct(private Application $app, private Request $request) { |
||||||
|
$this->controllerContainer = new ControllerContainer($app); |
||||||
|
$this->routes = [ |
||||||
|
RequestType::GET->value => [], |
||||||
|
RequestType::POST->value => [], |
||||||
|
RequestType::PUT->value => [], |
||||||
|
RequestType::DELETE->value => [], |
||||||
|
]; |
||||||
|
} |
||||||
|
|
||||||
|
private function addCallback(RequestType $type, string $route, callable | array $callback) : void { |
||||||
|
$this->routes[$type->value][$route] = $callback; |
||||||
|
} |
||||||
|
|
||||||
|
public function get(string $route, callable | array $callback) : void { |
||||||
|
$this->addCallback(RequestType::GET, $route, $callback); |
||||||
|
} |
||||||
|
|
||||||
|
public function post(string $route, callable | array $callback) : void { |
||||||
|
$this->addCallback(RequestType::POST, $route, $callback); |
||||||
|
} |
||||||
|
|
||||||
|
public function put(string $route, callable | array $callback) : void { |
||||||
|
$this->addCallback(RequestType::PUT, $route, $callback); |
||||||
|
} |
||||||
|
|
||||||
|
public function delete(string $route, callable | array $callback) : void { |
||||||
|
$this->addCallback(RequestType::DELETE, $route, $callback); |
||||||
|
} |
||||||
|
|
||||||
|
public function view(string $route, string $template, array $context = []) { |
||||||
|
$this->addCallback(RequestType::GET, $route, fn() => view($template, $context)); |
||||||
|
} |
||||||
|
|
||||||
|
public function route() : void { |
||||||
|
$matched = false; |
||||||
|
foreach ($this->routes[$this->request->type->value] as $route => $callback) { |
||||||
|
if ($this->request->match($route)) { |
||||||
|
$matched = true; |
||||||
|
$this->call($callback); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if (!$matched) |
||||||
|
view("404"); |
||||||
|
} |
||||||
|
|
||||||
|
public static function Redirect(string $target) : void { |
||||||
|
header("Location: $target"); |
||||||
|
} |
||||||
|
|
||||||
|
private function call(callable | array $callback) : void { |
||||||
|
$callbackInfo = null; |
||||||
|
|
||||||
|
if (is_array($callback)) { |
||||||
|
assert(count($callback) === 2, "only callables can be routed"); |
||||||
|
[$class, $method] = $callback; |
||||||
|
$callbackInfo = new ReflectionMethod($class, $method); |
||||||
|
$arguments = $this->getArgs($callbackInfo); |
||||||
|
$callbackInfo->invokeArgs($this->controllerContainer->get($class), $arguments); |
||||||
|
} else { |
||||||
|
$callbackInfo = new ReflectionFunction($callback); |
||||||
|
$arguments = $this->getArgs($callbackInfo); |
||||||
|
$callbackInfo->invokeArgs($arguments); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private function getArgs(ReflectionFunction | ReflectionMethod $function) : array { |
||||||
|
$arguments = []; |
||||||
|
foreach ($function->getParameters() as $parameter) { |
||||||
|
$type = $parameter->getType(); |
||||||
|
if ($type === null) |
||||||
|
throw new Exception("constructor arguments need to name a type"); |
||||||
|
array_push($arguments, $this->app->fetchService($type->getName())); |
||||||
|
} |
||||||
|
|
||||||
|
return $arguments; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
namespace { |
||||||
|
|
||||||
|
function redirect(string $target) { |
||||||
|
massivedynamic\lucidity\services\Router::Redirect($target); |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,6 @@ |
|||||||
|
<?php declare(strict_types=1); |
||||||
|
|
||||||
|
namespace massivedynamic\lucidity\services; |
||||||
|
|
||||||
|
interface Service { |
||||||
|
} |
@ -0,0 +1,11 @@ |
|||||||
|
<?php declare(strict_types=1); |
||||||
|
|
||||||
|
namespace massivedynamic\lucidity\validators; |
||||||
|
|
||||||
|
// @struct |
||||||
|
class Validation { |
||||||
|
public function __construct( |
||||||
|
public readonly ValidationType $type, |
||||||
|
public readonly string $parameter |
||||||
|
){} |
||||||
|
} |
@ -0,0 +1,9 @@ |
|||||||
|
<?php declare(strict_types=1); |
||||||
|
|
||||||
|
namespace massivedynamic\lucidity\validators; |
||||||
|
|
||||||
|
enum ValidationType : string { |
||||||
|
case REQUIRED = "required"; |
||||||
|
case MIN = "min"; |
||||||
|
case MAX = "max"; |
||||||
|
} |
@ -0,0 +1,87 @@ |
|||||||
|
<?php declare(strict_types=1); |
||||||
|
|
||||||
|
namespace massivedynamic\lucidity\validators; |
||||||
|
|
||||||
|
use Exception; |
||||||
|
|
||||||
|
use massivedynamic\lucidity\exceptions\UnknownValidatorException; |
||||||
|
use massivedynamic\lucidity\validators\Validation; |
||||||
|
use massivedynamic\lucidity\validators\ValidationType; |
||||||
|
use massivedynamic\lucidity\Utils; |
||||||
|
|
||||||
|
class Validator { |
||||||
|
private array $errors; |
||||||
|
private array $validators; |
||||||
|
|
||||||
|
public function __construct(private array $pool, array $validation) { |
||||||
|
$this->errors = []; |
||||||
|
$this->validators = self::ParseValidationArray($validation); |
||||||
|
} |
||||||
|
|
||||||
|
public function validate() : ?array { |
||||||
|
foreach ($this->validators as $key => $validators) { |
||||||
|
foreach($validators as $validator) { |
||||||
|
switch ($validator->type) { |
||||||
|
case ValidationType::REQUIRED: |
||||||
|
$this->validateRequired($key); |
||||||
|
break; |
||||||
|
case ValidationType::MIN: |
||||||
|
$this->validateMin($key, $validator); |
||||||
|
break; |
||||||
|
case ValidationType::MAX: |
||||||
|
$this->validateMax($key, $validator); |
||||||
|
break; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
return count($this->errors) > 0 ? $this->errors : null; |
||||||
|
} |
||||||
|
|
||||||
|
private function validateMin(string $key, Validation $validator) : void { |
||||||
|
$length = strlen($this->pool[$key]); |
||||||
|
if ($length < intval($validator->parameter)) |
||||||
|
array_push($this->errors, "Field '$key' must be at least $validator->parameter characters long (is $length)"); |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
private function validateMax(string $key, Validation $validator) : void { |
||||||
|
$length = strlen($this->pool[$key]); |
||||||
|
if ($length > intval($validator->parameter)) |
||||||
|
array_push($this->errors, "Field '$key' must be at least $validator->parameter characters long (is $length)"); |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
private function validateRequired(string $key) : void { |
||||||
|
if (!isset($this->pool[$key])) |
||||||
|
array_push($this->errors, "Field '$key' is required"); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @@param array<string, array> $validation |
||||||
|
*/ |
||||||
|
public static function ParseValidationArray(array $validation) : array { |
||||||
|
$out = []; |
||||||
|
|
||||||
|
foreach ($validation as $key => $validators) { |
||||||
|
$validators = explode("|", $validators); |
||||||
|
if (count($validators) < 1) |
||||||
|
continue; |
||||||
|
|
||||||
|
$out[$key] = []; |
||||||
|
foreach ($validators as $validator) { |
||||||
|
$parts = explode(":", $validator); |
||||||
|
[$type, $argument] = count($parts) === 2 ? $parts : [$parts[0], ""]; |
||||||
|
$argument = $argument ?? ""; |
||||||
|
$typeResolved = ValidationType::tryFrom(strtolower($type)); |
||||||
|
|
||||||
|
if ($typeResolved === null) |
||||||
|
throw new UnknownValidatorException($type); |
||||||
|
|
||||||
|
$validator = new Validation($typeResolved, $argument); |
||||||
|
array_push($out[$key], $validator); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return $out; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,12 @@ |
|||||||
|
{{<LucidityLayout}} |
||||||
|
|
||||||
|
{{$content}} |
||||||
|
<main class="card"> |
||||||
|
<h1>HTTP 404</h1> |
||||||
|
<p> |
||||||
|
page not found |
||||||
|
</p> |
||||||
|
</main> |
||||||
|
{{/content}} |
||||||
|
|
||||||
|
{{/LucidityLayout}} |
@ -0,0 +1,35 @@ |
|||||||
|
{{< LucidityLayout}} |
||||||
|
|
||||||
|
{{$content}} |
||||||
|
|
||||||
|
<article class="card"> |
||||||
|
<span class="badge"> |
||||||
|
{{exceptionType}} |
||||||
|
</span> |
||||||
|
<h2> |
||||||
|
{{exception.getMessage}} |
||||||
|
</h2> |
||||||
|
{{#exception.getFile}} |
||||||
|
<a href="zed://file/{{.}}:{{exception.getLine}}" class="file">{{.}}:{{exception.getLine}}</a> |
||||||
|
{{/exception.getFile}} |
||||||
|
<section class="info"> |
||||||
|
<span>PHP {{phpVersion}}</span> |
||||||
|
<span>lucidity {{lucidityVersion}}</span> |
||||||
|
</section> |
||||||
|
</article> |
||||||
|
|
||||||
|
<article class="card edgeless"> |
||||||
|
{{#trace}} |
||||||
|
<article class="list-item"> |
||||||
|
{{#file}} |
||||||
|
<span class="file">{{.}}:{{line}}</span> |
||||||
|
{{/file}} |
||||||
|
<h4>{{class}}<h4> |
||||||
|
<h3>{{function}}</h3> |
||||||
|
</article> |
||||||
|
{{/trace}} |
||||||
|
</article> |
||||||
|
|
||||||
|
{{/content}} |
||||||
|
|
||||||
|
{{/LucidityLayout}} |
@ -0,0 +1,72 @@ |
|||||||
|
<html> |
||||||
|
<head> |
||||||
|
<title>{{title}}</title> |
||||||
|
<style type=text/css> |
||||||
|
* { |
||||||
|
box-sizing: border-box; |
||||||
|
} |
||||||
|
|
||||||
|
body { |
||||||
|
font-family: sans-serif; |
||||||
|
padding: 2rem; |
||||||
|
background: #e5e7eb; |
||||||
|
color: #333; |
||||||
|
} |
||||||
|
|
||||||
|
.badge { |
||||||
|
background : #ddd; |
||||||
|
display: inline-block; |
||||||
|
padding: 0.5rem 1rem; |
||||||
|
border-radius: 4px; |
||||||
|
font-weight: bold; |
||||||
|
color: #444; |
||||||
|
} |
||||||
|
|
||||||
|
.card { |
||||||
|
background: white; |
||||||
|
box-shadow: 0 0 16px rgba(0, 0, 0, 0.3); |
||||||
|
margin-bottom: 4rem; |
||||||
|
position: relative; |
||||||
|
} |
||||||
|
.card:not(.edgeless) { |
||||||
|
padding: 2rem; |
||||||
|
} |
||||||
|
|
||||||
|
.list-item { |
||||||
|
padding: 1rem 2rem; |
||||||
|
transition: all 0.2s ease-in-out; |
||||||
|
} |
||||||
|
.list-item:not(:last-of-type) { |
||||||
|
border-bottom : solid 1px #ddd; |
||||||
|
} |
||||||
|
.list-item:hover { |
||||||
|
background: #dd524c; |
||||||
|
} |
||||||
|
.list-item:hover, .list-item:hover * { |
||||||
|
color: white !important; |
||||||
|
} |
||||||
|
|
||||||
|
.file { |
||||||
|
color: #aaa; |
||||||
|
display: block; |
||||||
|
} |
||||||
|
|
||||||
|
.info { |
||||||
|
display: flex; |
||||||
|
white-space: nowrap; |
||||||
|
gap: 1rem; |
||||||
|
color: #888; |
||||||
|
font-weight: bold; |
||||||
|
position: absolute; |
||||||
|
top: 2rem; |
||||||
|
right: 2rem; |
||||||
|
} |
||||||
|
|
||||||
|
.info * { |
||||||
|
flex: 1; |
||||||
|
} |
||||||
|
</style> |
||||||
|
</head> |
||||||
|
<body> |
||||||
|
|
||||||
|
{{$content}}{{/content}} |
Loading…
Reference in new issue