16 Commits
Author SHA1 Message Date
mcndave 11756bdfd2 - WIP on package manager addition 2026-07-16 14:14:19 -04:00
mcndave 00e5a0380a - Added some more flexibility to castValue method 2026-07-14 15:19:25 +00:00
mcndave e093495909 - Added DiExtension ; this part will soon be revamped 2026-07-07 12:23:49 +00:00
mcndave 9ddb93b847 - Added enums to select values 2026-07-01 12:54:53 -04:00
Dev 363da77669 - Fixed a bug when an enum is given as value 2026-06-09 19:33:35 +00:00
Dave Mc Nicoll 94903d6f05 - Added some checkup on constructor properties 2026-05-21 15:00:37 +00:00
Dave Mc Nicoll 3d412d6a46 - Defined sets() as chainable 2026-02-25 19:15:21 +00:00
Dev ca2d71e3ed - Added a messages() method 2026-02-05 21:22:46 +00:00
mcndave 7e2d4c7dc2 - Added formContextValuesRoot to form context 2026-01-16 16:21:50 -05:00
mcndave 2e3788a295 - Fixed CSRF missing from sent form 2025-11-06 17:17:45 +00:00
Dave Mc Nicoll 6e7a8f12fb - Added casting of values from FormContext set property 2025-10-27 18:33:09 +00:00
Dave Mc Nicoll a1c51bed10 - Added a ui:/form token to form/endform 2025-10-15 15:01:20 +00:00
Dave Mc Nicoll 1044e35946 - WIP on object initialization for FormContext from request body values 2025-10-14 18:18:31 +00:00
Dave Mc Nicoll 6ccf07825a - Added form context defined property 2025-09-15 20:04:49 +00:00
mcndave 23f24fef50 - Added form interface which connect to this context 2025-07-17 18:36:09 +00:00
Dave Mc Nicoll 33e77b5129 - Added getRequest() method to replace direct property access 2025-05-21 18:38:25 +00:00
9 changed files with 164 additions and 49 deletions

No files matched your search

