picea-ui/src/Method/FormContext.php

251 lines
7.4 KiB
PHP

<?php
namespace Picea\Ui\Method;
use Psr\Http\Message\ServerRequestInterface,
Psr\Http\Message\ResponseInterface;
class FormContext implements FormContextInterface
{
public string $formContextMethod;
public bool $formSent = false;
public bool $formExecuted = false;
public mixed $formExecutionStatus = null;
protected string $formContextValuesRoot;
public array $formContextValues = [];
public array $formContextFiles = [];
public array $formContextMessages = [];
public bool $formSkipCsrf = false;
protected array $formContextDefinedProperties = [];
protected array $formContextCatchedMethods = [ 'POST', 'PUT', 'PATCH', 'DELETE', ];
protected array $formConstructorProperties = [];
public ? ResponseInterface $formContextResponse = null;
public function __construct(
public ServerRequestInterface $formContextRequest,
public readonly ? string $formName = null
) {
# From constructor
$reflection = new \ReflectionClass($this);
$this->formConstructorProperties = array_map(fn($e) => strtolower($e->getName()), array_filter($reflection->getConstructor()->getParameters(), fn($e) => $e->isPromoted()));
$this->formContextValues = $formContextRequest->getParsedBody() ?: [];
if ( ! $this->formContextValues ) {
$content = mb_convert_encoding((string) $formContextRequest->getBody(), 'UTF-8');
if ( $content && ( $json = json_decode($content, true) ) ) {
$this->formContextValues = $json;
}
}
if ($this->formContextValuesRoot ?? false) {
$this->formContextValues = $this->formContextValues[$this->formContextValuesRoot] ?? [];
}
$this->fillValues();
$this->formContextFiles = $formContextRequest->getUploadedFiles() ?: [];
$this->initialize();
}
public function initialize() : void {}
public function valid() : bool
{
foreach($this->formContextMessages as $message) {
if ( $message->isError() ) {
return false;
}
}
return true;
}
public function executed() : bool
{
return $this->formExecutionStatus === false ? false : $this->formExecuted;
}
public function formSent() : bool
{
$valid = in_array($this->requestMethod(), $this->formContextCatchedMethods);
if ( (! $this->formSkipCsrf) && ! empty($this->formName) ) {
$token = $this->get('picea-ui-form')[$this->formName] ?? false;
if ( $token ) {
if (! $this->formSkipCsrf) {
$valid = in_array($token, $_SESSION["picea-ui.form:{$this->formName}"] ?? []);
}
else {
$valid = (bool) $token;
}
}
else {
$valid = false;
}
}
return $this->formSent = $valid;
}
public function getExecutionStatus() : mixed
{
return $this->formExecutionStatus;
}
public function getRequest() : ServerRequestInterface
{
return $this->formContextRequest;
}
public function requestMethod() : string
{
return $this->formContextRequest->getMethod();
}
public function __set($key, $value)
{
return $this->set($key, $value);
}
public function __get($key)
{
return $this->get($key);
}
public function __isset($key)
{
return array_key_exists($key, $this->formContextValues);
}
public function __unset($key)
{
$this->delete($key);
}
public function get(string $key, mixed $default = null)
{
return $this->has($key) ? $this->formContextValues[$key] : $default;
}
public function set(string $key, mixed $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
{
unset($this->formContextValues[$key]);
}
public function has(string $key) : bool
{
return array_key_exists($key, $this->formContextValues);
}
public function pushMessage(FormMessage $message) : void
{
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
{
foreach($this->formContextValues as $property => $value) {
if ($this->canWriteProperty($property)) {
$this->$property = $this->castValue($property, $value);
$this->formContextDefinedProperties[$property] = true;
}
}
}
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;
}
}