15 Commits
8 changed files with 165 additions and 50 deletions

No files matched your search

+3 -1
View File
@@ -4,7 +4,7 @@ use function DI\autowire, DI\create, DI\get;
use Picea\{ Method\Request }; use Picea\{ Method\Request };
use Picea\Extension\{ LanguageExtension, TitleExtension, NumberExtension, UrlExtension }; use Picea\Extension\{ LanguageExtension, TitleExtension, NumberExtension, UrlExtension, DiExtension };
use Picea\Ui\{ Method, Ui }; use Picea\Ui\{ Method, Ui };
@@ -20,6 +20,8 @@ return [
$c->get(Method\Form::class), $c->get(Method\Form::class),
$c->get(Method\Pagination::class), $c->get(Method\Pagination::class),
$c->get(Request::class), $c->get(Request::class),
$c->get(DiExtension::class),
], class_exists(\Taxus\Picea\Extension::class) ? [ $c->get(\Taxus\Picea\Extension::class) ] : [], ], class_exists(\Taxus\Picea\Extension::class) ? [ $c->get(\Taxus\Picea\Extension::class) ] : [],
array_map(fn($class) => $c->get($class), $c->get(Lean\Lean::class)->getPiceaExtensions()) array_map(fn($class) => $c->get($class), $c->get(Lean\Lean::class)->getPiceaExtensions())
); );
+3 -2
View File
@@ -11,7 +11,7 @@ class UiForm extends UiElement implements Extension {
public string $defaultMethod = "get"; public string $defaultMethod = "get";
public array $token = [ "ui:form", "ui:endform" ]; public array $token = [ "ui:form", "ui:endform", "ui:/form" ];
public array $attributes = [ public array $attributes = [
'class' => 'ui-form', 'class' => 'ui-form',
@@ -30,6 +30,7 @@ class UiForm extends UiElement implements Extension {
switch($token) { switch($token) {
case 'ui.endform': # bw compat case 'ui.endform': # bw compat
case 'ui:endform': case 'ui:endform':
case 'ui:/form':
return "</form>"; return "</form>";
} }
@@ -84,7 +85,7 @@ class UiForm extends UiElement implements Extension {
if ($this->csrf) { if ($this->csrf) {
$token = md5($name . microtime()); $token = md5($name . microtime());
$key = "picea-ui:form:{$name}"; $key = "picea-ui.form:{$name}";
if (count($_SESSION[$key] ?? []) > 100) { if (count($_SESSION[$key] ?? []) > 100) {
array_shift($_SESSION[$key]); array_shift($_SESSION[$key]);
+12 -2
View File
@@ -79,13 +79,23 @@ class UiSelect extends UiElement implements Extension {
return $option; return $option;
} }
protected function isSelected($check, $value, bool $strict = true) : bool protected function isSelected($check, mixed $value, bool $strict = true) : bool
{ {
if ( is_array($value) ) { if ( is_array($value) ) {
return in_array($check, $value, $strict); return in_array($check, array_map(fn($e) => $this->normalizeValue($e), $value), $strict);
} }
$value = $this->normalizeValue($value);
return $strict ? $check === $value : $check == $value; return $strict ? $check === $value : $check == $value;
} }
protected function normalizeValue(mixed $value) : mixed
{
if ($value instanceof \BackedEnum) {
return $value->value;
}
return $value;
}
} }
@@ -0,0 +1,8 @@
<?php
namespace Picea\Ui\Method\Context;
interface FormContextCastableInterface
{
public static function instance(mixed $value);
}
+1 -1
View File
@@ -36,6 +36,6 @@ class Form implements Extension, FunctionExtension {
public function formClass(FormInterface $form, ? FormContext $formContext = null) : FormHandler public function formClass(FormInterface $form, ? FormContext $formContext = null) : FormHandler
{ {
return new FormHandler($formContext ? $formContext->request : $this->request, $form, $formContext); return new FormHandler($formContext ? $formContext->getRequest() : $this->request, $form, $formContext);
} }
} }
+130 -43
View File
@@ -7,9 +7,7 @@ use Psr\Http\Message\ServerRequestInterface,
class FormContext implements FormContextInterface class FormContext implements FormContextInterface
{ {
public string $method; public string $formContextMethod;
public string $formName;
public bool $formSent = false; public bool $formSent = false;
@@ -17,43 +15,50 @@ class FormContext implements FormContextInterface
public mixed $formExecutionStatus = null; public mixed $formExecutionStatus = null;
public array $values = []; protected string $formContextValuesRoot;
public array $files = []; public array $formContextValues = [];
public array $messages = []; public array $formContextFiles = [];
public bool $skipCsrf = false; public array $formContextMessages = [];
protected array $catchedMethods = [ 'POST', 'PUT', 'PATCH', 'DELETE', ]; public bool $formSkipCsrf = false;
public ServerRequestInterface $request; protected array $formContextDefinedProperties = [];
public ? ResponseInterface $response = null;
public function __construct(ServerRequestInterface $request, ? string $formName = null) protected array $formContextCatchedMethods = [ 'POST', 'PUT', 'PATCH', 'DELETE', ];
{
$this->request = $request;
if ( $formName ) { protected array $formConstructorProperties = [];
$this->formName = $formName;
}
$this->values = $request->getParsedBody() ?: []; public ? ResponseInterface $formContextResponse = null;
$this->method = $this->request->getMethod(); public function __construct(
public ServerRequestInterface $formContextRequest,
public readonly ? string $formName = null
) {
# From constructor
$reflection = new \ReflectionClass($this);
if ( ! $this->values ) { $this->formConstructorProperties = array_map(fn($e) => strtolower($e->getName()), array_filter($reflection->getConstructor()->getParameters(), fn($e) => $e->isPromoted()));
$content = mb_convert_encoding((string) $request->getBody(), 'UTF-8');
$this->formContextValues = $formContextRequest->getParsedBody() ?: [];
if ( ! $this->formContextValues ) {
$content = mb_convert_encoding((string) $formContextRequest->getBody(), 'UTF-8');
if ( $content && ( $json = json_decode($content, true) ) ) { if ( $content && ( $json = json_decode($content, true) ) ) {
$this->values = $json; $this->formContextValues = $json;
} }
} }
if ($this->formContextValuesRoot ?? false) {
$this->formContextValues = $this->formContextValues[$this->formContextValuesRoot] ?? [];
}
$this->fillValues(); $this->fillValues();
$this->files = $request->getUploadedFiles() ?: []; $this->formContextFiles = $formContextRequest->getUploadedFiles() ?: [];
$this->initialize(); $this->initialize();
} }
@@ -62,7 +67,7 @@ class FormContext implements FormContextInterface
public function valid() : bool public function valid() : bool
{ {
foreach($this->messages as $message) { foreach($this->formContextMessages as $message) {
if ( $message->isError() ) { if ( $message->isError() ) {
return false; return false;
} }
@@ -78,13 +83,13 @@ class FormContext implements FormContextInterface
public function formSent() : bool public function formSent() : bool
{ {
$valid = in_array($this->method, $this->catchedMethods); $valid = in_array($this->requestMethod(), $this->formContextCatchedMethods);
if ( ! $this->skipCsrf && ($this->formName ?? false) ) { if ( (! $this->formSkipCsrf) && ! empty($this->formName) ) {
$token = $this->get('picea-ui-form')[$this->formName] ?? false; $token = $this->get('picea-ui-form')[$this->formName] ?? false;
if ( $token ) { if ( $token ) {
if ($this->validateCsrfToken) { if (! $this->formSkipCsrf) {
$valid = in_array($token, $_SESSION["picea-ui.form:{$this->formName}"] ?? []); $valid = in_array($token, $_SESSION["picea-ui.form:{$this->formName}"] ?? []);
} }
else { else {
@@ -99,9 +104,19 @@ class FormContext implements FormContextInterface
return $this->formSent = $valid; return $this->formSent = $valid;
} }
public function getExecutionStatus() : mixed
{
return $this->formExecutionStatus;
}
public function getRequest() : ServerRequestInterface
{
return $this->formContextRequest;
}
public function requestMethod() : string public function requestMethod() : string
{ {
return $this->method; return $this->formContextRequest->getMethod();
} }
public function __set($key, $value) public function __set($key, $value)
@@ -116,7 +131,7 @@ class FormContext implements FormContextInterface
public function __isset($key) public function __isset($key)
{ {
return array_key_exists($key, $this->values); return array_key_exists($key, $this->formContextValues);
} }
public function __unset($key) public function __unset($key)
@@ -124,40 +139,112 @@ class FormContext implements FormContextInterface
$this->delete($key); $this->delete($key);
} }
public function get(string $key, $default = null) public function get(string $key, mixed $default = null)
{ {
return $this->has($key) ? $this->values[$key] : $default; return $this->has($key) ? $this->formContextValues[$key] : $default;
} }
public function set(string $key, $value) public function set(string $key, mixed $value)
{ {
return $this->values[$key] = $value;
if ($this->canWriteProperty($key)) {
$this->$key = $this->castValue($key, $value);
}
return $this->formContextValues[$key] = $value;
} }
public function sets(array $values) : static
{
foreach($values as $key =>$value) {
$this->set($key, $value);
}
return $this;
}
public function delete(string $key) : void public function delete(string $key) : void
{ {
unset($this->values[$key]); unset($this->formContextValues[$key]);
} }
public function has(string $key) : bool public function has(string $key) : bool
{ {
return array_key_exists($key, $this->values); return array_key_exists($key, $this->formContextValues);
} }
public function pushMessage(FormMessage $message) : void public function pushMessage(FormMessage $message) : void
{ {
$this->messages[] = $message; if ($message->isError() /* && $message->isSuccess() @TODO next major v. */ ) {
$this->formContextMessages = array_merge([ $message ], $this->formContextMessages);
}
else {
$this->formContextMessages[] = $message;
}
}
public function messages() : array
{
return $this->formContextMessages;
} }
protected function fillValues() : void protected function fillValues() : void
{ {
# Skipping overrides of this particular class vars as a security measure foreach($this->formContextValues as $property => $value) {
$skipping = array_keys(array_change_key_case(get_class_vars(FormContext::class), CASE_LOWER)); if ($this->canWriteProperty($property)) {
$this->$property = $this->castValue($property, $value);
foreach($this->values as $property => $value) { $this->formContextDefinedProperties[$property] = true;
if ( ! in_array(strtolower($property), $skipping) && property_exists($this, $property)) {
$this->$property = $value;
} }
} }
} }
protected function canWriteProperty(string $property) : bool
{
# Skipping overrides of this particular class vars as a security measure
static $skipping = array_keys(array_change_key_case(get_class_vars(FormContext::class), CASE_LOWER));
return ! in_array(strtolower($property), array_merge($skipping, $this->formConstructorProperties)) && property_exists($this, $property);
}
protected function definedProperty(string $property) : bool
{
return $this->formContextDefinedProperties[$property] ?? false;
}
protected function castValue(string $property, mixed $value) : mixed
{
$cast = fn($e) => match(true) {
is_a($e, Context\FormContextCastableInterface::class, true) => $e::instance($value),
$e === "array" => (function(array|string $value) use ($property) {
if (is_string($value)) {
return json_validate($value) ? json_decode($value, true) ?? [] : throw new \InvalidArgumentException("Field '$property' is awaiting JSON encoded content.");
}
return $value;
})($value),
is_null($value) => null,
enum_exists($e) => ( $value instanceof \BackedEnum ? $value : $e::tryFrom($value) ) ?? throw new \InvalidArgumentException(sprintf("Field '\$$property' awaiting values : '%s'.", implode("', '", array_map(fn($e) => $e->value, $e::cases())))),
function_exists($e) => $e($value),
class_exists($e) => new $e(...$value),
interface_exists($e) => throw new \InvalidArgumentException("There is no way provided to match an interface object yet."),
default => $value
};
$types = ( new \ReflectionProperty($this, $property) )->getType();
if ($types instanceof \ReflectionNamedType) {
$value = $cast($types->getName());
}
elseif ($types instanceof \ReflectionIntersectionType || $types instanceof \ReflectionUnionType) {
foreach($types->getTypes() as $type) {
return $cast($type->getName());
}
}
elseif ($types->isBuiltin()) {
return $cast($types->getName());
}
return $value;
}
} }
+6
View File
@@ -2,10 +2,16 @@
namespace Picea\Ui\Method; namespace Picea\Ui\Method;
use Psr\Http\Message\ServerRequestInterface;
interface FormContextInterface { interface FormContextInterface {
public function valid() : bool; public function valid() : bool;
public function formSent() : bool; public function formSent() : bool;
public function requestMethod() : string; public function requestMethod() : string;
public function getRequest() : ServerRequestInterface;
public function getExecutionStatus() : mixed;
} }
+2 -1
View File
@@ -2,6 +2,7 @@
namespace Picea\Ui\Method; namespace Picea\Ui\Method;
interface FormMessage { interface FormMessage {
public function isError() : bool; public function isError() : bool;
# public function isSuccess() : bool; # Should be added in next major version
} }