+3 -3
View File
@@ -10,12 +10,12 @@
}
],
"require": {
"mcnd/picea": "dev-master"
"mcnd/picea": "^1.4.0"
},
"repositories": [
{
"type": "vcs",
"url": "https://github.com/mcNdave/picea.git"
"type": "composer",
"url": "https://git.mcnd.ca/api/packages/mcndave/composer"
}
],
"autoload": {
+3 -1
View File
@@ -4,7 +4,7 @@ use function DI\autowire, DI\create, DI\get;
use Picea\{ Method\Request };
use Picea\Extension\{ LanguageExtension, TitleExtension, NumberExtension, UrlExtension };
use Picea\Extension\{ LanguageExtension, TitleExtension, NumberExtension, UrlExtension, DiExtension };
use Picea\Ui\{ Method, Ui };
@@ -20,6 +20,8 @@ return [
$c->get(Method\Form::class),
$c->get(Method\Pagination::class),
$c->get(Request::class),
$c->get(DiExtension::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())
);
+3 -2
View File
@@ -11,7 +11,7 @@ class UiForm extends UiElement implements Extension {
public string $defaultMethod = "get";
public array $token = [ "ui:form", "ui:endform" ];
public array $token = [ "ui:form", "ui:endform", "ui:/form" ];
public array $attributes = [
'class' => 'ui-form',
@@ -30,6 +30,7 @@ class UiForm extends UiElement implements Extension {
switch($token) {
case 'ui.endform': # bw compat
case 'ui:endform':
case 'ui:/form':
return "</form>";
}
@@ -84,7 +85,7 @@ class UiForm extends UiElement implements Extension {
if ($this->csrf) {
$token = md5($name . microtime());
$key = "picea-ui:form:{$name}";
$key = "picea-ui.form:{$name}";
if (count($_SESSION[$key] ?? []) > 100) {
array_shift($_SESSION[$key]);
+12 -2
View File
@@ -79,13 +79,23 @@ class UiSelect extends UiElement implements Extension {
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) ) {
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;
}
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
{
return new FormHandler($formContext ? $formContext->request : $this->request, $form, $formContext);
return new FormHandler($formContext ? $formContext->getRequest() : $this->request, $form, $formContext);
}
}
+127 -40
View File
@@ -7,9 +7,7 @@ use Psr\Http\Message\ServerRequestInterface,
class FormContext implements FormContextInterface
{
public string $method;
public string $formName;
public string $formContextMethod;
public bool $formSent = false;
@@ -17,43 +15,50 @@ class FormContext implements FormContextInterface
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;
protected array $formContextCatchedMethods = [ 'POST', 'PUT', 'PATCH', 'DELETE', ];
public function __construct(ServerRequestInterface $request, ? string $formName = null)
{
$this->request = $request;
protected array $formConstructorProperties = [];
if ( $formName ) {
$this->formName = $formName;
}
public ? ResponseInterface $formContextResponse = null;
$this->values = $request->getParsedBody() ?: [];
public function __construct(
public ServerRequestInterface $formContextRequest,
public readonly ? string $formName = null
) {
# From constructor
$reflection = new \ReflectionClass($this);
$this->method = $this->request->getMethod();
$this->formConstructorProperties = array_map(fn($e) => strtolower($e->getName()), array_filter($reflection->getConstructor()->getParameters(), fn($e) => $e->isPromoted()));
if ( ! $this->values ) {
$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) ) ) {
$this->values = $json;
$this->formContextValues = $json;
}
}
if ($this->formContextValuesRoot ?? false) {
$this->formContextValues = $this->formContextValues[$this->formContextValuesRoot] ?? [];
}
$this->fillValues();
$this->files = $request->getUploadedFiles() ?: [];
$this->formContextFiles = $formContextRequest->getUploadedFiles() ?: [];
$this->initialize();
}
@@ -62,7 +67,7 @@ class FormContext implements FormContextInterface
public function valid() : bool
{
foreach($this->messages as $message) {
foreach($this->formContextMessages as $message) {
if ( $message->isError() ) {
return false;
}
@@ -78,13 +83,13 @@ class FormContext implements FormContextInterface
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;
if ( $token ) {
if ($this->validateCsrfToken) {
if (! $this->formSkipCsrf) {
$valid = in_array($token, $_SESSION["picea-ui.form:{$this->formName}"] ?? []);
}
else {
@@ -99,9 +104,19 @@ class FormContext implements FormContextInterface
return $this->formSent = $valid;
}
public function getExecutionStatus() : mixed
{
return $this->formExecutionStatus;
}
public function getRequest() : ServerRequestInterface
{
return $this->formContextRequest;
}
public function requestMethod() : string
{
return $this->method;
return $this->formContextRequest->getMethod();
}
public function __set($key, $value)
@@ -116,7 +131,7 @@ class FormContext implements FormContextInterface
public function __isset($key)
{
return array_key_exists($key, $this->values);
return array_key_exists($key, $this->formContextValues);
}
public function __unset($key)
@@ -124,40 +139,112 @@ class FormContext implements FormContextInterface
$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
{
unset($this->values[$key]);
unset($this->formContextValues[$key]);
}
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
{
$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
{
# Skipping overrides of this particular class vars as a security measure
$skipping = array_keys(array_change_key_case(get_class_vars(FormContext::class), CASE_LOWER));
foreach($this->formContextValues as $property => $value) {
if ($this->canWriteProperty($property)) {
$this->$property = $this->castValue($property, $value);
$this->formContextDefinedProperties[$property] = true;
}
}
}
foreach($this->values as $property => $value) {
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;
use Psr\Http\Message\ServerRequestInterface;
interface FormContextInterface {
public function valid() : bool;
public function formSent() : bool;
public function requestMethod() : string;
public function getRequest() : ServerRequestInterface;
public function getExecutionStatus() : mixed;
}
+1
View File
@@ -4,4 +4,5 @@ namespace Picea\Ui\Method;
interface FormMessage {
public function isError() : bool;
# public function isSuccess() : bool; # Should be added in next major version
}