- 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;
#[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];

View File

@ -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();

View File

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

View File

@ -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,
];
}
}

View File

@ -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)
);
}
}

View File

@ -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();

View File

@ -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;
}
}

View File

@ -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),

View File

@ -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 %}
<span style="color: {{ $toggle ? 'green' : '#ac1b1b' }}">{{ $toggle ? 'oui' : 'non' }}</span>
{% endfunction %}
@ -28,10 +60,13 @@
</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="tag"><u>Attribut</u> : {{ $field['tag'] }}</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>
{% if $field['default'] %}
<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>
{% endif %}
{% if $field['default'] !== null %}
<div><u>Default</u> : {{ $field['default'] }}</div>
<div><u>Valeur par défaut</u> : {{ $field['default'] }}</div>
{% endif %}
{% if $field['values'] %}
<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())
.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;
}
</script>

View File

@ -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;}