From c3e3d6a73af1b14c3dd9f0f9f58d7e4b5efe37fe Mon Sep 17 00:00:00 2001 From: Dave M Date: Mon, 5 Jan 2026 12:39:41 +0000 Subject: [PATCH] - New code generation made for CLI --- meta/definitions/software.php | 6 +- skeleton/EntityGenerate/Api.php | 63 ++ skeleton/EntityGenerate/Controller.php | 66 ++ skeleton/EntityGenerate/DeleteForm.php | 20 + skeleton/EntityGenerate/DeleteFormContext.php | 22 + skeleton/EntityGenerate/Entity.php | 43 ++ skeleton/EntityGenerate/FactoryAppend.php | 20 + .../EntityGenerate/FactoryInterfaceAppend.php | 5 + skeleton/EntityGenerate/FactoryTrait.php | 32 + skeleton/EntityGenerate/SaveForm.php | 20 + skeleton/EntityGenerate/SaveFormContext.php | 18 + skeleton/EntityGenerate/api-docs.md | 21 + src/Action/Console/EntityGenerate.php | 599 ++++++++++++++++++ src/Cli/CodeGeneration.php | 120 ++++ src/Controller/Debug.php | 2 +- src/Controller/Debug/Errors.php | 2 +- src/Form/Delete.php | 11 - src/Form/Form.php | 7 + src/Form/Save.php | 4 +- view/lean-api/entity-generate/create.phtml | 1 + view/lean-api/entity-generate/edit.phtml | 1 + view/lean-api/entity-generate/index.phtml | 1 + 22 files changed, 1067 insertions(+), 17 deletions(-) create mode 100644 skeleton/EntityGenerate/Api.php create mode 100644 skeleton/EntityGenerate/Controller.php create mode 100644 skeleton/EntityGenerate/DeleteForm.php create mode 100644 skeleton/EntityGenerate/DeleteFormContext.php create mode 100644 skeleton/EntityGenerate/Entity.php create mode 100644 skeleton/EntityGenerate/FactoryAppend.php create mode 100644 skeleton/EntityGenerate/FactoryInterfaceAppend.php create mode 100644 skeleton/EntityGenerate/FactoryTrait.php create mode 100644 skeleton/EntityGenerate/SaveForm.php create mode 100644 skeleton/EntityGenerate/SaveFormContext.php create mode 100644 skeleton/EntityGenerate/api-docs.md create mode 100644 src/Action/Console/EntityGenerate.php create mode 100644 src/Cli/CodeGeneration.php create mode 100644 view/lean-api/entity-generate/create.phtml create mode 100644 view/lean-api/entity-generate/edit.phtml create mode 100644 view/lean-api/entity-generate/index.phtml diff --git a/meta/definitions/software.php b/meta/definitions/software.php index 8847256..a3df986 100644 --- a/meta/definitions/software.php +++ b/meta/definitions/software.php @@ -2,7 +2,7 @@ use Ulmus\ConnectionAdapter; -putenv('LEAN_API_PROJECT_PATH=' . $path = dirname(__DIR__, 2)); +define('LEAN_API_PROJECT_PATH', $path = dirname(__DIR__, 2)); return [ 'lean.api' => [ @@ -24,6 +24,10 @@ return [ ], ], + 'cli' => [ + 'Lean\\Api\\Cli' => implode(DIRECTORY_SEPARATOR, [ $path, "src", "Cli", "" ]), + ], + 'ulmus' => [ 'entities' => [ 'Lean\\Api\\Entity' => implode(DIRECTORY_SEPARATOR, [ $path, 'src', 'Entity', '' ]) ], 'adapters' => [ diff --git a/skeleton/EntityGenerate/Api.php b/skeleton/EntityGenerate/Api.php new file mode 100644 index 0000000..6f0042f --- /dev/null +++ b/skeleton/EntityGenerate/Api.php @@ -0,0 +1,63 @@ +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 + { + $request = $this->searchEntitiesFromRequest($request, %ENTITY_NS%\%CLASSNAME%::class); + $result = $request->getAttribute("lean.searchRequest")[%ENTITY_NS%\%CLASSNAME%::class]; + + return $this->outputSearchResult($result); + } + + #[Route("/", name: "api.%CLASSNAME_LC%:add", method: "POST")] + public function add(ServerRequestInterface $request, array $attributes) : ResponseInterface + { + $entity = new %ENTITY_NS%\%CLASSNAME%(); + + form($this->formFactory->%SAVE_FORM_CLASSNAME_LC%($entity), $this->formFactory->%SAVE_FORM_CONTEXT_CLASSNAME_LC%($request)); + + return $this->output($entity, (int) $entity->isLoaded()); + } + + #[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 + { + $request = $this->searchEntityFromRequest($request, %ENTITY_NS%\%CLASSNAME%::class); + $result = $request->getAttribute("lean.searchRequest")[%ENTITY_NS%\%CLASSNAME%::class]; + + form($this->formFactory->%SAVE_FORM_CLASSNAME_LC%($result->getResult()), $this->formFactory->%SAVE_FORM_CONTEXT_CLASSNAME_LC%($request)); + + return $this->outputSearchResult($result); + } + + #[Route("/{id:\d+}", name: "api.%CLASSNAME_LC%:delete", method: "DELETE")] + public function delete(ServerRequestInterface $request, array $attributes) : ResponseInterface + { + $request = $this->searchEntityFromRequest($request, %ENTITY_NS%\%CLASSNAME%::class); + $result = $request->getAttribute("lean.searchRequest")[%ENTITY_NS%\%CLASSNAME%::class]; + + form($this->formFactory->%DELETE_FORM_CLASSNAME_LC%($result->getResult()), $this->formFactory->%DELETE_FORM_CONTEXT_CLASSNAME_LC%($request)); + + return $this->outputSearchResult($result); + } +} \ No newline at end of file diff --git a/skeleton/EntityGenerate/Controller.php b/skeleton/EntityGenerate/Controller.php new file mode 100644 index 0000000..d86b338 --- /dev/null +++ b/skeleton/EntityGenerate/Controller.php @@ -0,0 +1,66 @@ +fromRequest($request) ; + $list = %ENTITY_NS%\%CLASSNAME%::repository()->filterServerRequest($search)->loadAll(); + + return $this->renderView('%CLASSNAME_LC%/index', get_defined_vars()); + } + + #[Route("/create", name: "%CLASSNAME_LC%:create", method: ["GET", "POST"])] + ##[Breadcrumb(parent: "%CLASSNAME_LC%:index", icon: "", lang: "app.lang.breadcrumb")] + public function create(ServerRequestInterface $request, array $arguments) : ResponseInterface + { + $entity = new %ENTITY_NS%\%CLASSNAME%(); + + form($this->formFactory->%SAVE_FORM_CLASSNAME_LC%($entity), $this->pushContext($this->formFactory->%SAVE_FORM_CONTEXT_CLASSNAME_LC%($request, "%CLASSNAME_LC%.create"))); + + if ($entity->isLoaded()) { + return $this->redirect(route("%CLASSNAME_LC%:edit", [ 'id' => $entity->id ]) ); + } + + return $this->renderView('%CLASSNAME_LC%/create', get_defined_vars()); + } + + #[Route("/{id:\d+}/edit", name: "%CLASSNAME_LC%:edit", method: ["GET", "POST"])] + ##[Breadcrumb(parent: "%CLASSNAME_LC%:index", icon: "", lang: "app.lang.breadcrumb")] + public function edit(ServerRequestInterface $request, array $arguments) : ResponseInterface + { + $search = %ENTITY_NS%\%CLASSNAME%::searchRequest()->fromRequest($request); + $entity = %ENTITY_NS%\%CLASSNAME%::repository()->filterServerRequest($search)->loadOne(); + + form($this->formFactory->%SAVE_FORM_CLASSNAME_LC%($entity), $this->pushContext($this->formFactory->%SAVE_FORM_CONTEXT_CLASSNAME_LC%($request, "%CLASSNAME_LC%.edit"))); + + return $this->renderView('%CLASSNAME_LC%/edit', get_defined_vars()); + } + + #[Route("/{id:\d+}/delete", name: "%CLASSNAME_LC%:delete", method: ["POST", "DELETE"])] + public function delete(ServerRequestInterface $request, array $arguments) : ResponseInterface + { + $search = %ENTITY_NS%\%CLASSNAME%::searchRequest()->fromRequest($request); + $entity = %ENTITY_NS%\%CLASSNAME%::repository()->filterServerRequest($search)->loadOne(); + + form($this->formFactory->%DELETE_FORM_CLASSNAME_LC%($entity), $this->pushContext($this->formFactory->%DELETE_FORM_CONTEXT_CLASSNAME_LC%($request, "%CLASSNAME_LC%.delete"))); + + return $this->redirect(route("%CLASSNAME_LC%:index")); + } +} diff --git a/skeleton/EntityGenerate/DeleteForm.php b/skeleton/EntityGenerate/DeleteForm.php new file mode 100644 index 0000000..0144d78 --- /dev/null +++ b/skeleton/EntityGenerate/DeleteForm.php @@ -0,0 +1,20 @@ +getEntity()->isLoaded() ) { + $this->pushErrorMessage('lean.api.form.delete.error.entity'); + } + + return parent::valid(); + } +} \ No newline at end of file diff --git a/skeleton/EntityGenerate/Entity.php b/skeleton/EntityGenerate/Entity.php new file mode 100644 index 0000000..10fd8fe --- /dev/null +++ b/skeleton/EntityGenerate/Entity.php @@ -0,0 +1,43 @@ +languageHandler, $this->message, $entity); + } + + public function %SAVE_FORM_CONTEXT_CLASSNAME_LC%(ServerRequestInterface $request, ? string $formName = ''): FormContextInterface + { + return new %FORM_NS%\%SAVE_FORM_CONTEXT_CLASSNAME%($this->languageHandler, $this->message, $request, $formName); + } + + public function %DELETE_FORM_CLASSNAME_LC%(EntityInterface|%ENTITY_NS%\%CLASSNAME% $entity): FormInterface + { + return new %FORM_NS%\%DELETE_FORM_CLASSNAME%($this->languageHandler, $this->message, $entity); + } + + public function %DELETE_FORM_CONTEXT_CLASSNAME_LC%(ServerRequestInterface $request, ? string $formName = ''): FormContextInterface + { + return new %FORM_NS%\%DELETE_FORM_CONTEXT_CLASSNAME%($this->languageHandler, $this->message, $request, $formName); + } +} \ No newline at end of file diff --git a/skeleton/EntityGenerate/FactoryInterfaceAppend.php b/skeleton/EntityGenerate/FactoryInterfaceAppend.php new file mode 100644 index 0000000..a1ce7db --- /dev/null +++ b/skeleton/EntityGenerate/FactoryInterfaceAppend.php @@ -0,0 +1,5 @@ + public function %SAVE_FORM_CLASSNAME_LC%(EntityInterface|%ENTITY_NS%\%CLASSNAME% $entity): FormInterface; + public function %SAVE_FORM_CONTEXT_CLASSNAME_LC%(ServerRequestInterface $request, ? string $formName = ''): FormContextInterface; + public function %DELETE_FORM_CLASSNAME_LC%(EntityInterface|%ENTITY_NS%\%CLASSNAME% $entity): FormInterface; + public function %DELETE_FORM_CONTEXT_CLASSNAME_LC%(ServerRequestInterface $request, ? string $formName = ''): FormContextInterface; +} \ No newline at end of file diff --git a/skeleton/EntityGenerate/FactoryTrait.php b/skeleton/EntityGenerate/FactoryTrait.php new file mode 100644 index 0000000..5a2f6f8 --- /dev/null +++ b/skeleton/EntityGenerate/FactoryTrait.php @@ -0,0 +1,32 @@ +languageHandler, $this->messageFactory, $entity); + } + + public function %SAVE_FORM_CONTEXT_CLASSNAME_LC%(ServerRequestInterface $request, ? string $formName = ''): FormContextInterface + { + return new %FORM_NS%\%SAVE_FORM_CONTEXT_CLASSNAME%($this->languageHandler, $this->messageFactory, $request, $formName); + } + + public function %DELETE_FORM_CLASSNAME_LC%(EntityInterface|%ENTITY_NS%\%CLASSNAME% $entity): FormInterface + { + return new %FORM_NS%\%DELETE_FORM_CLASSNAME%($this->languageHandler, $this->messageFactory, $entity); + } + + public function %DELETE_FORM_CONTEXT_CLASSNAME_LC%(ServerRequestInterface $request, ? string $formName = ''): FormContextInterface + { + return new %FORM_NS%\%DELETE_FORM_CONTEXT_CLASSNAME%($this->languageHandler, $this->messageFactory, $request, $formName); + } +} \ No newline at end of file diff --git a/skeleton/EntityGenerate/SaveForm.php b/skeleton/EntityGenerate/SaveForm.php new file mode 100644 index 0000000..bc2e024 --- /dev/null +++ b/skeleton/EntityGenerate/SaveForm.php @@ -0,0 +1,20 @@ +pathFromNamespace($this->entityNamespace, $this->application->entities); + + if (empty($entityPath)) { + throw new \InvalidArgumentException("Could not find entities path from given namespaces."); + } + + if (isset($this->webNamespace)) { + $webPath = $this->pathFromNamespace($this->webNamespace, $this->application->routes); + + if (empty($webPath)) { + throw new \InvalidArgumentException("Could not find web path routes from given namespaces."); + } + } + + if (isset($this->apiNamespace)) { + $apiPath = $this->pathFromNamespace($this->apiNamespace, $this->application->routes); + + if (empty($apiPath)) { + throw new \InvalidArgumentException("Could not find web path routes from given namespaces."); + } + + if ($this->includeDocs) { + $docPath = implode(DIRECTORY_SEPARATOR, [ getenv('META_PATH'), "docs" ]); + $docPath = $this->readline(sprintf("Path of API's documentation output (default: %s) : ", $docPath), $docPath); + } + } + + if (isset($this->formNamespace)) { + $formPath = implode(DIRECTORY_SEPARATOR, [ $this->pathFromPSR($this->formNamespace), $this->formNamespace ]); + $formPath = $this->readline(sprintf("Path of Forms (default: %s) : ", $formPath), $formPath); + } + + if (isset($this->factoryNamespace)) { + $factoryPath = implode(DIRECTORY_SEPARATOR, [ $this->pathFromPSR($this->factoryNamespace), $this->factoryNamespace ]); + $factoryPath = $this->readline(sprintf("Path of Factory (default: %s) : ", $factoryPath), $factoryPath); + } + + if ( Terminal::proposeListChoice("Input completed, write to files ?", [ true => 'Yes', false => 'No', ]) ) { + $this->writeToFile($entityPath, $this->name, $this->compiledEntity, ); + + if (isset($webPath)) { + $this->writeToFile($webPath, $this->name, $this->compiledWeb, ); + } + + if (isset($apiPath)) { + $this->writeToFile($apiPath, $this->name, $this->compiledApi, ); + + if ($this->includeDocs) { + $this->writeToFile($docPath , strtolower($this->name), $this->compiledDocs, ".md"); + } + } + + if (isset($formPath)) { + if ($this->saveForm) { + $this->writeToFile($formPath, $this->saveForm, $this->compiledSaveForm,); + $this->writeToFile($formPath, $this->saveContext, $this->compiledSaveFormContext,); + } + + if ($this->deleteForm) { + $this->writeToFile($formPath, $this->deleteForm, $this->compiledDeleteForm,); + $this->writeToFile($formPath, $this->deleteContext, $this->compiledDeleteFormContext,); + } + + if (isset($this->factoryNamespace)) { + if (isset($this->factoryTraitNamespace)) { + $traitName = $this->name . "Trait"; + + $this->writeToFile(implode(DIRECTORY_SEPARATOR, [$factoryPath, "Factories"]), $traitName, $this->compiledFactoryTrait,); + $this->appendToFile($factoryPath, $this->factoryFile, "\tuse Factories\\$traitName;\n}", ""); + } + else { + $this->appendToFile($factoryPath, $this->factoryFile, $this->compiledFactory, ""); + } + + $this->appendToFile($factoryPath, $this->factoryInterfaceFile, $this->compiledFactoryInterface, ""); + } + } + } + } + + protected function appendToFile(string $path, string $filename, string $content, string $extension = ".php"): void + { + $fullpath = implode(DIRECTORY_SEPARATOR, [ rtrim($path, DIRECTORY_SEPARATOR), $filename . "$extension" ]); + + $existingFileContent = file_get_contents($fullpath); + + $this->writeToFile($path, $filename, rtrim($existingFileContent, '} ') . $content, $extension); + } + + protected function writeToFile(string $path, string $filename, string $content, string $extension = ".php"): void + { + $fullpath = implode(DIRECTORY_SEPARATOR, [ rtrim($path, DIRECTORY_SEPARATOR), $filename . "$extension" ]); + + if (file_exists($fullpath)) { + if ((! $this->overwrite) && Terminal::proposeListChoice(sprintf("Trying to write over an existing file '%s',\nOverwrite file ?", $fullpath), [ false => 'Yes', true => 'No', ])) { + return; + } + } + elseif (! file_exists(dirname($fullpath))) { + mkdir(dirname($fullpath), 0755, true); + } + + file_put_contents($fullpath, $content, \LOCK_EX); + + Terminal::sprintfline("File successfully written '%s'", $fullpath); + } + + protected function pathFromNamespace(string $namespace, array $source) : false|string + { + $split = explode('\\', $namespace); + + while($split) { + $ns = $this->namespace . "\\" . implode('\\', $split); + + if ( isset($source[$ns]) ) { + return rtrim($source[$ns], DIRECTORY_SEPARATOR); + } + + array_pop($split); + } + + return false; + } + + protected function pathFromPSR(string $namespace) : false|string + { + $source = $this->readComposerPsr4(); + $split = explode('\\', $namespace); + + while($split) { + $ns = $this->namespace . "\\" . implode('\\', $split); + + if (! str_ends_with($ns, '\\')) { + $ns .= "\\"; + } + + if ( isset($source[$ns]) ) { + return $source[$ns][0]; + } + + array_pop($split); + } + + return $source[$this->namespace . "\\"][0] ?? false; + } + + public function outputResults() : void + { + + } + + public function readline(? string $prompt, mixed $default = null): string|false + { + $result = Terminal::readline($prompt); + + if ($this->default && $default !== null) { + return $default; + } + + return $result ?: $default; + } + + public function readNamespace() : void + { + $namespace = $this->readComposerNamespace(); + $this->namespace = $this->readline(sprintf('Namespace (default: %s) : ', $namespace), $namespace); + } + + public function readApi() : void + { + $this->apiNamespace = $this->readline('API namespace (default: Api\\) : ', 'Api'); + $this->apiBase = $this->readline('API URL base (default: /api) : ', '/api'); + } + + public function readWeb() : void + { + $this->webNamespace = $this->readline('Controller namespace (default: Controller\\) : ', 'Controller'); + $this->webBase = $this->readline('Controller URL base (default: /) : ', ""); + } + + public function readEntity() : void + { + $this->entityNamespace = $this->readline('Entity namespace (default: Entity\\) : ', 'Entity'); + } + + public function readFactory() : void + { + $this->factoryNamespace ??= $this->readline('Factory namespace (default: Factory\\) : ', 'Factory'); + $this->factoryFile = $this->readline('Form factory interface file (default: FormFactory.php) : ', 'FormFactory.php'); + $this->factoryInterfaceFile = $this->readline('Form factory interface file (default: FormFactoryInterface.php) : ', 'FormFactoryInterface.php'); + + $this->factoryAction = Terminal::proposeListChoice("Use new trait or append to file ?", [ 'trait' => 'Use trait', 'append' => 'Append to file', ]); + + if ($this->factoryAction === 'trait') { + $this->factoryTraitNamespace = implode('\\', [ $this->factoryNamespace, 'Factories' ]); + $this->factoryTraitNamespace = $this->readline(sprintf("Factory trait namespace (default: %s) : ", $this->factoryTraitNamespace), $this->factoryTraitNamespace); + } + } + + public function readSaveForm() : void + { + $this->formNamespace ??= $this->readline('Form namespace (default: Form\\) : ', 'Form'); + + $this->saveForm = ucfirst($this->name) . "Save"; + $this->saveForm = $this->readline(sprintf('Form "create/update" name (default: %s) : ', $this->saveForm), $this->saveForm); + + $this->saveContext = $this->saveForm . "Context"; + $this->saveContext = $this->readline(sprintf('Context "create/update" (default: %s) : ', $this->saveContext), $this->saveContext); + } + + public function readDeleteForm() : void + { + $this->formNamespace ??= $this->readline('Form namespace (default: Form\\) : ') ?: 'Form'; + + $this->deleteForm = ucfirst($this->name) . "Delete"; + $this->deleteForm = $this->readline(sprintf('Form "delete" name (default: %s) : ', $this->deleteForm), $this->deleteForm); + + $this->deleteContext = $this->deleteForm . "Context"; + $this->deleteContext = $this->readline(sprintf('Context "delete" (default: %s) : ', $this->deleteContext), $this->deleteContext); + } + + /*public function readApplicationPath(string $default) : void + { + $this->applicationPath = $this->readline(sprintf('Application path base (default: %s) : ', $default)) ?: $default; + }*/ + + public function generate() : void + { + $this->namespace = trim($this->namespace, '\\'); + $this->formNamespace = trim($this->formNamespace, '\\'); + $this->entityNamespace = trim($this->entityNamespace, '\\'); + + $this->compiledEntity = $this->defineEntityBoilerplate(); + + if (isset($this->saveForm)) { + $this->compiledSaveForm = $this->defineSaveFormBoilerplate(); + $this->compiledSaveFormContext = $this->defineSaveFormContextBoilerplate(); + } + + if (isset($this->deleteForm)) { + $this->compiledDeleteForm = $this->defineDeleteFormBoilerplate(); + $this->compiledDeleteFormContext = $this->defineDeleteFormContextBoilerplate(); + } + + if (isset($this->apiNamespace)) { + $this->compiledApi = $this->defineApiBoilerplate(); + + if ($this->includeDocs) { + $this->compiledDocs = $this->defineDocsBoilerplate(); + } + } + + if (isset($this->webNamespace)) { + $this->compiledWeb = $this->defineWebBoilerplate(); + } + + if (isset($this->factoryNamespace)) { + if (isset($this->factoryTraitNamespace)) { + $this->compiledFactoryTrait = $this->defineFactoryTraitBoilerplate(); + } + else { + $this->compiledFactory = $this->defineFactoryBoilerplate(); + } + + $this->compiledFactoryInterface = $this->defineFactoryInterfaceBoilerplate(); + } + } + + protected function defineEntityBoilerplate() : string + { + return $this->replaceBoilerplateCode($this->getBoilerplateFile('Entity')); + } + + protected function defineSaveFormBoilerplate() : string + { + return $this->replaceBoilerplateCode($this->getBoilerplateFile('SaveForm')); + } + + protected function defineSaveFormContextBoilerplate() : string + { + return $this->replaceBoilerplateCode($this->getBoilerplateFile('SaveFormContext')); + } + + protected function defineDeleteFormBoilerplate() : string + { + return $this->replaceBoilerplateCode($this->getBoilerplateFile('DeleteForm')); + } + + protected function defineDeleteFormContextBoilerplate() : string + { + return $this->replaceBoilerplateCode($this->getBoilerplateFile('DeleteFormContext')); + } + + protected function defineFactoryInterfaceBoilerplate() : string + { + return $this->replaceBoilerplateCode($this->getBoilerplateFile('FactoryInterfaceAppend')); + } + + protected function defineFactoryTraitBoilerplate() : string + { + return $this->replaceBoilerplateCode($this->getBoilerplateFile('FactoryTrait')); + } + + protected function defineFactoryBoilerplate() : string + { + return $this->replaceBoilerplateCode($this->getBoilerplateFile('FactoryAppend')); + } + + protected function defineApiBoilerplate() : string + { + return $this->replaceBoilerplateCode($this->getBoilerplateFile('Api')); + } + + protected function defineDocsBoilerplate() : string + { + return $this->replaceBoilerplateCode($this->getBoilerplateFile('api-docs', '.md')); + } + + protected function defineWebBoilerplate() : string + { + return $this->replaceBoilerplateCode($this->getBoilerplateFile('Controller')); + } + + protected function getBoilerplateFile(string $fileName, string $extension = ".php") : string + { + $path = implode(DIRECTORY_SEPARATOR, [ LEAN_API_PROJECT_PATH , 'skeleton', 'EntityGenerate', "{$fileName}{$extension}" ]); + $bp = file_get_contents($path); + + if (! $bp ) { + throw new \InvalidArgumentException("Boilerplate file not found in path " . $path); + } + + return $bp; + } + + protected function getEntityBoilerplate() : string + { + $path = implode(DIRECTORY_SEPARATOR, [ LEAN_API_PROJECT_PATH , 'skeleton', 'EntityGenerate', 'Entity.php' ]); + $bp = file_get_contents($path); + + if (! $bp ) { + throw new \InvalidArgumentException("Entity boilerplate file not found in path " . $path); + } + + return $bp; + } + + protected function replaceBoilerplateCode(string $boilerplate) : string + { + # to be reworked with the |> operator in 8.5+ + $boilerplate = $this->replaceNamespace($boilerplate); + $boilerplate = $this->replaceClassname($boilerplate); + $boilerplate = $this->replaceEntityNamespace($boilerplate); + $boilerplate = $this->replaceFormNamespace($boilerplate); + $boilerplate = $this->replaceSaveFormClassname($boilerplate); + $boilerplate = $this->replaceSaveFormContextClassname($boilerplate); + $boilerplate = $this->replaceDeleteFormClassname($boilerplate); + $boilerplate = $this->replaceDeleteFormContextClassname($boilerplate); + $boilerplate = $this->replaceFactoryNamespace($boilerplate); + $boilerplate = $this->replaceFactoryTraitNamespace($boilerplate); + $boilerplate = $this->replaceApiNamespace($boilerplate); + $boilerplate = $this->replaceApiBase($boilerplate); + $boilerplate = $this->replaceWebNamespace($boilerplate); + $boilerplate = $this->replaceWebBase($boilerplate); + + return $boilerplate; + } + + protected function replaceNamespace(string $boilerplate) : string + { + return str_replace('%NAMESPACE%', $this->namespace, $boilerplate); + } + + protected function replaceEntityNamespace(string $boilerplate) : string + { + return match($this->entityNamespace ?? false) { + false => $boilerplate, + default => str_replace( + [ '%USE_ENTITY_NS%', '%ENTITY_NS%' ], + [ strstr($this->entityNamespace, '\\', true) ?: $this->entityNamespace, $this->entityNamespace ], + $boilerplate + ) + }; + } + + protected function replaceSaveFormClassname(string $boilerplate) : string + { + return match($this->saveForm ?? false) { + false => $boilerplate, + default => str_replace( + [ '%SAVE_FORM_CLASSNAME%', '%SAVE_FORM_CLASSNAME_LC%' ], + [ $this->saveForm, lcfirst($this->saveForm) ], + $boilerplate + ) + }; + } + + protected function replaceSaveFormContextClassname(string $boilerplate) : string + { + return match($this->saveContext ?? false) { + false => $boilerplate, + default => str_replace( + [ '%SAVE_FORM_CONTEXT_CLASSNAME%', '%SAVE_FORM_CONTEXT_CLASSNAME_LC%' ], + [ $this->saveContext, lcfirst($this->saveContext) ], + $boilerplate + ) + }; + } + + protected function replaceDeleteFormClassname(string $boilerplate) : string + { + return match($this->deleteForm ?? false) { + false => $boilerplate, + default => str_replace( + [ '%DELETE_FORM_CLASSNAME%', '%DELETE_FORM_CLASSNAME_LC%' ], + [ $this->deleteForm, lcfirst($this->deleteForm) ], + $boilerplate + ) + }; + } + + protected function replaceDeleteFormContextClassname(string $boilerplate) : string + { + return match($this->deleteContext ?? false) { + false => $boilerplate, + default => str_replace( + [ '%DELETE_FORM_CONTEXT_CLASSNAME%', '%DELETE_FORM_CONTEXT_CLASSNAME_LC%' ], + [ $this->deleteContext, lcfirst($this->deleteContext) ], + $boilerplate + ) + }; + } + + protected function replaceFormNamespace(string $boilerplate) : string + { + return match($this->formNamespace ?? false) { + false => $boilerplate, + default => str_replace( + [ '%USE_FORM_NS%', '%FORM_NS%', ], + [ strstr($this->formNamespace, '\\', true) ?: $this->formNamespace, $this->formNamespace ], + $boilerplate + ) + }; + } + + protected function replaceApiNamespace(string $boilerplate) : string + { + return match($this->apiNamespace ?? false) { + false => $boilerplate, + default => str_replace( + [ '%API_NS%', ], + [ rtrim($this->apiNamespace, '\\') ], + $boilerplate + ) + }; + } + + protected function replaceApiBase(string $boilerplate) : string + { + return match($this->apiBase ?? false) { + false => $boilerplate, + default => str_replace( + [ '%API_ROUTE_BASE%', ], + [ $this->apiBase ], + $boilerplate + ) + }; + } + + protected function replaceWebNamespace(string $boilerplate) : string + { + return match($this->webNamespace ?? false) { + false => $boilerplate, + default => str_replace( + [ '%WEB_NS%', ], + [ rtrim($this->webNamespace, '\\') ], + $boilerplate + ) + }; + } + + protected function replaceWebBase(string $boilerplate) : string + { + return match($this->webBase ?? false) { + false => $boilerplate, + default => str_replace( + [ '%WEB_ROUTE_BASE%', ], + [ $this->webBase ], + $boilerplate + ) + }; + } + + protected function replaceFactoryNamespace(string $boilerplate) : string + { + return match($this->factoryNamespace ?? false) { + false => $boilerplate, + default => str_replace( + [ '%FACTORY_NS%', ], + [ rtrim($this->factoryNamespace, '\\') ], + $boilerplate + ) + }; + } + + protected function replaceFactoryTraitNamespace(string $boilerplate) : string + { + return match($this->factoryTraitNamespace ?? false) { + false => $boilerplate, + default => str_replace( + [ '%FACTORY_TRAIT_NS%', ], + [ rtrim($this->factoryTraitNamespace, '\\') ], + $boilerplate + ) + }; + } + + 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 + ) + }; + } + + protected function readComposerNamespace() : ? string + { + if ( false !== $composerJson = Composer::readComposerJson() ) { + if ($psr4 = $composerJson['autoload']['psr-4'] ?? false) { + foreach ($psr4 as $ns => $directory) { + if ($directory === 'src/') { + return $ns; + } + } + } + } + + return false; + } + + protected function readComposerPsr4() : array + { + return include(implode(DIRECTORY_SEPARATOR, [ getenv('PROJECT_PATH'), 'vendor' , 'composer', 'autoload_psr4.php' ])); + } +} \ No newline at end of file diff --git a/src/Cli/CodeGeneration.php b/src/Cli/CodeGeneration.php new file mode 100644 index 0000000..997b836 --- /dev/null +++ b/src/Cli/CodeGeneration.php @@ -0,0 +1,120 @@ +get('name')->value() ?: readline('Entity name : '); + + if ( empty($name)) { + throw new \InvalidArgumentException("An entity name must be provided"); + } + + $applicationList = array_reverse(array_map(fn($e) => $e->name, $this->lean->applications)); + $app = $options->get('application')->value() ?? Terminal::proposeListChoice("Select an application : ", $applicationList); + + $entityGenerate = new Action\Console\EntityGenerate( + application: $this->lean->getApplication($applicationList[$app]), + name: $name, + overwrite: $options->get('overwrite')->value() ?? false, + default: $options->get('default-answers')->value() ?? false, + ); + + if ("" === ( $entityGenerate->namespace = $options->get('namespace')->value() ?: "" )) { + $entityGenerate->readNamespace(); + } + + $entityGenerate->readEntity(); + + if ($options->get('saveForm')->value() ?? Terminal::proposeListChoice("Add a save form ?", [ true => 'Yes', false => 'No', ])) { + $entityGenerate->readSaveForm(); + $hasFactory = true; + } + + if ($options->get('deleteForm')->value() ?? Terminal::proposeListChoice("Add a delete form ?", [ true => 'Yes', false => 'No', ])) { + $entityGenerate->readDeleteForm(); + $hasFactory = true; + } + + if (isset($hasFactory) && ( + $options->get('factory')->value() ?? Terminal::proposeListChoice("Add to form factory ?", [ true => 'Yes', false => 'No', ])) + ) { + $entityGenerate->readFactory(); + } + + if ($options->get('web')->value() ?? Terminal::proposeListChoice("Add a Web controller ?", [ true => 'Yes', false => 'No', ])) { + $entityGenerate->readWeb(); + } + + if ($options->get('api')->value() ?? Terminal::proposeListChoice("Add an API controller ?", [ true => 'Yes', false => 'No', ])) { + $entityGenerate->readApi(); + + if ($options->get('api-docs')->value() ?? Terminal::proposeListChoice("Provides documentation for this route ?", [ true => 'Yes', false => 'No', ])) { + $entityGenerate->includeDocs = true; + } + } + + $entityGenerate->generate(); + + if ($options->get('write')->value() ?? Terminal::proposeListChoice("Write to disc or output results only ?", [ true => 'Write to disc', false => 'Output results only', ])) { + $entityGenerate->writeToDisc(); + } + else { + $entityGenerate->outputResults(); + } + + return $this->renderCLI($request, "done"); + } +} \ No newline at end of file diff --git a/src/Controller/Debug.php b/src/Controller/Debug.php index 8b2c538..5c990eb 100644 --- a/src/Controller/Debug.php +++ b/src/Controller/Debug.php @@ -18,6 +18,6 @@ class Debug { #[Route("/", name: "lean.api:debug-doc", method: "GET", description: "Documentation section")] public function documentation(ServerRequestInterface $request, array $arguments) : ResponseInterface { - return $this->renderMarkdown(getenv("LEAN_API_PROJECT_PATH") . "/meta/docs/lean-api.md", [ Entity\Error::class, ], [ Form\Debug\ErrorContext::class ]); + return $this->renderMarkdown(LEAN_API_PROJECT_PATH . "/meta/docs/lean-api.md", [ Entity\Error::class, ], [ Form\Debug\ErrorContext::class ]); } } diff --git a/src/Controller/Debug/Errors.php b/src/Controller/Debug/Errors.php index 75e6a7c..f05b1d5 100644 --- a/src/Controller/Debug/Errors.php +++ b/src/Controller/Debug/Errors.php @@ -19,7 +19,7 @@ class Errors { #[Route("/documentation", name: "lean.api:errors-doc", method: "GET", description: "Documentation section")] public function documentation(ServerRequestInterface $request, array $arguments) : ResponseInterface { - return $this->renderMarkdown(getenv("LEAN_API_PROJECT_PATH") . "/meta/docs/debug/errors.md", [ Entity\Error::class, ], [ Form\Debug\ErrorContext::class ]); + return $this->renderMarkdown(LEAN_API_PROJECT_PATH . "/meta/docs/debug/errors.md", [ Entity\Error::class, ], [ Form\Debug\ErrorContext::class ]); } #[Route("/", name: "lean.api:errors-list", method: [ "GET" ], description: "List every errors received")] diff --git a/src/Form/Delete.php b/src/Form/Delete.php index 3b55ad0..39af55c 100644 --- a/src/Form/Delete.php +++ b/src/Form/Delete.php @@ -8,17 +8,6 @@ use Ulmus\Entity\EntityInterface; abstract class Delete extends Form implements \Picea\Ui\Method\FormInterface { - public function validate(FormContextInterface $context) : bool - { - if ( ! $this->getEntity()->isLoaded() ) { - $context->pushMessage($this->message::generateError( - $this->lang('lean.api.form.delete.error.entity') - )); - } - - return $context->valid($this->getEntity()->isLoaded() ? $this->getEntity() : null); - } - public function execute(FormContextInterface $context) : void { try { diff --git a/src/Form/Form.php b/src/Form/Form.php index aa60f4e..64564f6 100644 --- a/src/Form/Form.php +++ b/src/Form/Form.php @@ -10,12 +10,19 @@ use Ulmus\EntityCollection; abstract class Form { + protected string $contextClass; + public function __construct( protected LanguageHandler $languageHandler, protected MessageFactoryInterface $message, # public EntityInterface $entity, ) {} + public function validate(FormContextInterface $context) : bool + { + return $context->valid($this->getEntity()->isLoaded() ? $this->getEntity() : null); + } + public function initialize(FormContextInterface $context) : void { if(isset($this->contextClass) && ! $context instanceof $this->contextClass ) { diff --git a/src/Form/Save.php b/src/Form/Save.php index cee94c3..dff63ba 100644 --- a/src/Form/Save.php +++ b/src/Form/Save.php @@ -19,8 +19,6 @@ abstract class Save extends Form implements \Picea\Ui\Method\FormInterface { protected array $reflectedProperties; - protected string $contextClass; - public function initialize(FormContextInterface $context) : void { if ( ! $this->getEntity()->isLoaded() ) { @@ -74,7 +72,7 @@ abstract class Save extends Form implements \Picea\Ui\Method\FormInterface { $context->pushErrorMessage($ex->getMessage()); } - return $saved; + return $saved ?? false; } protected function validateContextFields(FormContextInterface $context) : void diff --git a/view/lean-api/entity-generate/create.phtml b/view/lean-api/entity-generate/create.phtml new file mode 100644 index 0000000..a948c99 --- /dev/null +++ b/view/lean-api/entity-generate/create.phtml @@ -0,0 +1 @@ +@todo! \ No newline at end of file diff --git a/view/lean-api/entity-generate/edit.phtml b/view/lean-api/entity-generate/edit.phtml new file mode 100644 index 0000000..a948c99 --- /dev/null +++ b/view/lean-api/entity-generate/edit.phtml @@ -0,0 +1 @@ +@todo! \ No newline at end of file diff --git a/view/lean-api/entity-generate/index.phtml b/view/lean-api/entity-generate/index.phtml new file mode 100644 index 0000000..a948c99 --- /dev/null +++ b/view/lean-api/entity-generate/index.phtml @@ -0,0 +1 @@ +@todo! \ No newline at end of file