diff --git a/composer.json b/composer.json index ccfa04f..cb12d74 100644 --- a/composer.json +++ b/composer.json @@ -37,7 +37,7 @@ "symfony/validator": "^6.2 || ^7.0", "nette/finder": "^3.0", "opis/json-schema": "^2.3", - "matronator/parsem": "^3.0", + "matronator/parsem": "^3.1", "guzzlehttp/guzzle": "^7.5" }, "minimum-stability": "dev", diff --git a/src/Mtrgen/Registry/Connection.php b/src/Mtrgen/Registry/Connection.php index 2f1bfec..349c36b 100644 --- a/src/Mtrgen/Registry/Connection.php +++ b/src/Mtrgen/Registry/Connection.php @@ -8,6 +8,9 @@ use GuzzleHttp\Exception\RequestException; use Matronator\Mtrgen\Store\Path; use Matronator\Mtrgen\Store\Storage; +use Matronator\Mtrgen\Template; +use Matronator\Mtrgen\Template\ClassicGenerator; +use Matronator\Mtrgen\Template\Generator; use Matronator\Parsem\Parser; use Symfony\Component\Console\Output\OutputInterface; @@ -59,34 +62,12 @@ public function login(string $username, string $password, int $duration = 24): ? public function getTemplate(string $identifier): object { [$vendor, $name] = explode('/', $identifier); - $url = $this->apiUrl . "/templates/$vendor/$name/get"; - $client = new Client(); - $response = $client->get($url, [ - 'headers' => [ - 'X-Requested-By' => 'cli', - ], - ]); - $contentType = $response->getHeaderLine('Content-Type'); - - switch ($contentType) { - case 'application/json': - case 'text/json': - $extension = 'json'; - case 'text/x-yaml': - case 'application/x-yaml': - case 'text/yaml': - $extension = 'yaml'; - case 'application/x-neon': - case 'text/x-neon': - case 'text/neon': - $extension = 'neon'; - default: - $filename = $response->getHeaderLine('X-Template-Filename'); - $parts = explode('.', $filename); - $extension = end($parts); - } + ['client' => $client, + 'extension' => $extension, + 'contentType' => $contentType, + 'response' => $response ] = $this->getTemplateDetails($url); $typeUrl = $this->apiUrl . "/templates/$vendor/$name/type"; $typeResponse = $client->get($typeUrl); @@ -103,34 +84,11 @@ public function getTemplate(string $identifier): object public function getTemplateFromBundle(string $identifier, string $templateName): object { [$vendor, $name] = explode('/', $identifier); - $url = $this->apiUrl . "/bundles/$vendor/$name/$templateName/get"; - $client = new Client(); - $response = $client->get($url, [ - 'headers' => [ - 'X-Requested-By' => 'cli', - ], - ]); - $contentType = $response->getHeaderLine('Content-Type'); - - switch ($contentType) { - case 'application/json': - case 'text/json': - $extension = 'json'; - case 'text/x-yaml': - case 'application/x-yaml': - case 'text/yaml': - $extension = 'yaml'; - case 'application/x-neon': - case 'text/x-neon': - case 'text/neon': - $extension = 'neon'; - default: - $filename = $response->getHeaderLine('X-Template-Filename'); - $parts = explode('.', $filename); - $extension = end($parts); - } + ['extension' => $extension, + 'contentType' => $contentType, + 'response' => $response ] = $this->getTemplateDetails($url); return (object) [ 'filename' => "$name.template.$extension", @@ -145,12 +103,8 @@ public function postTemplate(string $path, ?OutputInterface $io = null): mixed if (!$profile->authenticate()) return 'You must login first.'; - $matched = preg_match('/^(.+\/)?(.+?\.(json|yml|yaml|neon))$/', $path, $matches); - if (!$matched) - return "Couldn't get filename from path '$path'."; - - $filename = $matches[2]; - $template = Parser::decodeByExtension($path, file_get_contents($path)); + $matches = explode(DIRECTORY_SEPARATOR, $path); + $filename = end($matches); $profileObject = $profile->loadProfile(); @@ -161,6 +115,7 @@ public function postTemplate(string $path, ?OutputInterface $io = null): mixed return 'Invalid bundle.'; $templates = []; + $template = Parser::decodeByExtension($path, file_get_contents($path)); foreach ($template->templates as $item) { $templates[] = (object) [ 'name' => $item->name, @@ -177,14 +132,17 @@ public function postTemplate(string $path, ?OutputInterface $io = null): mixed ]; $url = $this->apiUrl . '/bundles'; } else { - if (!Parser::isValid(Path::makeAbsolute($path), file_get_contents(Path::makeAbsolute($path)))) + $contents = file_get_contents(Path::makeAbsolute($path)); + $isLegacy = Template::isLegacy($path); + $name = $isLegacy ? ClassicGenerator::getName($path, $contents) : Generator::getName($contents); + if ($isLegacy && !Parser::isValid(Path::makeAbsolute($path), $contents)) return 'Invalid template.'; $body = [ 'username' => $profileObject->username, 'filename' => $filename, - 'name' => strtolower($template->name), - 'contents' => file_get_contents(Path::makeAbsolute($path)), + 'name' => strtolower($name), + 'contents' => $contents, ]; $url = $this->apiUrl . '/templates'; } @@ -213,4 +171,41 @@ public static function isDebug() return $config->debug ?? false; } + + private function getTemplateDetails(string $url): array + { + $client = new Client(); + $response = $client->get($url, [ + 'headers' => [ + 'X-Requested-By' => 'cli', + ], + ]); + $contentType = $response->getHeaderLine('Content-Type'); + + switch ($contentType) { + case 'application/json': + case 'text/json': + $extension = 'json'; + case 'text/x-yaml': + case 'text/yaml': + case 'application/x-yaml': + case 'application/yaml': + $extension = 'yaml'; + case 'application/x-neon': + case 'text/x-neon': + case 'text/neon': + $extension = 'neon'; + default: + $filename = $response->getHeaderLine('X-Template-Filename'); + $parts = explode('.', $filename); + $extension = end($parts); + } + + return [ + 'client' => $client, + 'extension' => $extension, + 'contentType' => $contentType, + 'response' => $response, + ]; + } } diff --git a/src/Mtrgen/Store/Storage.php b/src/Mtrgen/Store/Storage.php index 75b4936..46f055c 100644 --- a/src/Mtrgen/Store/Storage.php +++ b/src/Mtrgen/Store/Storage.php @@ -47,9 +47,9 @@ public function __construct() * @return boolean True if save is successful, false otherwise * @param string $filename * @param string|null $alias Alias to save the template under instead of the name defined inside the template - * @param string|null $bundle Name of the bundle or null if not a bundle + * @param string|null $parentBundle Name of the bundle if the template belongs to a bundle or null if not a bundle */ - public function save(string $filename, ?string $alias = null, ?string $bundle = null): bool + public function save(string $filename, ?string $alias = null, ?string $parentBundle = null): bool { $file = Path::canonicalize($filename); @@ -62,12 +62,12 @@ public function save(string $filename, ?string $alias = null, ?string $bundle = $name = Generator::getName(path: $file); } - $basename = $bundle ? $bundle . DIRECTORY_SEPARATOR . basename($file) : basename($file); - if ($bundle && !ClassicFileGenerator::folderExist($this->templateDir . DIRECTORY_SEPARATOR . $bundle) && !mkdir($concurrentDirectory = $this->templateDir . DIRECTORY_SEPARATOR . $bundle, 0777, true) && !is_dir($concurrentDirectory)) { + $basename = $parentBundle ? $parentBundle . DIRECTORY_SEPARATOR . basename($file) : basename($file); + if ($parentBundle && !ClassicFileGenerator::folderExist($this->templateDir . DIRECTORY_SEPARATOR . $parentBundle) && !mkdir($concurrentDirectory = $this->templateDir . DIRECTORY_SEPARATOR . $parentBundle, 0777, true) && !is_dir($concurrentDirectory)) { throw new \RuntimeException(sprintf('Directory "%s" was not created', $concurrentDirectory)); } - $this->saveEntry($alias ?? ($bundle ? "$bundle:" . $name : $name), $basename); + $this->saveEntry($alias ?? ($parentBundle ? $parentBundle . ':' . $name : $name), $basename); copy($file, Path::canonicalize($this->templateDir . DIRECTORY_SEPARATOR . $basename)); return true; @@ -90,7 +90,7 @@ public function saveBundle(object $bundleObject, string $format = 'json'): bool $contents = Neon::encode($bundleObject, true); break; default: - throw new InvalidArgumentException('Unsupported format.'); + throw new InvalidArgumentException('Unsupported bundle format.'); } $filename = "$name.bundle.$format"; @@ -164,10 +164,10 @@ public function saveFolder(string $path): ?int $store = $this->loadStore(); - $files = Finder::findFiles('*.template.yaml', '*.template.json', '*.template.neon')->in($path); + $files = Finder::findFiles('*.template.*', '*.mtr.*', '*.mtr')->in($path); $added = 0; foreach ($files as $key => $file) { - if (!Parser::isValid($key)) continue; + if (!Parser::isValid($key, $file->read())) continue; $store = $this->entry($store, ClassicGenerator::getName($key), $key); $added++; diff --git a/src/Mtrgen/Template.php b/src/Mtrgen/Template.php index 9b759e5..3ddd40b 100644 --- a/src/Mtrgen/Template.php +++ b/src/Mtrgen/Template.php @@ -20,9 +20,7 @@ public static function isLegacy(string $path): bool if (!$contents) { throw new TemplateNotFoundException($path); } - if (Parser::isValid($path, $contents)) { - return true; - } else if (Parser::isValidBundle($path, $contents)) { + if (Parser::isValid($path, $contents) || Parser::isValidBundle($path, $contents)) { return true; } } diff --git a/src/Mtrgen/Template/Generator.php b/src/Mtrgen/Template/Generator.php index d155cc4..25e64e9 100644 --- a/src/Mtrgen/Template/Generator.php +++ b/src/Mtrgen/Template/Generator.php @@ -10,16 +10,13 @@ class Generator { - public const RESERVED_KEYWORDS = ClassicGenerator::RESERVED_KEYWORDS; - public const RESERVED_CONSTANTS = ClassicGenerator::RESERVED_CONSTANTS; - public const HEADER_PATTERN = '/^\S+ --- MTRGEN ---.(.+)\s\S+ --- MTRGEN ---/ms'; public const COMMENT_PATTERN = '/\/\*\s?([a-zA-Z0-9_]+)\|?(([a-zA-Z0-9_]+?)(?:\:(?:(?:\'|")?\w(?:\'|")?,?)+?)*?)?\s?\*\//m'; public static function parseAnyFile(string $path, array $arguments = [], bool $useCommentSyntax = false): GenericFileObject { - $file = file_get_contents($path); + $file = ltrim(file_get_contents($path)); if (!$file) { throw new \RuntimeException(sprintf('File "%s" was not found', $path)); @@ -57,7 +54,8 @@ private static function write(GenericFileObject $file): void } file_put_contents(Path::safe(str_ends_with($file->directory, DIRECTORY_SEPARATOR) - ? $file->directory . $file->filename : $file->directory . DIRECTORY_SEPARATOR . $file->filename), $file->contents); + ? $file->directory . $file->filename + : $file->directory . DIRECTORY_SEPARATOR . $file->filename), $file->contents); } /** @@ -73,7 +71,7 @@ public static function getName(?string $content = null, ?string $path = null): s throw new \RuntimeException('Either content or path must be provided.'); } if (!$content && $path) { - $content = file_get_contents($path); + $content = file_get_contents(Path::canonicalize($path)); } return static::getTemplateHeader($content)->name; } diff --git a/tests/templates/Controller.mtr.php b/tests/templates/Controller.mtr.php new file mode 100644 index 0000000..d6751b0 --- /dev/null +++ b/tests/templates/Controller.mtr.php @@ -0,0 +1,137 @@ +// --- MTRGEN --- +// name: e3-controller +// filename: <% entity|upperFirst %>Controller.php +// path: /Users/matronator/Documents/Work/blueghost.nosync/eshop-3/back/src/Controller/Admin +// --- MTRGEN --- +; +use App\Exception\Admin\AdminErrorException; +use App\Service\<% parent|upperFirst %>\<% entity %>ListPageBuilder; +use App\Service\<% parent|upperFirst %>\<% entity %>Manager; +use App\Service\<% parent|upperFirst %>\<% entity %>Validator; +use App\Utils\Breadcrumb; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Routing\Annotation\Route; + +/** + * @Route("/<% parent|lower %>", name="<% parent|lower %>_") + */ +class <% entity %>Controller extends BaseEntityController +{ + public function __construct(<% entity %>Manager $manager, <% entity %>Validator $validator, <% entity %>ListPageBuilder $listPageBuilder) + { + $this->editTemplate = 'admin/<% templateDir %>/detail.html.twig'; + $this->listTemplate = 'admin/<% templateDir %>/list_page.html.twig'; + $this->entityName = '<% entity|snakeCase %>'; + $this->manager = $manager; + $this->validator = $validator; + $this->listPageBuilder = $listPageBuilder; + } + + /** + * @Route("/<% listRoute %>", name="<% shortName|lower %>_list", methods={"GET"}) + */ + public function listPage(Request $request): Response + { + return parent::listPage($request); + } + + /** + * @Route("/<% editRoute %>", name="<% shortName|lower %>_form", methods={"GET", "POST"}) + */ + public function edit(Request $request): Response + { + return parent::edit($request); + } + + /** + * Vytvori novy zaznam entity. Vrati ID entity pri uspechu a false pri selhani. + * + * @return int|bool + */ + protected function handleInsert(Request $request) + { + try { + $data = $request->request->all('admin'); + $id = $this->manager->create($data); + $this->addFlash(self::STATUS_SUCCESS, '<% czech|upperFirst %> byl úspěšně vytvořen.'); + + return $id; + } catch (AdminErrorException $ex) { + $this->addFlash(self::STATUS_ERROR, $ex->getMessage()); + + return false; + } + } + + /** + * Edituje existujici zaznam entity. + */ + protected function handleEdit(Request $request): void + { + try { + $data = $request->request->all('admin'); + $this->manager->update($data); + $this->addFlash(self::STATUS_SUCCESS, '<% czech|upperFirst %> byl úspěšně upraven.'); + } catch (AdminErrorException $ex) { + $this->addFlash(self::STATUS_ERROR, $ex->getMessage()); + } + } + + /** + * Odstrani zaznam entity. + */ + protected function handleDelete(Request $request): void + { + try { + $this->manager->delete($request->query->get('id')); + $this->addFlash(self::STATUS_SUCCESS, '<% czech|upperFirst %> byl úspěšně vymazán.'); + } catch (AdminErrorException $ex) { + $this->addFlash(self::STATUS_ERROR, $ex->getMessage()); + } + } + + /** + * Vrati instanci modelu konkretni entity dle predanych dat. + * + * @param int|array|null $data + */ + protected function getEntity($data = null): BaseIdModel + { + if (!isset($data)) { + $entity = new <% entity %>(); + } elseif (!is_array($data)) { + $entity = $this->manager->get($data); + } else { + $entity = new <% entity %>(); + $entity->deserialize($this->em, $data); + + $this->manager->fillRelationsFromPostData($entity, $data); + } + + return $entity; + } + + protected function getListPageBreadcrumbs(): array + { + $breadcrumbs = []; + $breadcrumbs[] = new Breadcrumb('Správa událostí', $this->generateUrl('<% parent|lower %>_menu')); + $breadcrumbs[] = new Breadcrumb('Typy událostí', $this->generateUrl('<% entity|snakeCase %>_list')); + + return $breadcrumbs; + } + + protected function getEditBreadcrumbs(): array + { + $breadcrumbs = $this->getListPageBreadcrumbs(); + + $breadcrumbs[] = new Breadcrumb('Správa typu'); + + return $breadcrumbs; + } +} diff --git a/tests/templates/ListPageBuilder.mtr.php b/tests/templates/ListPageBuilder.mtr.php new file mode 100644 index 0000000..41f5a06 --- /dev/null +++ b/tests/templates/ListPageBuilder.mtr.php @@ -0,0 +1,38 @@ +// --- MTRGEN --- +// name: list-page-builder +// filename: <% entity|upperFirst %>ListPageBuilder.php +// path: /Users/matronator/Documents/Work/blueghost.nosync/eshop-3/back/src/Service/Event +// --- MTRGEN --- +; +use App\Service\PaginationListPageBuilder; +use Doctrine\ORM\EntityManagerInterface; + +class <% entity %>ListPageBuilder extends PaginationListPageBuilder +{ + public function __construct(EntityManagerInterface $em) + { + $this->repository = $em->getRepository(<% entity %>::class); + $this->editRoute = '<% entity|snakeCase %>_form'; + } + + /** + * @param <% entity %> $entity + */ + public function getBaseListItem(BaseModel $entity): array + { + return [ + 'name0' => $entity->getName0(), + 'name1' => $entity->getName1(), + 'name2' => $entity->getName2(), + 'name3' => $entity->getName3(), + 'name4' => $entity->getName4(), + 'id' => $entity->getId(), + 'entity' => $entity, + ]; + } +} diff --git a/tests/templates/Manager.mtr.php b/tests/templates/Manager.mtr.php new file mode 100644 index 0000000..3c34c50 --- /dev/null +++ b/tests/templates/Manager.mtr.php @@ -0,0 +1,53 @@ +// --- MTRGEN --- +// name: e3-manager +// filename: <% entity|upperFirst %>Manager.php +// path: /Users/matronator/Documents/Work/blueghost.nosync/eshop-3/back/src/Service/Event +// --- MTRGEN --- +; +use App\Service\AbstractEntityManager; +use App\Utils\ModuleStatus; +use Doctrine\ORM\EntityManagerInterface; + +/** + * @method <% entity %> get($data) + */ +class <% entity %>Manager extends AbstractEntityManager +{ + public function __construct(EntityManagerInterface $em) + { + $this->subject = '<% czech|lower %>'; + $this->modelClass = <% entity %>::class; + $this->searchResultRoute = '<% entity|snakeCase %>_form'; + + parent::__construct($em); + } + + /** + * @param <% entity %> $entity + */ + protected function getSearchIndexSearchString(BaseIdModel $entity): string + { + $data = [ + $entity->getName0(), + $entity->getName1(), + $entity->getName2(), + $entity->getName3(), + $entity->getName4(), + ]; + + return join(' ', $data); + } + + /** + * @param <% entity %> $entity + */ + protected function getSearchIndexName(BaseIdModel $entity): string + { + return '<% czech|upperFirst %> události '.$entity->getName0(); + } +}