diff --git a/skeleton/EntityGenerate/Api.php b/skeleton/EntityGenerate/Api.php index 6f0042f..480e47b 100644 --- a/skeleton/EntityGenerate/Api.php +++ b/skeleton/EntityGenerate/Api.php @@ -14,13 +14,13 @@ class %CLASSNAME% { use Lib\ApiTrait; #[Route(route: "/documentation", name: "api.%CLASSNAME_LC%:docs", method: "GET")] - public function index(ServerRequestInterface $request, array $attributes): ResponseInterface + public function index(ServerRequestInterface $request, array $arguments): ResponseInterface { return $this->renderMarkdown(getenv("PROJECT_PATH") . "/meta/docs/%CLASSNAME_LC%.md", [ %ENTITY_NS%\%CLASSNAME%::class, ], [ %FORM_NS%\%SAVE_FORM_CONTEXT_CLASSNAME%::class, %FORM_NS%\%DELETE_FORM_CONTEXT_CLASSNAME%::class ]); } #[Route("/", name: "api.%CLASSNAME_LC%:list", method: "GET")] - public function list(ServerRequestInterface $request, array $attributes) : ResponseInterface + public function list(ServerRequestInterface $request, array $arguments) : ResponseInterface { $request = $this->searchEntitiesFromRequest($request, %ENTITY_NS%\%CLASSNAME%::class); $result = $request->getAttribute("lean.searchRequest")[%ENTITY_NS%\%CLASSNAME%::class]; @@ -29,7 +29,7 @@ class %CLASSNAME% { } #[Route("/", name: "api.%CLASSNAME_LC%:add", method: "POST")] - public function add(ServerRequestInterface $request, array $attributes) : ResponseInterface + public function add(ServerRequestInterface $request, array $arguments) : ResponseInterface { $entity = new %ENTITY_NS%\%CLASSNAME%(); @@ -40,7 +40,7 @@ class %CLASSNAME% { #[Route("/{id:\d+}", name: "api.%CLASSNAME_LC%:single", method: "GET")] #[Route("/{id:\d+}", name: "api.%CLASSNAME_LC%:edit", method: "PATCH")] - public function single(ServerRequestInterface $request, array $attributes) : ResponseInterface + public function single(ServerRequestInterface $request, array $arguments) : ResponseInterface { $request = $this->searchEntityFromRequest($request, %ENTITY_NS%\%CLASSNAME%::class); $result = $request->getAttribute("lean.searchRequest")[%ENTITY_NS%\%CLASSNAME%::class]; @@ -51,7 +51,7 @@ class %CLASSNAME% { } #[Route("/{id:\d+}", name: "api.%CLASSNAME_LC%:delete", method: "DELETE")] - public function delete(ServerRequestInterface $request, array $attributes) : ResponseInterface + public function delete(ServerRequestInterface $request, array $arguments) : ResponseInterface { $request = $this->searchEntityFromRequest($request, %ENTITY_NS%\%CLASSNAME%::class); $result = $request->getAttribute("lean.searchRequest")[%ENTITY_NS%\%CLASSNAME%::class]; diff --git a/skeleton/EntityGenerate/SaveFormContext.php b/skeleton/EntityGenerate/SaveFormContext.php index 1befdb2..5d1eaa2 100644 --- a/skeleton/EntityGenerate/SaveFormContext.php +++ b/skeleton/EntityGenerate/SaveFormContext.php @@ -11,6 +11,10 @@ class %SAVE_FORM_CONTEXT_CLASSNAME% extends \Lean\Api\Lib\FormContext # #[ContextField] # public string $name; + public function initializeEntity(%ENTITY_NS%\%CLASSNAME% $entity): void + { + } + public function valid(? %ENTITY_NS%\%CLASSNAME% $entity = null) : bool { return parent::valid(); diff --git a/src/Action/Console/EntityGenerate.php b/src/Action/Console/EntityGenerate.php index cc6f1ac..44445b6 100644 --- a/src/Action/Console/EntityGenerate.php +++ b/src/Action/Console/EntityGenerate.php @@ -567,14 +567,11 @@ class EntityGenerate protected function replaceClassname(string $boilerplate) : string { - return match($this->apiBase ?? false) { - false => $boilerplate, - default => str_replace( - [ '%CLASSNAME_LC%', '%CLASSNAME%' ], - [ strtolower(preg_replace('/([a-z])([A-Z])/', '$1_$2', $this->name)), $this->name ], - $boilerplate - ) - }; + return str_replace( + [ '%CLASSNAME_LC%', '%CLASSNAME%' ], + [ strtolower(preg_replace('/([a-z])([A-Z])/', '$1_$2', $this->name)), $this->name ], + $boilerplate + ); } protected function readComposerNamespace() : ? string diff --git a/src/EntityDescriptor.php b/src/EntityDescriptor.php index 86af740..3a91b4c 100644 --- a/src/EntityDescriptor.php +++ b/src/EntityDescriptor.php @@ -7,6 +7,7 @@ use Lean\Factory\HttpFactory; use Notes\ObjectReflection; use Notes\Route\Attribute\Method\Route; use Ulmus\Attribute\Obj\Table; +use Ulmus\Attribute\Property\ArrayOf; use Ulmus\Attribute\Property\Field; use Ulmus\SearchRequest\Attribute\SearchParameter; use Ulmus\SearchRequest\Attribute\SearchRequestParameter; @@ -34,6 +35,7 @@ class EntityDescriptor $field = $property->getAttribute(Field::class); if ($field) { + $arrayOf = $property->getAttribute(ArrayOf::class); $types = $property->getTypes(); if ($property->value ?? false) { @@ -53,6 +55,7 @@ class EntityDescriptor 'length' => $field->object->length ?? null, 'readonly' => $field->object->readonly, 'default' => $default, + 'arrayOf' => $arrayOf ?? null, ]; } } diff --git a/src/Form/Form.php b/src/Form/Form.php index 64564f6..6a500b8 100644 --- a/src/Form/Form.php +++ b/src/Form/Form.php @@ -27,7 +27,7 @@ abstract class Form { if(isset($this->contextClass) && ! $context instanceof $this->contextClass ) { throw new \LogicException( - sprintf("Your context type should be a %s", $this->contextClass) + sprintf("Your context type should be a %s, received %s", $this->contextClass, $context::class) ); } } diff --git a/src/Form/Save.php b/src/Form/Save.php index 4b93c35..c66e26f 100644 --- a/src/Form/Save.php +++ b/src/Form/Save.php @@ -14,16 +14,21 @@ use Lean\Api\Attribute\EntityField; use Ulmus\Entity\EntityInterface; use Ulmus\Entity\Field\Datetime; +use Ulmus\EntityCollection; abstract class Save extends Form implements \Picea\Ui\Method\FormInterface { + protected bool $skipEntityCreatedAt = false; + + protected bool $skipEntityLastModified = false; + protected array $reflectedProperties; protected string $contextClass = FormContext::class; public function initialize(FormContextInterface $context) : void { - if ( ! $this->getEntity()->isLoaded() ) { + if ( $this->getEntity() instanceof EntityInterface && ! $this->getEntity()->isLoaded() ) { if (method_exists($context, 'initializeEntity')) { $context->initializeEntity($this->getEntity()); } @@ -46,13 +51,13 @@ abstract class Save extends Form implements \Picea\Ui\Method\FormInterface { { $entity = $this->getEntity(); - if ($entity->isLoaded()) { + if ($entity->isLoaded() && ! $this->skipEntityLastModified) { if (property_exists($entity, 'updatedAt') && $entity->repository()->generateDatasetDiff($entity) ) { $cls = $entity::resolveEntity()->field('updatedAt')->type->type; $entity->updatedAt = new $cls(); } } - else { + elseif (! $this->skipEntityCreatedAt) { if (property_exists($entity, 'createdAt') && empty($entity->createdAt)) { $cls = $entity::resolveEntity()->field('createdAt')->type->type; $entity->createdAt = new $cls(); diff --git a/src/Middleware/ApiRenderer.php b/src/Middleware/ApiRenderer.php index 90b9857..bf2b5e8 100644 --- a/src/Middleware/ApiRenderer.php +++ b/src/Middleware/ApiRenderer.php @@ -24,8 +24,6 @@ class ApiRenderer implements MiddlewareInterface { $response = $handler->handle($request); } catch(\Throwable $ex) { - - if (static::awaitingJson($request)) { return HttpFactory::createJsonResponse([ 'status' => 'failed', @@ -48,7 +46,7 @@ class ApiRenderer implements MiddlewareInterface { $payload = $response->getPayload(); # For now, we match only response having a 'data' field - if (isset($payload['data'])) { + if ( (is_array($payload) || $payload instanceof \ArrayAccess) && isset($payload['data'])) { return HttpFactory::createJsonResponse([ 'status' => 'success', 'ts' => time(), @@ -84,4 +82,19 @@ class ApiRenderer implements MiddlewareInterface { { return str_contains(strtolower($request->getHeaderLine('content-type')), 'json'); } + + public static function encodeHtml(iterable& $array) : iterable + { + foreach($array as &$item) { + if (is_array($item)) { + $item = static::encodeHtml($item); + } + elseif (is_string($item)) { + dump($item); + $item = htmlspecialchars($item); + } + } + + return $array; + } } diff --git a/src/RouteDescriptor.php b/src/RouteDescriptor.php index ff0cd61..079b770 100644 --- a/src/RouteDescriptor.php +++ b/src/RouteDescriptor.php @@ -35,19 +35,20 @@ class RouteDescriptor $attribute = $reflector->reflectClass()->getAttribute(\Notes\Route\Attribute\Object\Route::class); - $base = $attribute ? $attribute->object->base : ""; + $base = $attribute ? $attribute->object->getBase() : ""; foreach($reflector->reflectMethods() as $method) { foreach(array_reverse($method->getAttributes(Route::class)) as $routeAttribute) { + $itemBase = $routeAttribute->object->getBase() ?? $base; $route = $routeAttribute->object; $path = rtrim($route->route, '/'); - $cleaned = $this->cleanRouteFromRegex($base.$path); + $cleaned = $this->cleanRouteFromRegex($itemBase.$path); $url = $this->urlExtension->buildUrl($cleaned); $routes[] = [ 'name' => $route->name, 'route' => $url, - 'path' => $base.$path, + 'path' => $itemBase.$path, 'cleaned' => $cleaned, 'description'=> $route->description, #'methods' =>implode(', ', (array)$route->method), diff --git a/view/lean-api/entity_descriptor.phtml b/view/lean-api/entity_descriptor.phtml index 8144456..0f64b34 100644 --- a/view/lean-api/entity_descriptor.phtml +++ b/view/lean-api/entity_descriptor.phtml @@ -1,5 +1,37 @@ {% language.set "lean.api.descriptor.entity" %} +{% function describeArrayOf($arrayOf) %} + {% php + $reflection = new \ReflectionClass($arrayOf->object->type); + $constructor = $reflection->getConstructor(); + + if (!$constructor) { + return null; + } + + $params = []; + + foreach ($constructor->getParameters() as $param) { + $paramStr = ''; + + if ($param->getType()) { + $paramStr .= (string)$param->getType() . ' '; + } + + $paramStr .= '$' . $param->getName(); + + if ($param->isDefaultValueAvailable()) { + $default = $param->getDefaultValue(); + $paramStr .= ' = ' . var_export($default, true); + } + + $params[] = $paramStr; + } + + return sprintf("%s (%s)", $reflection->getShortName(), implode(', ', $params)); + %} +{% endfunction %} + {% function yesOrNo(bool $toggle) : void %} {{ $toggle ? 'oui' : 'non' }} {% endfunction %} @@ -28,10 +60,13 @@ -
+
Champ SQL : {{ $field['fieldName'] }}
Attribut : {{ $field['tag'] }}
Type(s) : {{ $field['type'] }}
+ {% if $field['arrayOf'] %} +
Définition : {{ describeArrayOf($field['arrayOf']) }}
+ {% endif %}
Nullable {{ yesOrNo($field['allowNulls']) }}
{% if $field['default'] %}
Valeur par défaut : {{ $field['default'] }}
diff --git a/view/lean-api/form_descriptor.phtml b/view/lean-api/form_descriptor.phtml index 404088a..10edc84 100644 --- a/view/lean-api/form_descriptor.phtml +++ b/view/lean-api/form_descriptor.phtml @@ -44,7 +44,7 @@
Exemple : {{ $field['example'] }}
{% endif %} {% if $field['default'] !== null %} -
Default : {{ $field['default'] }}
+
Valeur par défaut : {{ $field['default'] }}
{% endif %} {% if $field['values'] %}
Valeurs possibles : [ {{= implode(', ', $field['values']) }} ]
diff --git a/view/lean-api/request_debugger.phtml b/view/lean-api/request_debugger.phtml index 39c55fd..c1bd086 100644 --- a/view/lean-api/request_debugger.phtml +++ b/view/lean-api/request_debugger.phtml @@ -156,7 +156,6 @@ launchRequest(requestMethod, input.value, editor.getValue()) .then((response) => { - console.log(response); responseHead.querySelector('.response-code').innerText = response.status; responseHead.querySelector('.response-message').innerText = response.statusText; @@ -165,10 +164,10 @@ return response.text(); }) .then(body => { - console.log(aceMode); if (aceMode === "json") { body = JSON.stringify(JSON.parse(body), null, 2); - responseEditorElement.innerHTML = body; + + responseEditorElement.innerHTML = DOMEncode(body); } else { responseEditorElement.innerText = body; @@ -285,4 +284,11 @@ } catch(e) {} } + + function DOMEncode(text) { + const tempElement = document.createElement('div'); + tempElement.textContent = text; + + return tempElement.innerHTML; + } \ No newline at end of file diff --git a/view/lean/layout/docs.phtml b/view/lean/layout/docs.phtml index c6da57e..b10df01 100644 --- a/view/lean/layout/docs.phtml +++ b/view/lean/layout/docs.phtml @@ -81,6 +81,8 @@ .entity-wrapper .entity-name {background:#eaa1af;color: #682828;font-size:110%} .entity-wrapper .fields-wrapper {border-color: #c14141} .entity-wrapper .header-fields {background:#c14141} + .entity-wrapper .field-desc {margin-top:10px;;background:#fff;padding:5px;font-size:0.9em} + .entity-wrapper .field-desc .array-of {font-weight:bold;padding-left:15px;color:#955252} .entity-wrapper ol {background: #e3d0d0;} .entity-wrapper li {border-color: #ae8585;} .entity-wrapper li .default {color:#bf7d4d;}