From 3d50cfaccb75bb24364f62ef747ede51acaf1a41 Mon Sep 17 00:00:00 2001 From: Dave Mc Nicoll Date: Mon, 1 Dec 2025 15:46:02 +0000 Subject: [PATCH] - Big WIP on forms component --- src/ApplicationStrategy/NotFoundDecorator.php | 7 ++-- src/Attribute/ContextField.php | 34 +++++++++++++++ src/Exception/MandatoryFieldException.php | 5 +++ src/Exception/MaximumLengthException.php | 5 +++ src/Exception/MinimumLengthException.php | 5 +++ src/Exception/RegexFormatException.php | 5 +++ src/Form/Delete.php | 5 +-- src/Form/{FormTrait.php => Form.php} | 2 +- src/Form/Save.php | 41 +++++++++++++++++-- src/LeanApiTrait.php | 2 +- src/Lib/FormContext.php | 3 ++ src/Middleware/ApiRenderer.php | 4 +- view/lean-api/request_debugger.phtml | 10 ++--- view/lean-api/route_descriptor.phtml | 6 ++- 14 files changed, 114 insertions(+), 20 deletions(-) create mode 100644 src/Exception/MandatoryFieldException.php create mode 100644 src/Exception/MaximumLengthException.php create mode 100644 src/Exception/MinimumLengthException.php create mode 100644 src/Exception/RegexFormatException.php rename src/Form/{FormTrait.php => Form.php} (94%) diff --git a/src/ApplicationStrategy/NotFoundDecorator.php b/src/ApplicationStrategy/NotFoundDecorator.php index 61f74f4..344dc99 100644 --- a/src/ApplicationStrategy/NotFoundDecorator.php +++ b/src/ApplicationStrategy/NotFoundDecorator.php @@ -3,6 +3,7 @@ namespace Lean\Api\ApplicationStrategy; use DI\Attribute\Inject; +use Lean\Api\Middleware\ApiRenderer; use Lean\Factory\HttpFactoryInterface; use Picea\Picea; use Psr\Container\ContainerInterface; @@ -15,14 +16,14 @@ class NotFoundDecorator extends \Lean\ApplicationStrategy\NotFoundDecorator { public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { - if (php_sapi_name() !== 'cli' and ! defined('STDIN')) { - return $this->throw404($request); + if (php_sapi_name() !== 'cli' and ! defined('STDIN') and ApiRenderer::awaitingJson($request)) { + return $this->throw404api($request); } return parent::process($request, $handler); } - public function throw404(ServerRequestInterface $request) : ResponseInterface + public function throw404api(ServerRequestInterface $request) : ResponseInterface { return $this->checkAssetTrigger($request) ?: $this->httpFactory->createJsonResponse([ 'status' => 'error', diff --git a/src/Attribute/ContextField.php b/src/Attribute/ContextField.php index 1e67e25..ac2f127 100644 --- a/src/Attribute/ContextField.php +++ b/src/Attribute/ContextField.php @@ -2,6 +2,11 @@ namespace Lean\Api\Attribute; +use Lean\Api\Exception\MandatoryFieldException; +use Lean\Api\Exception\MaximumLengthException; +use Lean\Api\Exception\MinimumLengthException; +use Lean\Api\Exception\RegexFormatException; + #[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_CLASS)] class ContextField { @@ -13,5 +18,34 @@ class ContextField public ?string $regexFormat = null, public ?string $example = null, public bool $hidden = false, + public mixed $default = false, ) {} + + public function assertValueSpecs(mixed $value, bool $isNew) : void + { + if ($isNew && ( $this->mandatory && $value === null )) { + throw new MandatoryFieldException("Mandatory field must be provided a value."); + } + elseif ($value !== null) { + if ($this->minLength || $this->maxLength) { + if (is_string($value)) { + if (mb_strlen($value) < $this->minLength) { + throw new MinimumLengthException("Minimum {$this->minLength} string length is required."); + } + elseif (mb_strlen($value) > $this->maxLength) { + throw new MaximumLengthException("Maximum {$this->maxLength} string length has been exceeded."); + } + } + } + + if ($this->regexFormat) { + if (! is_string($value)) { + throw new RegexFormatException("Property type must be a string if a regexFormat is provided."); + } + elseif (! preg_match($this->regexFormat, $value)) { + throw new RegexFormatException("Regex format {$this->regexFormat} is not valid for value {$value}."); + } + } + } + } } \ No newline at end of file diff --git a/src/Exception/MandatoryFieldException.php b/src/Exception/MandatoryFieldException.php new file mode 100644 index 0000000..ab67195 --- /dev/null +++ b/src/Exception/MandatoryFieldException.php @@ -0,0 +1,5 @@ +entity->isLoaded() ) { + if ( ! $this->getEntity()->isLoaded() ) { $context->pushMessage($this->message::generateError( $this->lang('lean.api.form.delete.error.entity') )); diff --git a/src/Form/FormTrait.php b/src/Form/Form.php similarity index 94% rename from src/Form/FormTrait.php rename to src/Form/Form.php index e2ac88c..aa60f4e 100644 --- a/src/Form/FormTrait.php +++ b/src/Form/Form.php @@ -8,7 +8,7 @@ use Picea\Ui\Method\FormContextInterface; use Ulmus\Entity\EntityInterface; use Ulmus\EntityCollection; -trait FormTrait +abstract class Form { public function __construct( protected LanguageHandler $languageHandler, diff --git a/src/Form/Save.php b/src/Form/Save.php index 75112a5..c906545 100644 --- a/src/Form/Save.php +++ b/src/Form/Save.php @@ -5,20 +5,38 @@ namespace Lean\Api\Form; use CSLSJ\Lean\Form\Session\EmailContext; use Picea\Ui\Method\{ FormContextInterface, }; +use Lean\Api\Attribute\ContextField; +use Lean\Api\Exception\MandatoryFieldException; +use Notes\ObjectReflection; use Ulmus\Attribute\Property\Field; use Lean\Api\Attribute\EntityField; use Ulmus\Entity\EntityInterface; use Ulmus\Entity\Field\Datetime; -abstract class Save implements \Picea\Ui\Method\FormInterface { - use FormTrait; +abstract class Save extends Form implements \Picea\Ui\Method\FormInterface { protected string $contextClass; + public function initialize(FormContextInterface $context) : void + { + if ( ! $this->getEntity()->isLoaded() || $context->formSent() ) { + if (method_exists($context, 'initializeEntity')) { + $context->initializeEntity($this->getEntity()); + } + else { + $this->assignContextToEntity($context); + } + } + + parent::initialize($context); + } + public function validate(FormContextInterface $context) : bool { - # Context is validating inputs + # Context validates ContextField attributes containing properties on empty entity + $this->validateContextFields($context); + return $context->valid($this->getEntity()->isLoaded() ? $this->getEntity() : null); } @@ -61,13 +79,30 @@ abstract class Save implements \Picea\Ui\Method\FormInterface { return $saved; } + protected function validateContextFields(FormContextInterface $context) : void + { + foreach (ObjectReflection::fromClass($context::class)->reflectProperties(\ReflectionProperty::IS_PUBLIC) as $name => $property) { + $attribute = $property->getAttribute(ContextField::class); + + if ($attribute) { + try { + $attribute->object->assertValueSpecs($context->$name ?? null, $this->getEntity()->isLoaded()); + } catch (MandatoryFieldException $e) { + throw new MandatoryFieldException("An error occured with field '$name': " . $e->getMessage()); + } + } + } + } + protected function assignContextToEntity(FormContextInterface $context) : void { $entity = $this->getEntity(); foreach($entity::resolveEntity()->fieldList() as $key => $property) { $field = $property->getAttribute(Field::class)->object; + if (! $field->readonly || ! $entity->isLoaded()) { + $apiField = $property->getAttribute(EntityField::class)->object ?? null; if ($apiField) { diff --git a/src/LeanApiTrait.php b/src/LeanApiTrait.php index c0f5100..80b8299 100644 --- a/src/LeanApiTrait.php +++ b/src/LeanApiTrait.php @@ -74,7 +74,7 @@ trait LeanApiTrait } #[Ignore] - protected function searchEntityFromRequest(ServerRequestInterface $request, string $entityType, ? string $resultKey = null) : ServerRequestInterface + protected function searchEntityFromRequest(ServerRequestInterface $request, string $entityType, ? string $resultKey = null) : ServerRequestInterface { $search = $entityType::searchRequest()->fromRequest($request); $entity = $entityType::repository()->filterServerRequest($search)->loadOne() ?? false; diff --git a/src/Lib/FormContext.php b/src/Lib/FormContext.php index 5520121..d87f874 100644 --- a/src/Lib/FormContext.php +++ b/src/Lib/FormContext.php @@ -5,6 +5,7 @@ namespace Lean\Api\Lib; use Lean\Api\Factory\MessageFactoryInterface; use Lean\LanguageHandler; use Psr\Http\Message\ServerRequestInterface; +use Ulmus\Entity\EntityInterface; class FormContext extends \Picea\Ui\Method\FormContext { @@ -18,6 +19,8 @@ class FormContext extends \Picea\Ui\Method\FormContext { parent::__construct($request, $formName); } + # public function initializeEntity(EntityInterface $entity) : void {} + public function lang(string $key, array $variables = []) { return $this->languageHandler->languageFromKey($key, $variables); diff --git a/src/Middleware/ApiRenderer.php b/src/Middleware/ApiRenderer.php index 616d958..e6c3ef2 100644 --- a/src/Middleware/ApiRenderer.php +++ b/src/Middleware/ApiRenderer.php @@ -26,7 +26,7 @@ class ApiRenderer implements MiddlewareInterface { catch(\Throwable $ex) { # $errorId = $this->saveError($request, $ex)->id; - if ($this->awaitingJson($request)) { + if (static::awaitingJson($request)) { return HttpFactory::createJsonResponse([ 'status' => 'failed', 'ts' => time(), @@ -80,7 +80,7 @@ class ApiRenderer implements MiddlewareInterface { return $entity; } - protected function awaitingJson(ServerRequestInterface $request) : bool + public static function awaitingJson(ServerRequestInterface $request) : bool { return str_contains(strtolower($request->getHeaderLine('content-type')), 'json'); } diff --git a/view/lean-api/request_debugger.phtml b/view/lean-api/request_debugger.phtml index eb016dc..101f983 100644 --- a/view/lean-api/request_debugger.phtml +++ b/view/lean-api/request_debugger.phtml @@ -35,13 +35,13 @@
-
-

-
         
+
+

+
     
@@ -55,8 +55,8 @@ .request-head .url button {font-size: 80%;width: 80px;border-radius:0;border: 1px solid #ccc;height:26px;padding: 0;background:#f9f9f9;color: #6f6f6f;cursor: pointer;} .request-head .url button:active {filter:contrast(85%);} - .response-head {display: flex;border:1px solid #9f9f9f;margin-bottom: 5px;} - .response-head .response-code {background:#dfdfdf;padding:0 5px;font-weight: bold;line-height: 29px;font-size: 80%;} + .response-head {display: flex;border:1px solid #ececec;margin-bottom: 5px;border-top:0} + .response-head .response-code {background:#dfdfdf;padding:0 8px;font-weight: bold;line-height: 29px;font-size: 80%;} .response-head .response-message {padding:0 10px;background:#fbfbfb;line-height: 30px;font-size: 80%;} .request-compose {position:relative;} diff --git a/view/lean-api/route_descriptor.phtml b/view/lean-api/route_descriptor.phtml index 06fed63..61a63c7 100644 --- a/view/lean-api/route_descriptor.phtml +++ b/view/lean-api/route_descriptor.phtml @@ -10,8 +10,10 @@ {{ $route['cleaned'] }} - - - {{= $route['description'] }} + {% if ! empty($route['description']) %} + - + {{= $route['description'] }} + {% endif %}