- Some work done on entity generation

This commit is contained in:
Dave Mc Nicoll 2026-05-21 14:58:21 +00:00
parent 05975089e5
commit 2dc71cd350
12 changed files with 94 additions and 28 deletions

View File

@ -14,13 +14,13 @@ class %CLASSNAME% {
use Lib\ApiTrait; use Lib\ApiTrait;
#[Route(route: "/documentation", name: "api.%CLASSNAME_LC%:docs", method: "GET")] #[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 ]); 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")] #[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); $request = $this->searchEntitiesFromRequest($request, %ENTITY_NS%\%CLASSNAME%::class);
$result = $request->getAttribute("lean.searchRequest")[%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")] #[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%(); $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%:single", method: "GET")]
#[Route("/{id:\d+}", name: "api.%CLASSNAME_LC%:edit", method: "PATCH")] #[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); $request = $this->searchEntityFromRequest($request, %ENTITY_NS%\%CLASSNAME%::class);
$result = $request->getAttribute("lean.searchRequest")[%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")] #[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); $request = $this->searchEntityFromRequest($request, %ENTITY_NS%\%CLASSNAME%::class);
$result = $request->getAttribute("lean.searchRequest")[%ENTITY_NS%\%CLASSNAME%::class]; $result = $request->getAttribute("lean.searchRequest")[%ENTITY_NS%\%CLASSNAME%::class];

View File

@ -11,6 +11,10 @@ class %SAVE_FORM_CONTEXT_CLASSNAME% extends \Lean\Api\Lib\FormContext
# #[ContextField] # #[ContextField]
# public string $name; # public string $name;
public function initializeEntity(%ENTITY_NS%\%CLASSNAME% $entity): void
{
}
public function valid(? %ENTITY_NS%\%CLASSNAME% $entity = null) : bool public function valid(? %ENTITY_NS%\%CLASSNAME% $entity = null) : bool
{ {
return parent::valid(); return parent::valid();

View File

@ -567,14 +567,11 @@ class EntityGenerate
protected function replaceClassname(string $boilerplate) : string protected function replaceClassname(string $boilerplate) : string
{ {
return match($this->apiBase ?? false) { return str_replace(
false => $boilerplate,
default => str_replace(
[ '%CLASSNAME_LC%', '%CLASSNAME%' ], [ '%CLASSNAME_LC%', '%CLASSNAME%' ],
[ strtolower(preg_replace('/([a-z])([A-Z])/', '$1_$2', $this->name)), $this->name ], [ strtolower(preg_replace('/([a-z])([A-Z])/', '$1_$2', $this->name)), $this->name ],
$boilerplate $boilerplate
) );
};
} }
protected function readComposerNamespace() : ? string protected function readComposerNamespace() : ? string

View File

@ -7,6 +7,7 @@ use Lean\Factory\HttpFactory;
use Notes\ObjectReflection; use Notes\ObjectReflection;
use Notes\Route\Attribute\Method\Route; use Notes\Route\Attribute\Method\Route;
use Ulmus\Attribute\Obj\Table; use Ulmus\Attribute\Obj\Table;
use Ulmus\Attribute\Property\ArrayOf;
use Ulmus\Attribute\Property\Field; use Ulmus\Attribute\Property\Field;
use Ulmus\SearchRequest\Attribute\SearchParameter; use Ulmus\SearchRequest\Attribute\SearchParameter;
use Ulmus\SearchRequest\Attribute\SearchRequestParameter; use Ulmus\SearchRequest\Attribute\SearchRequestParameter;
@ -34,6 +35,7 @@ class EntityDescriptor
$field = $property->getAttribute(Field::class); $field = $property->getAttribute(Field::class);
if ($field) { if ($field) {
$arrayOf = $property->getAttribute(ArrayOf::class);
$types = $property->getTypes(); $types = $property->getTypes();
if ($property->value ?? false) { if ($property->value ?? false) {
@ -53,6 +55,7 @@ class EntityDescriptor
'length' => $field->object->length ?? null, 'length' => $field->object->length ?? null,
'readonly' => $field->object->readonly, 'readonly' => $field->object->readonly,
'default' => $default, 'default' => $default,
'arrayOf' => $arrayOf ?? null,
]; ];
} }
} }

View File

@ -27,7 +27,7 @@ abstract class Form
{ {
if(isset($this->contextClass) && ! $context instanceof $this->contextClass ) { if(isset($this->contextClass) && ! $context instanceof $this->contextClass ) {
throw new \LogicException( 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)
); );
} }
} }

View File

@ -14,16 +14,21 @@ use Lean\Api\Attribute\EntityField;
use Ulmus\Entity\EntityInterface; use Ulmus\Entity\EntityInterface;
use Ulmus\Entity\Field\Datetime; use Ulmus\Entity\Field\Datetime;
use Ulmus\EntityCollection;
abstract class Save extends Form implements \Picea\Ui\Method\FormInterface { abstract class Save extends Form implements \Picea\Ui\Method\FormInterface {
protected bool $skipEntityCreatedAt = false;
protected bool $skipEntityLastModified = false;
protected array $reflectedProperties; protected array $reflectedProperties;
protected string $contextClass = FormContext::class; protected string $contextClass = FormContext::class;
public function initialize(FormContextInterface $context) : void public function initialize(FormContextInterface $context) : void
{ {
if ( ! $this->getEntity()->isLoaded() ) { if ( $this->getEntity() instanceof EntityInterface && ! $this->getEntity()->isLoaded() ) {
if (method_exists($context, 'initializeEntity')) { if (method_exists($context, 'initializeEntity')) {
$context->initializeEntity($this->getEntity()); $context->initializeEntity($this->getEntity());
} }
@ -46,13 +51,13 @@ abstract class Save extends Form implements \Picea\Ui\Method\FormInterface {
{ {
$entity = $this->getEntity(); $entity = $this->getEntity();
if ($entity->isLoaded()) { if ($entity->isLoaded() && ! $this->skipEntityLastModified) {
if (property_exists($entity, 'updatedAt') && $entity->repository()->generateDatasetDiff($entity) ) { if (property_exists($entity, 'updatedAt') && $entity->repository()->generateDatasetDiff($entity) ) {
$cls = $entity::resolveEntity()->field('updatedAt')->type->type; $cls = $entity::resolveEntity()->field('updatedAt')->type->type;
$entity->updatedAt = new $cls(); $entity->updatedAt = new $cls();
} }
} }
else { elseif (! $this->skipEntityCreatedAt) {
if (property_exists($entity, 'createdAt') && empty($entity->createdAt)) { if (property_exists($entity, 'createdAt') && empty($entity->createdAt)) {
$cls = $entity::resolveEntity()->field('createdAt')->type->type; $cls = $entity::resolveEntity()->field('createdAt')->type->type;
$entity->createdAt = new $cls(); $entity->createdAt = new $cls();

View File

@ -24,8 +24,6 @@ class ApiRenderer implements MiddlewareInterface {
$response = $handler->handle($request); $response = $handler->handle($request);
} }
catch(\Throwable $ex) { catch(\Throwable $ex) {
if (static::awaitingJson($request)) { if (static::awaitingJson($request)) {
return HttpFactory::createJsonResponse([ return HttpFactory::createJsonResponse([
'status' => 'failed', 'status' => 'failed',
@ -48,7 +46,7 @@ class ApiRenderer implements MiddlewareInterface {
$payload = $response->getPayload(); $payload = $response->getPayload();
# For now, we match only response having a 'data' field # 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([ return HttpFactory::createJsonResponse([
'status' => 'success', 'status' => 'success',
'ts' => time(), 'ts' => time(),
@ -84,4 +82,19 @@ class ApiRenderer implements MiddlewareInterface {
{ {
return str_contains(strtolower($request->getHeaderLine('content-type')), 'json'); 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;
}
} }

View File

@ -35,19 +35,20 @@ class RouteDescriptor
$attribute = $reflector->reflectClass()->getAttribute(\Notes\Route\Attribute\Object\Route::class); $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($reflector->reflectMethods() as $method) {
foreach(array_reverse($method->getAttributes(Route::class)) as $routeAttribute) { foreach(array_reverse($method->getAttributes(Route::class)) as $routeAttribute) {
$itemBase = $routeAttribute->object->getBase() ?? $base;
$route = $routeAttribute->object; $route = $routeAttribute->object;
$path = rtrim($route->route, '/'); $path = rtrim($route->route, '/');
$cleaned = $this->cleanRouteFromRegex($base.$path); $cleaned = $this->cleanRouteFromRegex($itemBase.$path);
$url = $this->urlExtension->buildUrl($cleaned); $url = $this->urlExtension->buildUrl($cleaned);
$routes[] = [ $routes[] = [
'name' => $route->name, 'name' => $route->name,
'route' => $url, 'route' => $url,
'path' => $base.$path, 'path' => $itemBase.$path,
'cleaned' => $cleaned, 'cleaned' => $cleaned,
'description'=> $route->description, 'description'=> $route->description,
#'methods' =>implode(', ', (array)$route->method), #'methods' =>implode(', ', (array)$route->method),

View File

@ -1,5 +1,37 @@
{% language.set "lean.api.descriptor.entity" %} {% 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 %} {% function yesOrNo(bool $toggle) : void %}
<span style="color: {{ $toggle ? 'green' : '#ac1b1b' }}">{{ $toggle ? 'oui' : 'non' }}</span> <span style="color: {{ $toggle ? 'green' : '#ac1b1b' }}">{{ $toggle ? 'oui' : 'non' }}</span>
{% endfunction %} {% endfunction %}
@ -28,10 +60,13 @@
</div> </div>
</div> </div>
<div class="field-desc" style="margin-top:10px;;background:#fff;padding:5px;font-size:0.9em"> <div class="field-desc">
<div class="fieldname"><u>Champ SQL</u> : {{ $field['fieldName'] }}</div> <div class="fieldname"><u>Champ SQL</u> : {{ $field['fieldName'] }}</div>
<div class="tag"><u>Attribut</u> : {{ $field['tag'] }}</div> <div class="tag"><u>Attribut</u> : {{ $field['tag'] }}</div>
<div class="type"><u>Type(s)</u> : {{ $field['type'] }}</div> <div class="type"><u>Type(s)</u> : {{ $field['type'] }}</div>
{% if $field['arrayOf'] %}
<div class="array-of"><u>Définition</u> : {{ describeArrayOf($field['arrayOf']) }}</div>
{% endif %}
<div class="nullable"><u>Nullable</u> {{ yesOrNo($field['allowNulls']) }}</div> <div class="nullable"><u>Nullable</u> {{ yesOrNo($field['allowNulls']) }}</div>
{% if $field['default'] %} {% if $field['default'] %}
<div class="default"><u>Valeur par défaut</u> : {{ $field['default'] }}</div> <div class="default"><u>Valeur par défaut</u> : {{ $field['default'] }}</div>

View File

@ -44,7 +44,7 @@
<div><u>Exemple</u> : {{ $field['example'] }}</div> <div><u>Exemple</u> : {{ $field['example'] }}</div>
{% endif %} {% endif %}
{% if $field['default'] !== null %} {% if $field['default'] !== null %}
<div><u>Default</u> : {{ $field['default'] }}</div> <div><u>Valeur par défaut</u> : {{ $field['default'] }}</div>
{% endif %} {% endif %}
{% if $field['values'] %} {% if $field['values'] %}
<div><u>Valeurs possibles</u> : [ <u>{{= implode('</u>, <u>', $field['values']) }}</u> ]</div> <div><u>Valeurs possibles</u> : [ <u>{{= implode('</u>, <u>', $field['values']) }}</u> ]</div>

View File

@ -156,7 +156,6 @@
launchRequest(requestMethod, input.value, editor.getValue()) launchRequest(requestMethod, input.value, editor.getValue())
.then((response) => { .then((response) => {
console.log(response);
responseHead.querySelector('.response-code').innerText = response.status; responseHead.querySelector('.response-code').innerText = response.status;
responseHead.querySelector('.response-message').innerText = response.statusText; responseHead.querySelector('.response-message').innerText = response.statusText;
@ -165,10 +164,10 @@
return response.text(); return response.text();
}) })
.then(body => { .then(body => {
console.log(aceMode);
if (aceMode === "json") { if (aceMode === "json") {
body = JSON.stringify(JSON.parse(body), null, 2); body = JSON.stringify(JSON.parse(body), null, 2);
responseEditorElement.innerHTML = body;
responseEditorElement.innerHTML = DOMEncode(body);
} }
else { else {
responseEditorElement.innerText = body; responseEditorElement.innerText = body;
@ -285,4 +284,11 @@
} }
catch(e) {} catch(e) {}
} }
function DOMEncode(text) {
const tempElement = document.createElement('div');
tempElement.textContent = text;
return tempElement.innerHTML;
}
</script> </script>

View File

@ -81,6 +81,8 @@
.entity-wrapper .entity-name {background:#eaa1af;color: #682828;font-size:110%} .entity-wrapper .entity-name {background:#eaa1af;color: #682828;font-size:110%}
.entity-wrapper .fields-wrapper {border-color: #c14141} .entity-wrapper .fields-wrapper {border-color: #c14141}
.entity-wrapper .header-fields {background:#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 ol {background: #e3d0d0;}
.entity-wrapper li {border-color: #ae8585;} .entity-wrapper li {border-color: #ae8585;}
.entity-wrapper li .default {color:#bf7d4d;} .entity-wrapper li .default {color:#bf7d4d;}