├── .github
└── workflows
│ ├── coding-standard.yml
│ └── mess-detector.yml
├── Api
└── MakerInterface.php
├── Console
└── Command
│ ├── Crud.php
│ ├── Database
│ ├── Constraint.php
│ ├── Entity.php
│ └── Field.php
│ ├── Model.php
│ └── Module.php
├── Exception
├── CommandIoNotInitializedException.php
├── ConstraintDefinitionException.php
├── ExistingClassException.php
├── ExistingFieldException.php
├── InvalidArrayException.php
└── TableDefinitionException.php
├── Generator
├── Generator.php
├── GeneratorController.php
├── GeneratorCrud.php
├── GeneratorModel.php
├── GeneratorModule.php
├── GeneratorRepository.php
├── GeneratorUiComponent.php
└── templates
│ ├── Controller
│ └── controller.php.tpl
│ ├── Model
│ ├── ResourceModel
│ │ ├── Collection
│ │ │ └── grid.tpl.php
│ │ └── resourcemodel.tpl.php
│ └── model.tpl.php
│ ├── composer.json.tpl
│ ├── etc
│ ├── acl.xml.tpl
│ ├── adminhtml
│ │ ├── menu.xml.tpl
│ │ └── routes.xml.tpl
│ └── module.xml.tpl
│ ├── registration.php.tpl
│ └── view
│ └── adminhtml
│ ├── layout
│ └── listing.xml.tpl
│ └── ui_component
│ └── listing.xml.tpl
├── LICENSE
├── Maker
├── MakeConstraint.php
├── MakeCrud.php
├── MakeEntity.php
├── MakeField.php
└── MakeModel.php
├── README.md
├── Service
├── CommandIoProvider.php
├── CurrentModule.php
├── Database
│ ├── ConstraintDefinition.php
│ ├── DataTableAutoCompletion.php
│ ├── DbSchemaCreator.php
│ ├── DbSchemaParser.php
│ ├── DbSchemaPath.php
│ └── Field.php
└── Php
│ ├── ClassGenerator.php
│ ├── DefaultClassNameGetter.php
│ ├── InterfaceGenerator.php
│ └── NamespaceGetter.php
├── Test
└── Unit
│ └── Database
│ └── Service
│ ├── ConstraintDefinitionTest.php
│ ├── DataTableAutoCompletionTest.php
│ ├── DbSchemaCreatorTest.php
│ └── DbSchemaParserTest.php
├── Utils
├── ConsoleCrudEntitySelector.php
├── ConsoleModuleSelector.php
└── StringTransformationTools.php
├── composer.json
├── docs
├── images
│ └── make-entity-1.png
└── make-entity.md
├── etc
├── di.xml
└── module.xml
└── registration.php
/.github/workflows/coding-standard.yml:
--------------------------------------------------------------------------------
1 | name: ExtDN M2 Coding Standard
2 | on:
3 | push:
4 | branches:
5 | - master
6 | pull_request:
7 |
8 | jobs:
9 | static:
10 | name: M2 Coding Standard
11 | runs-on: ubuntu-latest
12 | steps:
13 | - uses: actions/checkout@v4
14 | - uses: extdn/github-actions-m2/magento-coding-standard/8.1@master
--------------------------------------------------------------------------------
/.github/workflows/mess-detector.yml:
--------------------------------------------------------------------------------
1 | name: ExtDN M2 Mess Detector
2 | on:
3 | push:
4 | branches:
5 | - master
6 | pull_request:
7 |
8 | jobs:
9 | phpmd:
10 | name: M2 Mess Detector
11 | runs-on: ubuntu-latest
12 | steps:
13 | - uses: actions/checkout@v4
14 | - uses: extdn/github-actions-m2/magento-mess-detector/@master
--------------------------------------------------------------------------------
/Api/MakerInterface.php:
--------------------------------------------------------------------------------
1 | setName('makegento:crud')
40 | ->setDescription('makegento let you do everything you want');
41 | parent::configure();
42 | }
43 |
44 | /**
45 | * CLI command description.
46 | *
47 | * @param InputInterface $input
48 | * @param OutputInterface $output
49 | *
50 | * @return int
51 | */
52 | protected function execute(InputInterface $input, OutputInterface $output): int
53 | {
54 | try {
55 | $this->appState->setAreaCode(Area::AREA_GLOBAL);
56 | } catch (LocalizedException $e) {
57 | // Do nothing area code is already set
58 | }
59 |
60 | $commandHelper = $this->getHelper('question');
61 |
62 | $this->commandIoProvider->init($input, $output, $commandHelper);
63 | $this->consoleModuleSelector->execute(true);
64 | $entityName = $this->consoleCrudEntitySelector->execute();
65 | try {
66 | $this->makeCrud->generate($entityName);
67 | } catch (LocalizedException $e) {
68 | $output->writeln("{$e->getMessage()}");
69 | return Command::FAILURE;
70 | }
71 |
72 | return Command::SUCCESS;
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/Console/Command/Database/Constraint.php:
--------------------------------------------------------------------------------
1 | setName('makegento:db-schema:constraint')
30 | ->setDescription('Create a new entity')
31 | ->setHelp('This command allows you to create a new foreign key.');
32 | parent::configure();
33 | }
34 |
35 | /**
36 | * @throws TableDefinitionException
37 | * @throws ConstraintDefinitionException
38 | */
39 | public function execute(InputInterface $input, OutputInterface $output): int
40 | {
41 | $questionHelper = $this->getHelper('question');
42 | $this->commandIoProvider->init($input, $output, $questionHelper);
43 |
44 | try {
45 | $this->moduleSelector->execute(true);
46 | } catch (\Exception $e) {
47 | $output->writeln("{$e->getMessage()}");
48 | return Command::FAILURE;
49 | }
50 |
51 | $this->makeConstraint->generate();
52 | return Command::SUCCESS;
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/Console/Command/Database/Entity.php:
--------------------------------------------------------------------------------
1 | setName('makegento:db-schema:entity')
33 | ->setDescription('Create a new entity')
34 | ->setHelp('This command allows you to create a new entity.');
35 | parent::configure();
36 | }
37 |
38 | /**
39 | */
40 | public function execute(InputInterface $input, OutputInterface $output)
41 | {
42 | $questionHelper = $this->getHelper('question');
43 | $this->commandIoProvider->init($input, $output, $questionHelper);
44 |
45 | try {
46 | $this->moduleSelector->execute(true);
47 | } catch (\Exception $e) {
48 | $output->writeln("{$e->getMessage()}");
49 | return Command::FAILURE;
50 | }
51 |
52 | $this->makeEntity->generate();
53 | return Command::SUCCESS;
54 | }
55 |
56 |
57 | }
58 |
--------------------------------------------------------------------------------
/Console/Command/Database/Field.php:
--------------------------------------------------------------------------------
1 | setName('makegento:db-schema:field')
29 | ->setDescription('Add a new field')
30 | ->setHelp('This command allows you to add a new field to db_schema.xml. It will propose then to add a new constraint.');
31 | parent::configure();
32 | }
33 |
34 | /**
35 | * @throws TableDefinitionException
36 | */
37 | public function execute(InputInterface $input, OutputInterface $output): int
38 | {
39 | $questionHelper = $this->getHelper('question');
40 | $this->commandIoProvider->init($input, $output, $questionHelper);
41 |
42 | try {
43 | $selectedModule = $this->moduleSelector->execute(true);
44 | } catch (\Exception $e) {
45 | $output->writeln("{$e->getMessage()}");
46 | return Command::FAILURE;
47 | }
48 |
49 | $this->makeField->generate($selectedModule);
50 | return Command::SUCCESS;
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/Console/Command/Model.php:
--------------------------------------------------------------------------------
1 | setName('makegento:model')
27 | ->setDescription('Makegento create a model from a db_schema.xml file');
28 | parent::configure();
29 | }
30 |
31 | /**
32 | * CLI command description.
33 | *
34 | * @param InputInterface $input
35 | * @param OutputInterface $output
36 | *
37 | * @return int
38 | */
39 | protected function execute(InputInterface $input, OutputInterface $output): int
40 | {
41 | $commandHelper = $this->getHelper('question');
42 |
43 | $this->commandIoProvider->init($input, $output, $commandHelper);
44 |
45 | $this->consoleModuleSelector->execute(true);
46 |
47 | try {
48 | $this->makeModel->generate();
49 | } catch (\Exception $e) {
50 | $output->writeln("{$e->getMessage()}");
51 | return Command::FAILURE;
52 | }
53 |
54 | return Command::SUCCESS;
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/Console/Command/Module.php:
--------------------------------------------------------------------------------
1 | rootDir = $this->filesystem->getDirectoryRead(DirectoryList::ROOT);
34 | }
35 |
36 | /**
37 | * Initialization of the command.
38 | */
39 | protected function configure()
40 | {
41 | $this->setName('makegento:module')
42 | ->setDescription('Create a new module');
43 | parent::configure();
44 | }
45 |
46 | /**
47 | * CLI command description.
48 | *
49 | * @param InputInterface $input
50 | * @param OutputInterface $output
51 | *
52 | * @return int
53 | */
54 | protected function execute(InputInterface $input, OutputInterface $output): int
55 | {
56 | $this->appState->setAreaCode(Area::AREA_GLOBAL);
57 |
58 | $commandHelper = $this->getHelper('question');
59 |
60 | $question = $this->questionFactory->create([
61 | 'question' => 'Enter Vendor Name' . PHP_EOL,
62 | ]);
63 |
64 | $vendorName = $commandHelper->ask($input, $output, $question);
65 |
66 | $question = $this->questionFactory->create([
67 | 'question' => 'Enter Module Name' . PHP_EOL,
68 | ]);
69 |
70 | $moduleName = $commandHelper->ask($input, $output, $question);
71 |
72 | $this->generatorModule->generateModule($vendorName, $moduleName);
73 |
74 | $output->writeln(PHP_EOL);
75 | $output->writeln("\t" . ' >');
76 | $output->writeln("\t" . ' >');
77 | $output->writeln("\t" . ' Module and files created with Success! >');
78 | $output->writeln("\t" . ' >');
79 | $output->writeln("\t" . ' >');
80 | $output->writeln(PHP_EOL);
81 | $output->writeln("\t" . 'Your new module\'s name is $newModuleName>>');
82 | $output->writeln(PHP_EOL);
83 | $output->writeln("\t" . 'Do not forget to run bin/magento setup:upgrade> to activate your new module!>');
84 | $output->writeln(PHP_EOL);
85 |
86 | $output->writeln("Long live to Opengento!");
87 |
88 | return Command::SUCCESS;
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/Exception/CommandIoNotInitializedException.php:
--------------------------------------------------------------------------------
1 | filePath;
22 | }
23 |
24 | /**
25 | * Get the value of className
26 | */
27 | public function getClassName(): string
28 | {
29 | return $this->className;
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/Exception/ExistingFieldException.php:
--------------------------------------------------------------------------------
1 | ioFile->read($templatePath);
62 |
63 | $newFileContent = str_replace(
64 | $fieldsToUpdate,
65 | $fieldsReplacement,
66 | $template
67 | );
68 | } catch (\Exception $e) {
69 | throw new LocalizedException(__("Template file not found: %1", $e->getMessage()));
70 | }
71 |
72 | $newFilePath = $newFilePathFromModuleDirectory;
73 | if (!$isNewModule) {
74 | $newFilePath = $this->reader->getModuleDir(null, $this->currentModule->getModuleName())
75 | . '/' . $newFilePathFromModuleDirectory;
76 | }
77 |
78 | try {
79 | $newFilePathWithName = $newFilePath . '/' . $fileName;
80 |
81 | if (!$this->ioFile->fileExists($newFilePathWithName, false)) {
82 | $this->ioFile->mkdir($newFilePath, 0755);
83 | }
84 |
85 | $this->ioFile->write($newFilePathWithName, $newFileContent);
86 | } catch (\Exception $e) {
87 | throw new LocalizedException(__("Error while attempting to create file: %1", $e->getMessage()));
88 | }
89 | return $newFilePathWithName;
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/Generator/GeneratorController.php:
--------------------------------------------------------------------------------
1 | entityName = $this->stringTransformationTools->getPascalCase($entityName);
33 | return $this;
34 | }
35 |
36 | public function getEntityName(): string
37 | {
38 | return $this->entityName;
39 | }
40 |
41 | /**
42 | * @return string
43 | * @throws ExistingClassException
44 | */
45 | public function generateListingController(): string
46 | {
47 | $modulePath = $this->currentModule->getModulePath();
48 | $controllerPath = mb_ucfirst(
49 | mb_strtolower($this->stringTransformationTools->getCamelCase($this->getEntityName()), 'UTF-8'),
50 | 'UTF-8'
51 | );
52 |
53 | $filePath = $modulePath . '/Controller/' . mb_ucfirst(Area::AREA_ADMINHTML . '/' . $controllerPath, 'UTF-8');
54 | $filePathWithName = $filePath . '/Index.php';
55 |
56 | if ($this->ioFile->fileExists($filePath)) {
57 | throw new ExistingClassException("Controller already exists", strtolower($controllerPath).'/index');
58 | }
59 |
60 | $namespace = $this->currentModule->getModuleNamespace('/Controller/Adminhtml/' . $this->getEntityName());
61 |
62 | $listingControllerContent = $this->classGenerator->generate(
63 | 'Index',
64 | $namespace,
65 | [
66 | "ADMIN_RESOURCE" => $this->currentModule->getModuleName() . '::view'
67 | ],
68 | [],
69 | [
70 | 'execute' => [
71 | 'visibility' => 'public',
72 | 'arguments' => [],
73 | 'returnType' => \Magento\Framework\Controller\ResultInterface::class,
74 | 'body' => [
75 | '$resultPage = $this->resultFactory->create(\Magento\Framework\Controller\ResultFactory::TYPE_PAGE);'. PHP_EOL .
76 | '$resultPage->setActiveMenu("' . $this->currentModule->getModuleName() . '::index");'. PHP_EOL .
77 | 'return $resultPage;'
78 | ]
79 | ]
80 | ],
81 | '\\'.\Magento\Backend\App\Action::class
82 | );
83 |
84 | if (!$this->ioFile->fileExists($filePathWithName, false)) {
85 | $this->ioFile->mkdir($filePath, 0755);
86 | }
87 |
88 | $this->ioFile->write(
89 | $filePathWithName,
90 | $listingControllerContent
91 | );
92 | return strtolower($controllerPath).'/index';
93 | }
94 |
95 | /**
96 | * @return array
97 | */
98 | public function generateFormControllers(): array
99 | {
100 | $modulePath = $this->currentModule->getModulePath();
101 | $controllerPath = mb_ucfirst(
102 | mb_strtolower($this->stringTransformationTools->getCamelCase($this->getEntityName()), 'UTF-8'),
103 | 'UTF-8'
104 | );
105 | $controllers = [];
106 |
107 | $filePath = $modulePath . '/Controller/' . mb_ucfirst(Area::AREA_ADMINHTML . '/' . $controllerPath, 'UTF-8');
108 | $filePathWithName = $filePath . '/Edit.php';
109 |
110 | $namespace = $this->currentModule->getModuleNamespace('/Controller/Adminhtml/' . $this->getEntityName());
111 |
112 | $repositoryInterface = $this->currentModule->getModuleNamespace( '\Api') . '\\' . $this->getEntityName() . 'RepositoryInterface';
113 | $factory = $this->currentModule->getModuleNamespace( '\Model') . '\\' . $this->getEntityName() . 'Factory';
114 |
115 | $repositoryVariableName = $this->getEntityNameVariable() . 'Repository';
116 | try {
117 | $this->checkConstructorVariable($repositoryVariableName, \Magento\Backend\App\Action::class);
118 | } catch (\InvalidArgumentException $e) {
119 | $repositoryVariableName = mb_lcfirst($this->currentModule->getModuleName()).$this->getEntityName().'Repository';
120 | }
121 |
122 | $factoryVariableName = $this->getEntityNameVariable() . 'Factory';
123 | try {
124 | $this->checkConstructorVariable($factoryVariableName, \Magento\Backend\App\Action::class);
125 | } catch (\InvalidArgumentException $e) {
126 | $factoryVariableName = mb_lcfirst($this->currentModule->getModuleName()).$this->getEntityName().'Factory';
127 | }
128 |
129 | // Edit Controller
130 | if ($this->ioFile->fileExists($filePath)) {
131 | $controllers['edit'] = $filePathWithName;
132 | } else {
133 | $editControllerContent = $this->classGenerator->generate(
134 | 'Edit',
135 | $namespace,
136 | [
137 | "ADMIN_RESOURCE" => $this->currentModule->getModuleName() . '::manage'
138 | ],
139 | [],
140 | [
141 | '__construct' => [
142 | 'visibility' => 'public',
143 | 'arguments' => [
144 | '\Magento\Backend\App\Action\Context $context',
145 | 'private readonly \\'. $repositoryInterface .' $' . $repositoryVariableName,
146 | 'private readonly \\'. $factory .' $' . $factoryVariableName
147 | ],
148 | 'body' => [
149 | 'parent::__construct($context);'
150 | ]
151 | ],
152 | 'execute' => [
153 | 'visibility' => 'public',
154 | 'arguments' => [],
155 | 'returnType' => \Magento\Framework\Controller\ResultInterface::class,
156 | 'body' => [
157 | '$id = $this->getRequest()->getParam("id");' . PHP_EOL .
158 | '$resultPage = $this->resultFactory->create(\Magento\Framework\Controller\ResultFactory::TYPE_PAGE);' . PHP_EOL .
159 | 'if ($id) {' . PHP_EOL .
160 | ' $' . $this->getEntityNameVariable() . ' = $this->' . $repositoryVariableName . '->getById($id);' . PHP_EOL .
161 | ' if (!$' . $this->getEntityNameVariable() . '->getId()) {' . PHP_EOL .
162 | ' $this->messageManager->addErrorMessage(__("This '. strtolower($this->getEntityName()) .' no longer exists."));' . PHP_EOL .
163 | ' $resultRedirect = $this->resultRedirectFactory->create();' . PHP_EOL .
164 | ' return $resultRedirect->setPath("*/*/");' . PHP_EOL .
165 | ' }' . PHP_EOL .
166 | ' $resultPage->getConfig()->getTitle()->prepend(__("Edit '.$this->getEntityName().'"));' . PHP_EOL .
167 | '} else {' . PHP_EOL .
168 | ' $' . $this->getEntityNameVariable() . ' = $this->' . $factoryVariableName . '->create();' . PHP_EOL .
169 | ' $resultPage->getConfig()->getTitle()->prepend(__("New '.$this->getEntityName().'"));' . PHP_EOL .
170 | '}' . PHP_EOL .
171 | '$resultPage->setActiveMenu("' . $this->currentModule->getModuleName() . '::index");' . PHP_EOL .
172 | PHP_EOL .
173 | 'return $resultPage;'
174 |
175 | ]
176 | ]
177 | ],
178 | '\\'.\Magento\Backend\App\Action::class
179 | );
180 |
181 | if (!$this->ioFile->fileExists($filePathWithName, false)) {
182 | $this->ioFile->mkdir($filePath, 0755);
183 | }
184 |
185 | $this->ioFile->write(
186 | $filePathWithName,
187 | $editControllerContent
188 | );
189 |
190 | $controllers['edit'] = $filePathWithName;
191 | }
192 |
193 | // Create Controller
194 | $filePathWithName = $filePath . '/Create.php';
195 |
196 | if ($this->ioFile->fileExists($filePath)) {
197 | $controllers['create'] = $filePathWithName;
198 | } else {
199 | $createControllerContent = $this->classGenerator->generate(
200 | 'Create',
201 | $namespace,
202 | [
203 | "ADMIN_RESOURCE" => $this->currentModule->getModuleName() . '::manage'
204 | ],
205 | [],
206 | [
207 | 'execute' => [
208 | 'visibility' => 'public',
209 | 'arguments' => [],
210 | 'returnType' => \Magento\Framework\Controller\ResultInterface::class,
211 | 'body' => [
212 | '$resultPage = $this->resultFactory->create(\Magento\Framework\Controller\ResultFactory::TYPE_FORWARD);'. PHP_EOL .
213 | '$resultPage->forward("edit");'. PHP_EOL .
214 | 'return $resultPage;'
215 | ]
216 | ]
217 | ],
218 | '\\'.\Magento\Backend\App\Action::class
219 | );
220 |
221 | if (!$this->ioFile->fileExists($filePathWithName, false)) {
222 | $this->ioFile->mkdir($filePath, 0755);
223 | }
224 |
225 | $this->ioFile->write(
226 | $filePathWithName,
227 | $createControllerContent
228 | );
229 | }
230 |
231 | // Save Controller
232 | $filePathWithName = $filePath . '/Save.php';
233 |
234 | if ($this->ioFile->fileExists($filePath)) {
235 | $controllers['save'] = $filePathWithName;
236 | } else {
237 | $saveControllerContent = $this->classGenerator->generate(
238 | 'Save',
239 | $namespace,
240 | [
241 | "ADMIN_RESOURCE" => $this->currentModule->getModuleName() . '::manage'
242 | ],
243 | [],
244 | [
245 | '__construct' => [
246 | 'visibility' => 'public',
247 | 'arguments' => [
248 | '\Magento\Backend\App\Action\Context $context',
249 | 'private readonly \\'. $repositoryInterface .' $' . $repositoryVariableName,
250 | 'private readonly \\'. $factory .' $' . $factoryVariableName
251 | ],
252 | 'body' => [
253 | 'parent::__construct($context);'
254 | ]
255 | ],
256 | 'execute' => [
257 | 'visibility' => 'public',
258 | 'arguments' => [],
259 | 'returnType' => \Magento\Framework\Controller\ResultInterface::class,
260 | 'body' => [
261 | 'try {' . PHP_EOL .
262 | '$' . $this->getEntityNameVariable() . ' = $this->' . $factoryVariableName . '->create();' . PHP_EOL .
263 | '$request = $this->_request->getParams();'. PHP_EOL .
264 | '$' . $this->getEntityNameVariable() . '->setData($request);'. PHP_EOL .
265 | '$this->' . $repositoryVariableName . '->save($' . $this->getEntityNameVariable() . ');'. PHP_EOL .
266 | '$this->messageManager->addSuccessMessage(__("'.$this->getEntityName().' have been registered."));' . PHP_EOL .
267 | '} catch (\Magento\Framework\Exception\LocalizedException $e) {' . PHP_EOL .
268 | '$this->messageManager->addErrorMessage($e->getMessage());' . PHP_EOL .
269 | '} catch (\Exception $e) {' . PHP_EOL .
270 | '$this->messageManager->addExceptionMessage($e, __("Something went wrong while saving the '.$this->getEntityName().'."));' . PHP_EOL .
271 | '}' . PHP_EOL .
272 | 'return $this->resultRedirectFactory->create()->setPath("*/*/index");'
273 | ]
274 | ]
275 | ],
276 | '\\'.\Magento\Backend\App\Action::class
277 | );
278 |
279 | if (!$this->ioFile->fileExists($filePathWithName, false)) {
280 | $this->ioFile->mkdir($filePath, 0755);
281 | }
282 |
283 | $this->ioFile->write(
284 | $filePathWithName,
285 | $saveControllerContent
286 | );
287 | }
288 |
289 | // Delete Controller
290 | $filePathWithName = $filePath . '/Delete.php';
291 |
292 | if ($this->ioFile->fileExists($filePath)) {
293 | $controllers['delete'] = $filePathWithName;
294 | } else {
295 | $deleteControllerContent = $this->classGenerator->generate(
296 | 'Delete',
297 | $namespace,
298 | [
299 | "ADMIN_RESOURCE" => $this->currentModule->getModuleName() . '::manage'
300 | ],
301 | [],
302 | [
303 | '__construct' => [
304 | 'visibility' => 'public',
305 | 'arguments' => [
306 | '\Magento\Backend\App\Action\Context $context',
307 | 'private readonly \\' . $repositoryInterface . ' $' . $repositoryVariableName
308 | ],
309 | 'body' => [
310 | 'parent::__construct($context);'
311 | ]
312 | ],
313 | 'execute' => [
314 | 'visibility' => 'public',
315 | 'arguments' => [],
316 | 'returnType' => \Magento\Framework\Controller\ResultInterface::class,
317 | 'body' => [
318 | '$id = $this->getRequest()->getParam("id");' . PHP_EOL .
319 | 'if ($id) {' . PHP_EOL .
320 | ' try {' . PHP_EOL .
321 | ' $' . $this->getEntityNameVariable() . ' = $this->' . $repositoryVariableName . '->getById($id);' . PHP_EOL .
322 | ' $this->' . $repositoryVariableName . '->delete($' . $this->getEntityNameVariable() . ');' . PHP_EOL .
323 | ' $this->messageManager->addSuccessMessage(__("' . $this->getEntityName() . ' have been deleted."));' . PHP_EOL .
324 | ' } catch (\Magento\Framework\Exception\LocalizedException $e) {' . PHP_EOL .
325 | ' $this->messageManager->addErrorMessage($e->getMessage());' . PHP_EOL .
326 | ' } catch (\Exception $e) {' . PHP_EOL .
327 | ' $this->messageManager->addExceptionMessage($e, __("Something went wrong while deleting the ' . $this->getEntityName() . '."));' . PHP_EOL .
328 | ' }' . PHP_EOL .
329 | '} else {' . PHP_EOL .
330 | ' $this->messageManager->addErrorMessage(__("We can\'t find a ' . $this->getEntityName() . ' to delete."));' . PHP_EOL .
331 | '}' . PHP_EOL .
332 | 'return $this->resultRedirectFactory->create()->setPath("*/*/index");'
333 | ]
334 | ]
335 | ],
336 | '\\'.\Magento\Backend\App\Action::class
337 | );
338 |
339 | if (!$this->ioFile->fileExists($filePathWithName, false)) {
340 | $this->ioFile->mkdir($filePath, 0755);
341 | }
342 |
343 | $this->ioFile->write(
344 | $filePathWithName,
345 | $deleteControllerContent
346 | );
347 | }
348 |
349 |
350 | return $controllers;
351 | }
352 |
353 | private function checkConstructorVariable(string $variable, string $parentClass): string
354 | {
355 | $parentClassReflection = new \ReflectionClass($parentClass);
356 | $parentClassProperties = $parentClassReflection->getProperties();
357 | if (!empty($parentClassProperties)) {
358 | foreach ($parentClassProperties as $property) {
359 | if ($property->getName() === $variable) {
360 | throw new \InvalidArgumentException("Variable $variable already exists in parent class $parentClass");
361 | }
362 | }
363 | }
364 | return $variable;
365 | }
366 |
367 | private function getEntityNameVariable(): string
368 | {
369 | return mb_lcfirst($this->stringTransformationTools->getCamelCase($this->getEntityName()));
370 | }
371 | }
372 |
--------------------------------------------------------------------------------
/Generator/GeneratorCrud.php:
--------------------------------------------------------------------------------
1 | entityName = $this->stringTransformationTools->getPascalCase($entityName);
45 | return $this;
46 | }
47 |
48 | public function getEntityName(): string
49 | {
50 | return $this->entityName;
51 | }
52 |
53 | /**
54 | * @return string
55 | * @throws ExistingClassException
56 | * @throws LocalizedException
57 | */
58 | public function generateRoutes(): string
59 | {
60 | $moduleNameSnakeCase = $this->currentModule->getModuleNameSnakeCase();
61 |
62 | if ($this->ioFile->fileExists(Dir::MODULE_ETC_DIR . '/' . Area::AREA_ADMINHTML . '/' . self::ROUTES_XML)) {
63 | throw new ExistingClassException("Routes already exists", Dir::MODULE_ETC_DIR . '/' . Area::AREA_ADMINHTML . '/' . self::ROUTES_XML);
64 | }
65 |
66 | $templatePath = $this->reader->getModuleDir(null, self::OPENGENTO_MAKEGENTO_CLI)
67 | . '/Generator/templates/etc/adminhtml/routes.xml.tpl';
68 |
69 | $fieldsToUpdate = ['{{id}}', '{{frontName}}', '{{name}}'];
70 | $fieldsReplacement = [$moduleNameSnakeCase, $moduleNameSnakeCase, $this->currentModule->getModuleName()];
71 |
72 | $this->generate(
73 | $templatePath,
74 | $fieldsToUpdate,
75 | $fieldsReplacement,
76 | Dir::MODULE_ETC_DIR . '/' . Area::AREA_ADMINHTML,
77 | self::ROUTES_XML,
78 | );
79 | return $moduleNameSnakeCase;
80 | }
81 |
82 | /**
83 | * Generate menu.xml file
84 | *
85 | * @return void
86 | * @throws LocalizedException
87 | */
88 | public function generateMenuEntry(string $route, string $listingControllerRoute): void
89 | {
90 | $templatePath = $this->reader->getModuleDir(null, self::OPENGENTO_MAKEGENTO_CLI)
91 | . '/Generator/templates/etc/adminhtml/menu.xml.tpl';
92 |
93 | list($controller, $action) = explode('/', $listingControllerRoute);
94 |
95 | $fieldsToUpdate = [
96 | '{{module_name}}',
97 | '{{menuEntryTitle}}',
98 | '{{order}}',
99 | '{{frontName}}',
100 | '{{controller}}',
101 | '{{action}}'
102 | ];
103 | $fieldsReplacement = [
104 | $this->currentModule->getModuleName(),
105 | str_replace('_', ' ', $this->currentModule->getModuleName()),
106 | '42',
107 | $route,
108 | $controller,
109 | $action
110 | ];
111 |
112 | $this->generate(
113 | $templatePath,
114 | $fieldsToUpdate,
115 | $fieldsReplacement,
116 | Dir::MODULE_ETC_DIR . '/' . Area::AREA_ADMINHTML,
117 | self::MENU_XML,
118 | );
119 | }
120 |
121 | /**
122 | * Generate Listing Layout file
123 | *
124 | * @param string $route
125 | * @return string
126 | * @throws LocalizedException
127 | */
128 | public function generateListingLayout(string $route): string
129 | {
130 | $templatePath = $this->reader->getModuleDir(null, self::OPENGENTO_MAKEGENTO_CLI)
131 | . '/Generator/templates/view/adminhtml/layout/listing.xml.tpl';
132 |
133 | $layoutName = $route . '_' . strtolower($this->getEntityName()) . '_index';
134 |
135 | $layoutUiComponentName = $route . '_' . strtolower($this->getEntityName()) . '_' . self::LISTING;
136 |
137 | $layoutFileName = $layoutName . self::XML_EXTENSION;
138 |
139 | $fieldsToUpdate = [
140 | '{{uiComponentName}}'
141 | ];
142 | $fieldsReplacement = [
143 | $layoutUiComponentName,
144 | ];
145 |
146 | $this->generate(
147 | $templatePath,
148 | $fieldsToUpdate,
149 | $fieldsReplacement,
150 | Dir::MODULE_VIEW_DIR . '/' . Area::AREA_ADMINHTML . '/layout',
151 | $layoutFileName,
152 | );
153 | return $layoutUiComponentName;
154 | }
155 |
156 | /**
157 | * Generate ACL file
158 | *
159 | * @return void
160 | * @throws LocalizedException
161 | */
162 | public function generateAcl(): void
163 | {
164 | $templatePath = $this->reader->getModuleDir(null, self::OPENGENTO_MAKEGENTO_CLI)
165 | . '/Generator/templates/etc/acl.xml.tpl';
166 |
167 | $fieldsToUpdate = [
168 | '{{module_name}}',
169 | '{{aclTitleView}}',
170 | '{{aclTitleManage}}',
171 | ];
172 | $fieldsReplacement = [
173 | $this->currentModule->getModuleName(),
174 | str_replace('_', ' ', $this->currentModule->getModuleName()) . ' View',
175 | str_replace('_', ' ', $this->currentModule->getModuleName()) . ' Manage',
176 | ];
177 |
178 | $this->generate(
179 | $templatePath,
180 | $fieldsToUpdate,
181 | $fieldsReplacement,
182 | Dir::MODULE_ETC_DIR,
183 | self::ACL_XML,
184 | );
185 | }
186 | }
187 |
--------------------------------------------------------------------------------
/Generator/GeneratorModel.php:
--------------------------------------------------------------------------------
1 | currentModule->getModulePath();
39 | $newFilePath = $modulePath . '/Api/Data/';
40 | $newFilePathWithName = $newFilePath . $modelClassName . 'Interface.php';
41 |
42 | $namespace = $this->currentModule->getModuleNamespace('/Api/Data');
43 |
44 | if ($this->ioFile->fileExists($newFilePathWithName)) {
45 | throw new ExistingClassException('Interface already exists', $newFilePathWithName, '\\' . $namespace . '\\' . $modelClassName . 'Interface');
46 | }
47 |
48 | $interfaceContent = $this->interfaceGenerator->generate(
49 | $modelClassName . 'Interface',
50 | $namespace,
51 | $this->initInterfaceConst($properties),
52 | $this->initGetterSetter($properties)
53 | );
54 |
55 | if (!$this->ioFile->fileExists($newFilePathWithName, false)) {
56 | $this->ioFile->mkdir($newFilePath, 0755);
57 | }
58 |
59 | $this->ioFile->write($newFilePathWithName, $interfaceContent);
60 |
61 | return '\\' . $namespace . '\\' . $modelClassName . 'Interface';
62 | }
63 |
64 | /**
65 | * Generate a model class for the model interface
66 | *
67 | * @param string $modelClassName
68 | * @param array $properties
69 | * @param string $interface
70 | * @param string $subFolder
71 | * @return string
72 | * @throws ExistingClassException
73 | */
74 | public function generateModel(string $modelClassName, array $properties, string $interface, $subFolder = ''): string
75 | {
76 | $modulePath = $this->currentModule->getModulePath();
77 | if (empty($subFolder)) {
78 | $subFolder = '/Model/';
79 | } else {
80 | $subFolder = '/Model/' . $subFolder . '/';
81 | }
82 | $newFilePath = $modulePath . $subFolder;
83 | $newFilePathWithName = $newFilePath . $modelClassName . '.php';
84 |
85 | $namespace = $this->currentModule->getModuleNamespace(rtrim($subFolder, '/'));
86 |
87 | if ($this->ioFile->fileExists($newFilePathWithName)) {
88 | throw new ExistingClassException('Model already exists', $newFilePathWithName, '\\' . $namespace . '\\' . $modelClassName);
89 | }
90 |
91 | $classContent = $this->classGenerator->generate(
92 | $modelClassName,
93 | $namespace,
94 | [],
95 | [],
96 | $this->initGetterSetter($properties),
97 | '\Magento\Framework\Model\AbstractModel',
98 | [$interface]
99 | );
100 |
101 | if (!$this->ioFile->fileExists($newFilePathWithName, false)) {
102 | $this->ioFile->mkdir($newFilePath, 0755);
103 | }
104 |
105 | $this->ioFile->write($newFilePathWithName, $classContent);
106 |
107 | return '\\' . $namespace . '\\' . $modelClassName;
108 | }
109 |
110 | /**
111 | * Generate a resource model class for the model
112 | *
113 | * @param string $modelClassName
114 | * @param string $modelClassInterface
115 | * @param string $tableName
116 | * @param string $subFolder
117 | * @return string
118 | * @throws ExistingClassException
119 | */
120 | public function generateResourceModel(string $modelClassName, string $modelClassInterface, string $tableName, string $subFolder = ''): string
121 | {
122 | $modulePath = $this->currentModule->getModulePath();
123 | if (empty($subFolder)) {
124 | $subFolder = self::RESOURCE_MODEL_PATH;
125 | } else {
126 | $subFolder = self::RESOURCE_MODEL_PATH . $subFolder . '/';
127 | }
128 | $newFilePath = $modulePath . $subFolder;
129 | $newFilePathWithName = $newFilePath . $modelClassName . '.php';
130 |
131 | $namespace = $this->currentModule->getModuleNamespace(rtrim($subFolder, '/'), );
132 |
133 | if ($this->ioFile->fileExists($newFilePathWithName)) {
134 | throw new ExistingClassException('Resource model already exists', $newFilePathWithName, '\\' . $namespace . '\\' . $modelClassName);
135 | }
136 |
137 | $resourceModelConstructor['_construct'] = [
138 | 'body' => "\$this->_init('{$tableName}', {$modelClassInterface}::ID);",
139 | 'visibility' => 'protected'
140 | ];
141 |
142 | $classContent = $this->classGenerator->generate(
143 | $modelClassName,
144 | $namespace,
145 | [],
146 | [],
147 | $resourceModelConstructor,
148 | '\Magento\Framework\Model\ResourceModel\Db\AbstractDb'
149 | );
150 |
151 | if (!$this->ioFile->fileExists($newFilePath, false)) {
152 | $this->ioFile->mkdir($newFilePath, 0755);
153 | }
154 |
155 | $this->ioFile->write($newFilePathWithName, $classContent);
156 |
157 | return '\\' . $namespace . '\\' . $modelClassName;
158 | }
159 |
160 | /**
161 | * Generate a collection class for the model and the resource model
162 | *
163 | * @param string $modelClassName
164 | * @param string $modelClass
165 | * @param string $resourceModelClass
166 | * @param string $subFolder
167 | * @return string
168 | * @throws ExistingClassException
169 | */
170 | public function generateCollection(string $modelClassName, string $modelClass, string $resourceModelClass, string $subFolder = ''): string
171 | {
172 | $modulePath = $this->currentModule->getModulePath();
173 | if (empty($subFolder)) {
174 | $subFolder = self::RESOURCE_MODEL_PATH;
175 | } else {
176 | $subFolder = self::RESOURCE_MODEL_PATH . $subFolder . '/';
177 | }
178 | $newFilePath = $modulePath . $subFolder . $modelClassName . '/';
179 | $newFilePathWithName = $newFilePath . 'Collection.php';
180 |
181 | $namespace = $this->currentModule->getModuleNamespace(rtrim($subFolder . $modelClassName, '/'));
182 |
183 | if ($this->ioFile->fileExists($newFilePathWithName)) {
184 | throw new ExistingClassException('Collection already exists', $newFilePathWithName, '\\' . $namespace . '\\Collection');
185 | }
186 |
187 | $collectionConstructor['_construct'] = [
188 | 'body' => "\$this->_init($modelClass::class, $resourceModelClass::class);",
189 | 'visibility' => 'protected'
190 | ];
191 |
192 | $classContent = $this->classGenerator->generate(
193 | 'Collection',
194 | $namespace,
195 | [],
196 | [],
197 | $collectionConstructor,
198 | '\Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection'
199 | );
200 |
201 | if (!$this->ioFile->fileExists($newFilePathWithName, false)) {
202 | $this->ioFile->mkdir($newFilePath, 0755);
203 | }
204 |
205 | $this->ioFile->write($newFilePathWithName, $classContent);
206 |
207 | return '\\' . $namespace . '\\' . $modelClassName . '\\Collection';
208 | }
209 |
210 | /**
211 | * Loops through the properties and creates the getter and setter methods
212 | *
213 | * @param $properties
214 | * @return array
215 | */
216 | private function initGetterSetter($properties): array
217 | {
218 | $methods = [];
219 | foreach ($properties as $property => $definition) {
220 | $type = $this->classGenerator->getPhpTypeFromEntityType($definition['type']);
221 | $constName = $this->getConstName($property, $definition);
222 | $methods["get" . $this->stringTransformationTools->getPascalCase($property)] = [
223 | 'arguments' => [],
224 | 'body' => "return \$this->getData(self::$constName);",
225 | 'visibility' => 'public'
226 | ];
227 | $methods["set" . $this->stringTransformationTools->getPascalCase($property)] = [
228 | 'arguments' => ["$type \$$property"],
229 | 'body' => "\$this->setData(self::$constName, \$$property);",
230 | 'visibility' => 'public'
231 | ];
232 | }
233 |
234 | return $methods;
235 | }
236 |
237 | /**
238 | * Loops through the properties and creates the constants for the interface
239 | *
240 | * @param array $properties
241 | * @return array
242 | */
243 | private function initInterfaceConst(array $properties): array
244 | {
245 | $methods = [];
246 | foreach ($properties as $property => $definition) {
247 | $constName = $this->getConstName($property, $definition);
248 | $methods[$constName] = $property;
249 | }
250 |
251 | return $methods;
252 | }
253 |
254 | /**
255 | * Get the constant name for the property
256 | *
257 | * @param string $property
258 | * @param array $definition
259 | * @return string
260 | */
261 | private function getConstName(string $property, array $definition): string
262 | {
263 | if (!empty($definition['identity'])) {
264 | return 'ID';
265 | }
266 | return strtoupper($this->stringTransformationTools->getSnakeCase($property));
267 | }
268 | }
269 |
--------------------------------------------------------------------------------
/Generator/GeneratorModule.php:
--------------------------------------------------------------------------------
1 | stringTransformationTools->getKebabCase($moduleVendorName)
19 | . '/'
20 | . $this->stringTransformationTools->getKebabCase($moduleName);
21 | }
22 |
23 | /**
24 | * @return void
25 | */
26 | public function generateModule(string $moduleVendorName, string $moduleName): void
27 | {
28 | $moduleVendorName = $this->stringTransformationTools
29 | ->getPascalCase($this->stringTransformationTools->sanitizeString($moduleVendorName));
30 | $moduleName = $this->stringTransformationTools
31 | ->getPascalCase($this->stringTransformationTools->sanitizeString($moduleName));
32 | $fullMagentoStandardNewModuleName = $moduleVendorName . '_' . $moduleName;
33 |
34 | $this->currentModule->setCurrentModule($fullMagentoStandardNewModuleName);
35 |
36 | $explodedPath = [
37 | DirectoryList::APP,
38 | DirectoryList::GENERATED_CODE,
39 | $moduleVendorName,
40 | $moduleName,
41 | ];
42 |
43 | $rootPath = $this->filesystem->getDirectoryRead(DirectoryList::ROOT);
44 |
45 | $currentWorkingPath = $rootPath->getAbsolutePath();
46 | foreach ($explodedPath as $key => $path) {
47 | $currentWorkingPath .= $path;
48 | $currentWorkingPath .= ($key < count($explodedPath) - 1) ? '/' : '';
49 | if (!$rootPath->isDirectory($currentWorkingPath)) {
50 | $this->ioFile->mkdir($currentWorkingPath, 0755);
51 | }
52 | }
53 |
54 | $etcModuleXmlTemplatePath = $this->reader->getModuleDir(null, self::OPENGENTO_MAKEGENTO_CLI)
55 | . '/Generator/templates/etc/module.xml.tpl';
56 | $composerJsonTemplatePath = $this->reader->getModuleDir(null, self::OPENGENTO_MAKEGENTO_CLI)
57 | . '/Generator/templates/composer.json.tpl';
58 | $regristrationPhpTemplatePath = $this->reader->getModuleDir(null, self::OPENGENTO_MAKEGENTO_CLI)
59 | . '/Generator/templates/registration.php.tpl';
60 |
61 | // etc/module.xml
62 | $this->generate(
63 | $etcModuleXmlTemplatePath,
64 | ['{{fullMagentoStandardNewModuleName}}'],
65 | [$fullMagentoStandardNewModuleName],
66 | $currentWorkingPath . '/' . DirectoryList::CONFIG,
67 | 'module.xml',
68 | true,
69 | );
70 |
71 | // composer.json
72 | $this->generate(
73 | $composerJsonTemplatePath,
74 | [
75 | '{{fullComposerStandardNewModuleName}}',
76 | '{{vendorName}}',
77 | '{{moduleName}}',
78 | ],
79 | [
80 | $this->getComposerStandardModuleName($moduleVendorName, $moduleName),
81 | $moduleVendorName,
82 | $moduleName,
83 | ],
84 | $currentWorkingPath,
85 | 'composer.json',
86 | true,
87 | );
88 |
89 | // registration.php
90 | $this->generate(
91 | $regristrationPhpTemplatePath,
92 | ['{{fullMagentoStandardNewModuleName}}'],
93 | [$fullMagentoStandardNewModuleName],
94 | $currentWorkingPath,
95 | 'registration.php',
96 | true,
97 | );
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/Generator/GeneratorRepository.php:
--------------------------------------------------------------------------------
1 | currentModule->getModulePath();
35 | $newFilePath = $modulePath . '/Api/Data/';
36 | $newFilePathWithName = $newFilePath . $modelClassName . 'SearchCriteriaInterface.php';
37 |
38 | $namespace = $this->currentModule->getModuleNamespace('/Api/Data');
39 |
40 | if ($this->ioFile->fileExists($newFilePathWithName)) {
41 | throw new ExistingClassException('Interface already exists', $newFilePathWithName, '\\' . $namespace . '\\' . $modelClassName . 'SearchCriteriaInterface');
42 | }
43 |
44 | $interfaceContent = $this->interfaceGenerator->generate(
45 | $modelClassName . 'SearchCriteriaInterface',
46 | $namespace,
47 | [],
48 | [
49 | 'setItems' => ['visibility' => 'public', 'returnType' => 'void', 'arguments' => ['array $items']],
50 | 'getItems' => ['visibility' => 'public', 'returnType' => 'array']
51 | ]
52 | );
53 |
54 | if (!$this->ioFile->fileExists($newFilePathWithName, false)) {
55 | $this->ioFile->mkdir($newFilePath, 0755);
56 | }
57 |
58 | $this->ioFile->write($newFilePathWithName, $interfaceContent);
59 |
60 | return '\\' . $namespace . '\\' . $modelClassName . 'SearchCriteriaInterface';
61 | }
62 |
63 | /**
64 | * @param string $modulePath
65 | * @param string $modelClassName
66 | * @param string $searchCriteriaInterface
67 | * @param string $resourceClass
68 | * @param string $collectionClass
69 | * @param string $modelInterface
70 | * @param string $namespace
71 | * @return string
72 | * @throws ExistingClassException
73 | */
74 | public function generateRepositoryInterface(
75 | string $modelClassName,
76 | string $searchCriteriaInterface,
77 | string $resourceClass,
78 | string $collectionClass,
79 | string $modelInterface
80 | ): string
81 | {
82 | $modulePath = $this->currentModule->getModulePath();
83 | $newFilePath = $modulePath . '/Api/';
84 | $newFilePathWithName = $newFilePath . $modelClassName . 'RepositoryInterface.php';
85 |
86 | $namespace = $this->currentModule->getModuleNamespace( '/Api');
87 |
88 | if ($this->ioFile->fileExists($newFilePathWithName)) {
89 | throw new ExistingClassException('Interface already exists', $newFilePathWithName, '\\' . $namespace . '\\' . $modelClassName . 'RepositoryInterface');
90 | }
91 |
92 | $interfaceContent = $this->interfaceGenerator->generate(
93 | $modelClassName . 'RepositoryInterface',
94 | $namespace,
95 | [],
96 | $this->getRepositoryMethods($modelClassName, $searchCriteriaInterface, $resourceClass, $collectionClass, $modelInterface, $namespace)
97 | );
98 |
99 | if (!$this->ioFile->fileExists($newFilePathWithName, false)) {
100 | $this->ioFile->mkdir($newFilePath, 0755);
101 | }
102 |
103 | $this->ioFile->write($newFilePathWithName, $interfaceContent);
104 |
105 | return '\\' . $namespace . '\\' . $modelClassName . 'RepositoryInterface';
106 | }
107 |
108 | /**
109 | * @param string $modelClassName
110 | * @param string $searchCriteriaInterface
111 | * @param string $resourceClass
112 | * @param string $collectionClass
113 | * @param string $modelInterface
114 | * @param string $repositoryInterface
115 | * @return string
116 | * @throws ExistingClassException
117 | */
118 | public function generateRepository(
119 | string $modelClassName,
120 | string $searchCriteriaInterface,
121 | string $resourceClass,
122 | string $collectionClass,
123 | string $modelInterface,
124 | string $repositoryInterface,
125 | ): string
126 | {
127 | $modulePath = $this->currentModule->getModulePath();
128 | $newFilePath = $modulePath . '/Model/';
129 | $newFilePathWithName = $newFilePath . $modelClassName . 'Repository.php';
130 |
131 | $namespace = $this->currentModule->getModuleNamespace('/Model');
132 |
133 | if ($this->ioFile->fileExists($newFilePathWithName)) {
134 | throw new ExistingClassException('Model already exists', $newFilePathWithName, '\\' . $namespace . '\\' . $modelClassName . 'Repository');
135 | }
136 |
137 | $repositoryContent = $this->classGenerator->generate(
138 | $modelClassName . 'Repository',
139 | $namespace,
140 | [],
141 | [],
142 | $this->getRepositoryMethods($modelClassName, $searchCriteriaInterface, $resourceClass, $collectionClass, $modelInterface),
143 | '',
144 | [$repositoryInterface]
145 | );
146 |
147 | if (!$this->ioFile->fileExists($newFilePathWithName, false)) {
148 | $this->ioFile->mkdir($newFilePath, 0755);
149 | }
150 |
151 | $this->ioFile->write($newFilePathWithName, $repositoryContent);
152 |
153 | return '\\' . $namespace . '\\' . $modelClassName . 'Repository';
154 | }
155 |
156 | private function getRepositoryMethods(
157 | string $modelClassName,
158 | string $searchCriteriaInterface,
159 | string $resourceClass,
160 | string $collectionClass,
161 | string $modelInterface
162 | ): array
163 | {
164 | $namespace = $this->currentModule->getModuleNamespace('');
165 | $modelFactoryClass = '\\' . $namespace . '\\Model\\' . $modelClassName . 'Factory';
166 | $collectionFactoryClass = $collectionClass.'Factory';
167 |
168 | return [
169 | '__construct' => [
170 | 'visibility' => 'public',
171 | 'returnType' => 'void',
172 | 'arguments' => [
173 | "private readonly $resourceClass \$resource",
174 | "private readonly $modelFactoryClass \${$this->getConstructorArgumentName($modelFactoryClass)}",
175 | "private readonly $collectionFactoryClass \${$this->getConstructorArgumentName($collectionFactoryClass)}",
176 | "private readonly \Magento\Framework\Api\Search\SearchResultFactory \$searchResultFactory",
177 | "private readonly \Magento\Framework\Api\SearchCriteria\CollectionProcessorInterface \$collectionProcessor"
178 | ],
179 | 'body' => ''
180 | ],
181 | 'save' => [
182 | 'visibility' => 'public',
183 | 'returnType' => 'void',
184 | 'arguments' => [
185 | "$modelInterface \${$this->getConstructorArgumentName($modelClassName)}"
186 | ],
187 | 'body' =>
188 | "try {
189 | \$this->resource->save(\${$this->getConstructorArgumentName($modelClassName)});
190 | } catch (\\Exception \$exception) {
191 | throw new \\Magento\\Framework\\Exception\\CouldNotSaveException(__(\"Could not save the {$modelClassName}: %1\", \$exception->getMessage()));
192 | }
193 | return \${$this->getConstructorArgumentName($modelInterface)};"
194 | ],
195 | 'getById' => [
196 | 'visibility' => 'public',
197 | 'returnType' => $modelInterface,
198 | 'arguments' => [
199 | '$id'
200 | ],
201 | 'body' =>
202 | "\${$this->getConstructorArgumentName($modelClassName)} = \$this->{$this->getConstructorArgumentName($modelFactoryClass)}->create();
203 | \$this->resource->load(\${$this->getConstructorArgumentName($modelClassName)}, \$id);
204 | if (!\${$this->getConstructorArgumentName($modelClassName)}->getId()) {
205 | throw new \\Magento\\Framework\\Exception\\NoSuchEntityException(__(\"{$modelClassName} with id \\\"%1\\\" does not exist.\", \$id));
206 | }
207 | return \${$this->getConstructorArgumentName($modelClassName)};"
208 | ],
209 | 'getList' => [
210 | 'visibility' => 'public',
211 | 'returnType' => '\Magento\Framework\Api\SearchResultsInterface',
212 | 'arguments' => [
213 | "\Magento\Framework\Api\SearchCriteriaInterface \$searchCriteria"
214 | ],
215 | 'body' =>
216 | "\$collection = \$this->{$this->getConstructorArgumentName($collectionFactoryClass)}->create();
217 | \$this->collectionProcessor->process(\$searchCriteria, \$collection);
218 | \$searchResults = \$this->searchResultFactory->create();
219 | \$searchResults->setSearchCriteria(\$searchCriteria);
220 | \$searchResults->setItems(\$collection->getItems());
221 | \$searchResults->setTotalCount(\$collection->getSize());
222 | return \$searchResults;"
223 | ],
224 | 'delete' => [
225 | 'visibility' => 'public',
226 | 'returnType' => 'void',
227 | 'arguments' => [
228 | "$modelInterface \${$this->getConstructorArgumentName($modelClassName)}"
229 | ],
230 | 'body' =>
231 | "try {
232 | \$this->resource->delete(\${$this->getConstructorArgumentName($modelClassName)});
233 | } catch (\\Exception \$exception) {
234 | throw new \\Magento\\Framework\\Exception\\CouldNotDeleteException(__(\"Could not delete the {$modelClassName}: %1\", \$exception->getMessage()));
235 | }
236 | return true;"
237 | ]
238 | ];
239 | }
240 |
241 | private function getConstructorArgumentName(string $className): string
242 | {
243 | $classNameParts = explode('\\', $className);
244 | $className = end($classNameParts);
245 | $className = str_replace('Interface', '', $className);
246 | return lcfirst($className);
247 | }
248 | }
249 |
--------------------------------------------------------------------------------
/Generator/GeneratorUiComponent.php:
--------------------------------------------------------------------------------
1 | modulePath = $this->currentModule->getModulePath();
38 | $this->resourceModel = $entityName;
39 | $this->listingLayoutUiComponent = $listingLayoutUiComponent;
40 | $this->route = $route;
41 | $this->entityName = $entityName;
42 | $this->addVirtualTypes();
43 | $this->addUiComponent();
44 | }
45 |
46 | /**
47 | * @throws LocalizedException
48 | */
49 | private function getResource(): \Magento\Framework\Model\ResourceModel\Db\AbstractDb
50 | {
51 | if ($this->resource === null) {
52 | $directory = new \RecursiveDirectoryIterator($this->modulePath . '/Model/ResourceModel', FilesystemIterator::SKIP_DOTS);
53 | $iterator = new \RecursiveIteratorIterator($directory, \RecursiveIteratorIterator::SELF_FIRST);
54 |
55 | foreach ($iterator as $file) {
56 | if ($file->isFile()) {
57 | $modelFileName = $file->getBasename('.php');
58 | if ($modelFileName === $this->resourceModel) {
59 | // strip the path from the module path
60 | $path = str_replace($this->modulePath, '', $file->getPath());
61 | $path = str_replace('/', '\\', $path);
62 | $resourceModelClass = $this->currentModule->getModuleNamespace(). $path . '\\' . $modelFileName;
63 | $this->resource = $this->objectManager->create($resourceModelClass);
64 | break;
65 | }
66 | }
67 | }
68 |
69 | if ($this->resource === null) {
70 | throw new LocalizedException(__('Resource model not found'));
71 | }
72 | }
73 | return $this->resource;
74 | }
75 |
76 | /**
77 | * Create virtual types for the UI component and write them in the di.xml file.
78 | *
79 | * @throws LocalizedException
80 | */
81 | private function addVirtualTypes(): void
82 | {
83 | $mainTable = $this->getResource()->getMainTable();
84 | $this->dataProviderName = $this->currentModule->getModuleNamespace() . '\Ui\DataProvider\\' . $this->resourceModel;
85 | $filterPoolName = $this->currentModule->getModuleNamespace() . '\Ui\FilterPool\\' . $this->resourceModel;
86 | $this->gridCollectionName = $this->currentModule->getModuleNamespace() . '\\' . $this->resourceModel . '\Grid\Collection';
87 | $virtualTypes = [
88 | "dataprovider" => [
89 | 'name' => $this->dataProviderName,
90 | 'type' => 'Magento\Framework\View\Element\UiComponent\DataProvider\DataProvider',
91 | 'arguments' => [
92 | [
93 | 'name' => 'collection',
94 | 'type' => 'object',
95 | 'value' => $this->gridCollectionName
96 | ],
97 | [
98 | 'name' => 'filterPool',
99 | 'type' => 'object',
100 | 'value' => $filterPoolName
101 | ]
102 | ]
103 | ],
104 | "filterpool" => [
105 | 'name' => $filterPoolName,
106 | 'type' => 'Magento\Framework\View\Element\UiComponent\DataProvider\FilterPool',
107 | 'arguments' => [
108 | [
109 | 'name' => 'appliers',
110 | 'type' => 'array',
111 | 'value' => [
112 | 'regular' => [
113 | 'name' => 'regular',
114 | 'type' => 'object',
115 | 'value' => 'Magento\Framework\View\Element\UiComponent\DataProvider\RegularFilter'
116 | ],
117 | 'fulltext' => [
118 | 'name' => 'fulltext',
119 | 'type' => 'object',
120 | 'value' => 'Magento\Framework\View\Element\UiComponent\DataProvider\FulltextFilter'
121 | ]
122 | ]
123 | ]
124 | ]
125 | ],
126 | "collection" => [
127 | 'name' => $this->gridCollectionName,
128 | 'type' => 'Magento\Framework\View\Element\UiComponent\DataProvider\SearchResult',
129 | 'arguments' => [
130 | [
131 | 'name' => 'mainTable',
132 | 'type' => 'string',
133 | 'value' => $mainTable
134 | ],
135 | [
136 | 'name' => 'resourceModel',
137 | 'type' => 'string',
138 | 'value' => $this->getResource()::class
139 | ]
140 | ]
141 | ]
142 | ];
143 | $this->searchAndReplaceInDI($virtualTypes);
144 |
145 | }
146 |
147 | /**
148 | * Search and replace the virtual types in the di.xml file. If the virtual type already exists, we don't add it.
149 | * It also adds the grid collection name in the collection factory type.
150 | *
151 | * @param array $entries
152 | */
153 | private function searchAndReplaceInDI(array $entries): void
154 | {
155 | $path = $this->modulePath . '/etc/di.xml';
156 | $xmlContent = $this->ioFile->read($path);
157 | $additionalXml = '';
158 | $entriesToAdd = $this->filterEntries($entries, $xmlContent);
159 |
160 | foreach ($entriesToAdd as $entry) {
161 | $additionalXml .= ' ' . PHP_EOL;
162 | $additionalXml .= ' ' . PHP_EOL;
163 | foreach ($entry['arguments'] as $argument) {
164 | $additionalXml .= $this->getArgumentLine($argument);
165 | }
166 | $additionalXml .= ' ' . PHP_EOL;
167 | $additionalXml .= ' ' . PHP_EOL;
168 | }
169 |
170 | if (!empty($additionalXml)) {
171 | $xmlContent = str_replace('', $additionalXml . '', $xmlContent);
172 | }
173 |
174 | $xmlContent = $this->addOrUpdateCollectionFactory($xmlContent);
175 |
176 | $this->ioFile->write($path, $xmlContent);
177 | }
178 |
179 | /**
180 | * Filter the entries to add only the ones that don't already exist in the di.xml file.
181 | */
182 | private function filterEntries(array $entries, string $xmlContent): array
183 | {
184 | $regex = '/[^"]+)"\s+type="(?P[^"]+)")|(?:type="(?P[^"]+)"\s+name="(?P[^"]+)"))\s*>/i';
185 |
186 | $existingEntries = [];
187 | if (preg_match_all($regex, $xmlContent, $matches, PREG_SET_ORDER)) {
188 | foreach ($matches as $match) {
189 | $name = $match['name1'] ?: $match['name2'];
190 | $type = $match['type1'] ?: $match['type2'];
191 | $existingEntries["{$name}_{$type}"] = true;
192 | }
193 | }
194 |
195 | return array_filter($entries, function ($entry) use ($existingEntries) {
196 | $key = "{$entry['name']}_{$entry['type']}";
197 | return !isset($existingEntries[$key]);
198 | });
199 | }
200 |
201 | /**
202 | * If the type already exists, we add the new item to the arguments array. Else, we create the type.
203 | */
204 | private function addOrUpdateCollectionFactory(string $xmlContent): string
205 | {
206 | $typeRegex = '/.*?<\/type>/s';
207 | $itemRegex = '/- listingLayoutUiComponent, '/') . '_data_source[\'"]\s+xsi:type=[\'"]string[\'"]\s*>.*?<\/item>/';
208 |
209 | $newItem = '
- '
210 | . $this->gridCollectionName . '
' . PHP_EOL;
211 |
212 | // If type already exists, we add the new item to the arguments array
213 | if (preg_match($typeRegex, $xmlContent, $typeMatch)) {
214 | $typeContent = $typeMatch[0];
215 |
216 | // Check if the item already exists, if not, we add it
217 | if (!preg_match($itemRegex, $typeContent)) {
218 | $updatedContent = $this->insertItemIntoType($typeContent, $newItem);
219 | $xmlContent = str_replace($typeMatch[0], $updatedContent, $xmlContent);
220 | }
221 | } else {
222 | // If the type doesn't exist, we create it
223 | $newType = $this->generateNewType($newItem);
224 | $xmlContent = str_replace('', $newType . '', $xmlContent);
225 | }
226 |
227 | return $xmlContent;
228 | }
229 |
230 | /**
231 | * Insert the new item into the existing type.
232 | */
233 | private function insertItemIntoType(string $typeContent, string $newItem): string
234 | {
235 | return preg_replace(
236 | '/<\/argument><\/arguments>\s*<\/type>/',
237 | $newItem . '' . PHP_EOL . ' ' . PHP_EOL . ' ',
238 | $typeContent
239 | );
240 | }
241 |
242 | /**
243 | * Generate the new type for the collection factory with the new item in the arguments array.
244 | */
245 | private function generateNewType(string $newItem): string
246 | {
247 | return ' ' . PHP_EOL
248 | . ' ' . PHP_EOL
249 | . ' ' . PHP_EOL
250 | . $newItem
251 | . ' ' . PHP_EOL
252 | . ' ' . PHP_EOL
253 | . ' ' . PHP_EOL;
254 | }
255 |
256 |
257 | private function getArgumentLine(array $argument): string
258 | {
259 | if ($argument['type'] === 'array') {
260 | $value = '';
261 | foreach ($argument['value'] as $item) {
262 | $value .= '- ' . $item['value'] . '
' . PHP_EOL;
263 | }
264 | return ' ' . PHP_EOL . $value . ' ' . PHP_EOL;
265 | }
266 | return ' ' . $argument['value'] . '' . PHP_EOL;
267 | }
268 |
269 | private function addUiComponent()
270 | {
271 | $headTemplatePath = $this->reader->getModuleDir(null, Generator::OPENGENTO_MAKEGENTO_CLI)
272 | . '/Generator/templates/view/adminhtml/ui_component/listing.xml.tpl';
273 | $template = $this->ioFile->read($headTemplatePath);
274 |
275 | // Let's replace listing by columns in the ui component name to get the columns name
276 | $columnsName = str_replace('listing', 'columns', $this->listingLayoutUiComponent);
277 |
278 | $fieldsToUpdate = [
279 | '{{ui_component_name}}',
280 | '{{buttons}}',
281 | '{{columns}}',
282 | '{{primary_field_name}}',
283 | '{{data_provider}}',
284 | '{{columns_name}}'
285 | ];
286 |
287 | $fieldsReplacement = [
288 | $this->listingLayoutUiComponent,
289 | $this->getButtons(),
290 | $this->getColumns(),
291 | $this->resource->getIdFieldName(),
292 | $this->dataProviderName,
293 | $columnsName
294 | ];
295 |
296 | $newFileContent = str_replace(
297 | $fieldsToUpdate,
298 | $fieldsReplacement,
299 | $template
300 | );
301 |
302 | $newFilePath = $this->modulePath . '/view/adminhtml/ui_component/' . $this->listingLayoutUiComponent . '.xml';
303 | if (!$this->ioFile->fileExists($newFilePath, false)) {
304 | $this->ioFile->mkdir($this->modulePath . '/view/adminhtml/ui_component', 0755);
305 | }
306 | $this->ioFile->write($newFilePath, $newFileContent);
307 | }
308 |
309 | /**
310 | * Get the buttons for the ui component It checks for existence of form related controller and adds the buttons
311 | *
312 | * @return string
313 | */
314 | private function getButtons(): string
315 | {
316 | $buttons = '';
317 | $controllerFolder = $this->modulePath . '/Controller/Adminhtml/' . $this->entityName;
318 | if ($this->ioFile->fileExists($controllerFolder . '/Create.php', false)) {
319 | $buttons = '
320 | -
321 |
-
322 |
- add
323 | - Create new ' . strtolower($this->entityName) . '
324 | - primary
325 | - */' . $this->route . '/create
326 |
327 | ';
328 | }
329 | return $buttons;
330 | }
331 |
332 | /**
333 | * Get the columns for the ui component
334 | *
335 | * @return string
336 | * @throws LocalizedException
337 | */
338 | private function getColumns(): string
339 | {
340 | $mainTable = $this->getResource()->getMainTable();
341 | $tableColumns = $this->getResource()->getConnection()->describeTable($mainTable);
342 | $columns = '';
343 | foreach ($tableColumns as $column) {
344 | $dataType = $column['DATA_TYPE'];
345 | $class = '';
346 | $component = '';
347 | $columnType = 'text';
348 | if ($dataType === 'datetime' || $dataType === 'date') {
349 | $class = 'class="' . \Magento\Ui\Component\Listing\Columns\Date::class . '"';
350 | $component = 'component="Magento_Ui/js/grid/columns/date"';
351 | $columnType = 'date';
352 | }
353 | $columns .= '
354 |
355 | -
356 |
- text
357 | - ' . $column['COLUMN_NAME'] . '
358 | - ' . $columnType . '
359 |
360 |
361 | ' . PHP_EOL;
362 | }
363 | return $columns;
364 | }
365 |
366 | }
367 |
--------------------------------------------------------------------------------
/Generator/templates/Controller/controller.php.tpl:
--------------------------------------------------------------------------------
1 | resultPageFactory->create();
44 | $resultPage->setActiveMenu(self::ADMIN_RESOURCE);
45 | $resultPage->addBreadcrumb(__('Reports'), __('Reports'));
46 | $resultPage->addBreadcrumb(__('{{module_title}}'), __('{{module_title}}'));
47 | $resultPage->getConfig()->getTitle()->prepend(__('{{module_title}} Listing'));
48 |
49 | return $resultPage;
50 | }
51 | }
52 |
53 |
--------------------------------------------------------------------------------
/Generator/templates/Model/ResourceModel/Collection/grid.tpl.php:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/opengento/magento2-makegento-cli/237fa52665de5ff77f1c56cc02028700b8b17ed1/Generator/templates/Model/ResourceModel/Collection/grid.tpl.php
--------------------------------------------------------------------------------
/Generator/templates/Model/ResourceModel/resourcemodel.tpl.php:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/opengento/magento2-makegento-cli/237fa52665de5ff77f1c56cc02028700b8b17ed1/Generator/templates/Model/ResourceModel/resourcemodel.tpl.php
--------------------------------------------------------------------------------
/Generator/templates/Model/model.tpl.php:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/opengento/magento2-makegento-cli/237fa52665de5ff77f1c56cc02028700b8b17ed1/Generator/templates/Model/model.tpl.php
--------------------------------------------------------------------------------
/Generator/templates/composer.json.tpl:
--------------------------------------------------------------------------------
1 | {
2 | "name": "{{fullComposerStandardNewModuleName}}",
3 | "version": "100.0.0",
4 | "description": "N/A",
5 | "type": "magento2-module",
6 | "require": {
7 | "magento/framework": "*"
8 | },
9 | "license": [
10 | "Proprietary"
11 | ],
12 | "autoload": {
13 | "files": [
14 | "registration.php"
15 | ],
16 | "psr-4": {
17 | "{{vendorName}}\\{{moduleName}}\\": ""
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Generator/templates/etc/acl.xml.tpl:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/Generator/templates/etc/adminhtml/menu.xml.tpl:
--------------------------------------------------------------------------------
1 |
2 |
4 |
12 |
13 |
--------------------------------------------------------------------------------
/Generator/templates/etc/adminhtml/routes.xml.tpl:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/Generator/templates/etc/module.xml.tpl:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/Generator/templates/registration.php.tpl:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/Generator/templates/view/adminhtml/ui_component/listing.xml.tpl:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | -
5 |
- {{ui_component_name}}.{{ui_component_name}}_data_source
6 | - {{ui_component_name}}.{{ui_component_name}}_data_source
7 |
8 | - {{columns_name}}
9 | {{buttons}}
10 |
11 |
12 |
13 |
14 | {{data_provider}}
15 | {{ui_component_name}}_data_source
16 | {{primary_field_name}}
17 | id
18 |
19 |
20 | -
21 |
- Magento_Ui/js/grid/provider
22 |
23 | -
24 |
25 |
- {{primary_field_name}}
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 | true
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 | {{primary_field_name}}
47 |
48 |
49 | {{columns}}
50 |
51 |
52 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 OpenGento
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Maker/MakeConstraint.php:
--------------------------------------------------------------------------------
1 | currentModule->getModuleName();
34 | $tableName = $this->dataTableAutoCompletion->tableSelector($selectedModule);
35 | $dataTables = $this->dbSchemaParser->getModuleDataTables($selectedModule);
36 | $fields = $dataTables[$tableName]['fields'];
37 |
38 | $constraintDefinition = $this->foreignKeyDefinition->define($tableName, $fields);
39 |
40 | $existingConstraints = $dataTables[$tableName]['constraints'];
41 | $existingConstraints = array_merge($existingConstraints, $constraintDefinition);
42 | $dataTables[$tableName]['constraints'] = $existingConstraints;
43 |
44 | $this->dbSchemaCreator->createDbSchema($selectedModule, $dataTables);
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/Maker/MakeCrud.php:
--------------------------------------------------------------------------------
1 | generatorCrud->setEntityName($entityName);
38 |
39 | // Generate route
40 | $this->commandIoProvider->getOutput()->writeln('Creating routes.xml');
41 | $route = $this->generatorCrud->generateRoutes();
42 | // Generate acl
43 | $this->commandIoProvider->getOutput()->writeln('Creating acl.xml');
44 | $this->generatorCrud->generateAcl();
45 | // Generate controller
46 | $this->commandIoProvider->getOutput()->writeln('Creating adminhtml controller');
47 | $this->generatorController->setEntityName($entityName);
48 | try {
49 | $listingControllerRoute = $this->generatorController->generateListingController();
50 | } catch (ExistingClassException $e) {
51 | $listingControllerRoute = $e->getFilePath();
52 | }
53 | // Generate route
54 | $this->commandIoProvider->getOutput()->writeln('Creating menu.xml');
55 | $this->generatorCrud->generateMenuEntry($route, $listingControllerRoute);
56 |
57 | // Ask user if he wants to generate a form
58 | $generateForm = $this->yesNoQuestionPerformer->execute(
59 | ['Do you want to generate a form for this entity? [y/n]'],
60 | $this->commandIoProvider->getInput(),
61 | $this->commandIoProvider->getOutput()
62 | );
63 | if ($generateForm) {
64 | $formControllers = $this->generatorController->generateFormControllers();
65 | }
66 |
67 | // Generate layout
68 | $this->commandIoProvider->getOutput()->writeln('Creating layout');
69 | $listingLayoutUiComponent = $this->generatorCrud->generateListingLayout($route);
70 |
71 | // Generate ui components
72 | $this->commandIoProvider->getOutput()->writeln('Creating ui-component');
73 | $this->generatorUiComponent->generateListing($entityName, $listingLayoutUiComponent, $route);
74 |
75 | $this->commandIoProvider->getOutput()->writeln('Crud for entity ' . $entityName . ' has been created');
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/Maker/MakeEntity.php:
--------------------------------------------------------------------------------
1 | output = $this->commandIoProvider->getOutput();
49 | $this->input = $this->commandIoProvider->getInput();
50 | $selectedModule = $this->currentModule->getModuleName();
51 | try {
52 | $dataTables = $this->dbSchemaParser->getModuleDataTables($selectedModule);
53 | $this->output->writeln("Database schema already exists");
54 | $this->output->writeln("Database schema modification");
55 | foreach ($dataTables as $tableName => $table) {
56 | $this->dataTableAutoCompletion->addDataTable($tableName);
57 | $this->dataTableAutoCompletion->addTableFields($tableName, array_keys($table['fields']), $table['primary']);
58 | }
59 | } catch (TableDefinitionException $e) {
60 | $this->output->writeln("Database schema creation");
61 | $dataTables = [];
62 | }
63 | $addNewTable = true;
64 | while ($addNewTable) {
65 | try {
66 | $tableDefinition = $this->tableCreation();
67 | if (empty($tableDefinition)) {
68 | $addNewTable = false;
69 | } else {
70 | $dataTables = array_merge($dataTables, $tableDefinition);
71 | }
72 | } catch (InvalidArrayException $e) {
73 | $this->output->writeln("You did not create any field in last table, going to generation.");
74 | $addNewTable = false;
75 | }
76 | }
77 | if (!empty($dataTables)) {
78 | try {
79 | $this->dbSchemaCreator->createDbSchema($selectedModule, $dataTables);
80 | $this->output->writeln("Database schema created");
81 | } catch (TableDefinitionException $e) {
82 | $this->output->writeln("{$e->getMessage()}");
83 | }
84 | }
85 | }
86 |
87 | /**
88 | * This part will ask questions to user to be able to know general settings of the table. It will then call the
89 | * functions to create fields, indexes and constraints.
90 | *
91 | * @return array[]
92 | * @throws InvalidArrayException|\Opengento\MakegentoCli\Exception\CommandIoNotInitializedException
93 | *
94 | * @SuppressWarnings(PHPMD.CyclomaticComplexity)
95 | */
96 | private function tableCreation(): array
97 | {
98 | $tableName = $this->questionHelper->ask(
99 | $this->input,
100 | $this->output,
101 | new Question('Enter the datatable name leave empty to go to db_schema.xml generation: ' . PHP_EOL)
102 | );
103 | if ($tableName == '') {
104 | return [];
105 | }
106 | $engine = $this->questionHelper->ask(
107 | $this->input,
108 | $this->output,
109 | new ChoiceQuestion(
110 | 'Choose the table engine (default : innodb)',
111 | ['innodb', 'memory'],
112 | 'innodb'
113 | )
114 | );
115 | $resource = $this->questionHelper->ask(
116 | $this->input,
117 | $this->output,
118 | new ChoiceQuestion(
119 | 'Choose the table resource (default : default)',
120 | ['default', 'sales', 'checkout'],
121 | 'default'
122 | )
123 | );
124 | $comment = $this->questionHelper->ask(
125 | $this->input,
126 | $this->output,
127 | new Question('Enter the table comment (default : Table comment): ', 'Table comment')
128 | );
129 | $this->dataTableAutoCompletion->addDataTable($tableName);
130 | $tableFields = [];
131 | $addNewField = true;
132 | $primary = false;
133 | $indexes = [];
134 | $constraints = [];
135 | while ($addNewField) {
136 | try {
137 | $field = $this->field->create($primary, $tableName);
138 | } catch (ExistingFieldException $e) {
139 | $this->output->writeln("{$e->getMessage()}");
140 | }
141 | if (empty($field)) {
142 | $addNewField = false;
143 | } else {
144 | $tableFields = array_merge($tableFields, $field);
145 | }
146 | }
147 | if (empty($tableFields) || count($tableFields) === 1) {
148 | throw new InvalidArrayException('Table fields cannot be empty');
149 | }
150 | $addConstraint = true;
151 | while ($addConstraint) {
152 | try {
153 | $constraint = $this->constraintDefinition->define($tableName, $tableFields);
154 | if (empty($constraint)) {
155 | $addConstraint = false;
156 | } else {
157 | $constraints = array_merge($constraints, $constraint);
158 | }
159 | } catch (ConstraintDefinitionException $e) {
160 | $this->output->writeln("Previous constraint was not added due to : {$e->getMessage()}. Please try again!");
161 | }
162 | }
163 | $addIndex = true;
164 | while ($addIndex) {
165 | try {
166 | $index = $this->createIndex($tableFields);
167 | if (empty($index)) {
168 | $addIndex = false;
169 | } else {
170 | $indexes = array_merge($indexes, $index);
171 | }
172 | } catch (InvalidArrayException $e) {
173 | $addIndex = false;
174 | }
175 | }
176 | return [$tableName => [
177 | 'fields' => $tableFields,
178 | 'constraints' => $constraints,
179 | 'primary' => $primary,
180 | 'indexes' => $indexes,
181 | 'table_attr' => [
182 | 'engine' => $engine,
183 | 'resource' => $resource,
184 | 'comment' => $comment
185 | ]
186 | ]];
187 | }
188 |
189 |
190 | /**
191 | * This part will ask questions to user to be able to know what type of index he wants to create.
192 | *
193 | * @param $fields
194 | * @return array
195 | * @throws InvalidArrayException
196 | */
197 | private function createIndex($fields): array
198 | {
199 |
200 | $indexName = $this->questionHelper->ask(
201 | $this->input,
202 | $this->output,
203 | new Question('Enter the index name (leave empty to add a new table): ' . PHP_EOL)
204 | );
205 | if ($indexName == "") {
206 | return [];
207 | }
208 | $indexFields = [];
209 | $indexType = $this->questionHelper->ask($this->input, $this->output, new ChoiceQuestion(
210 | 'Choose the index type',
211 | ['btree', 'fulltext', 'hash']
212 | ));
213 | $addNewIndexField = true;
214 | while ($addNewIndexField) {
215 | $fieldSelection = new Question('Enter the index field (leave empty to stop adding field to this index): ' . PHP_EOL);
216 | $fieldSelection->setAutocompleterValues(array_keys($fields));
217 | $field = $this->questionHelper->ask($this->input, $this->output, $fieldSelection);
218 | if ($field == '') {
219 | $addNewIndexField = false;
220 | } else {
221 | $indexFields[] = $field;
222 | }
223 | }
224 | if (empty($indexFields)) {
225 | throw new InvalidArrayException('Index fields cannot be empty');
226 | }
227 | return [
228 | $indexName => [
229 | 'type' => $indexType,
230 | 'fields' => $indexFields
231 | ]
232 | ];
233 | }
234 | }
235 |
--------------------------------------------------------------------------------
/Maker/MakeField.php:
--------------------------------------------------------------------------------
1 | currentModule->getModuleName();
42 | $tableName = $this->dataTableAutoCompletion->tableSelector($selectedModule);
43 | $dataTables = $this->dbSchemaParser->getModuleDataTables($selectedModule);
44 | if (!isset($dataTables[$tableName])) {
45 | throw new TableDefinitionException("Table $tableName does not exist in the module $selectedModule");
46 | }
47 | $primary = $dataTables[$tableName]['primary'] ?? '';
48 |
49 | try {
50 | $field = $this->field->create($primary, $tableName);
51 | } catch (ExistingFieldException $e) {
52 | $this->commandIoProvider->getOutput()->writeln("{$e->getMessage()}");
53 | return;
54 | }
55 | $existingFields = $dataTables[$tableName]['fields'];
56 | $dataTables[$tableName]['fields'] = array_merge($existingFields, $field);
57 |
58 | $constraints = $this->constraintDefinition->define($tableName, $dataTables[$tableName]['fields']);
59 | $existingConstraints = $dataTables[$tableName]['constraints'];
60 | $dataTables[$tableName]['constraints'] = array_merge($existingConstraints, $constraints);
61 |
62 | $this->dbSchemaCreator->createDbSchema($selectedModule, $dataTables);
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/Maker/MakeModel.php:
--------------------------------------------------------------------------------
1 | currentModule->getModuleName();
42 | $input = $this->commandIoProvider->getInput();
43 | $output = $this->commandIoProvider->getOutput();
44 | $questionHelper = $this->commandIoProvider->getQuestionHelper();
45 |
46 | $output->writeln('Make Model');
47 |
48 | $output->writeln('Module: ' . $selectedModule);
49 |
50 | $tables = $this->dbSchemaParser->getModuleDataTables($selectedModule);
51 |
52 | $tableName = $this->dataTableAutoCompletion->tableSelector($selectedModule);
53 |
54 | $modelClassName = $this->defaultClassNameGetter->get($tableName, $selectedModule);
55 |
56 | $modelClassName = $questionHelper->ask($input, $output, new Question('Enter the class name [default : '. $modelClassName .']: ', $modelClassName));
57 |
58 | $modelSubFolder = ucfirst($questionHelper->ask($input, $output, new Question('Enter the subfolder [default : none]: ', '')));
59 |
60 | $properties = $tables[$tableName];
61 |
62 | $output->writeln('Start generating model for table ' . $tableName);
63 | try {
64 | $interface = $this->generatorModel->generateModelInterface($modelClassName, $properties['fields']);
65 | $output->writeln('Interface '. $interface . ' generated');
66 | } catch (ExistingClassException $e) {
67 | $output->writeln("{$e->getMessage()}");
68 | $interface = $e->getClassName();
69 | }
70 |
71 | try {
72 | $modelClass = $this->generatorModel->generateModel($modelClassName, $properties['fields'], $interface, $modelSubFolder);
73 | $output->writeln('Model '. $modelClass .' generated');
74 | } catch (ExistingClassException $e) {
75 | $output->writeln("{$e->getMessage()}");
76 | $modelClass = $e->getClassName();
77 | }
78 |
79 | try {
80 | $resourceClass = $this->generatorModel->generateResourceModel($modelClassName, $interface, $tableName, $modelSubFolder);
81 | $output->writeln('Resource Model '. $resourceClass .' generated');
82 | } catch (ExistingClassException $e) {
83 | $output->writeln("{$e->getMessage()}");
84 | $resourceClass = $e->getClassName();
85 | }
86 |
87 | try {
88 | $collectionClass = $this->generatorModel->generateCollection($modelClassName, $modelClass, $resourceClass, $modelSubFolder);
89 | $output->writeln('Collection '. $collectionClass .' generated');
90 | } catch (ExistingClassException $e) {
91 | $output->writeln("{$e->getMessage()}");
92 | $collectionClass = $e->getClassName();
93 | }
94 |
95 | try {
96 | $searchResultInterface = $this->generatorRepository->generateSearchCriteriaInterface($modelClassName);
97 | $output->writeln('Repository '. $searchResultInterface .' generated');
98 | } catch (ExistingClassException $e) {
99 | $output->writeln("{$e->getMessage()}");
100 | $searchResultInterface = $e->getClassName();
101 | }
102 |
103 | try {
104 | $repositoryInterface = $this->generatorRepository->generateRepositoryInterface($modelClassName, $searchResultInterface, $resourceClass, $collectionClass, $interface);
105 | $output->writeln('Repository '. $repositoryInterface .' generated');
106 | } catch (ExistingClassException $e) {
107 | $output->writeln("{$e->getMessage()}");
108 | $repositoryInterface = $e->getClassName();
109 | }
110 |
111 | try {
112 | $repositoryClass = $this->generatorRepository->generateRepository($modelClassName, $searchResultInterface, $resourceClass, $collectionClass, $interface, $repositoryInterface);
113 | $output->writeln('Repository '. $repositoryClass .' generated');
114 | } catch (ExistingClassException $e) {
115 | $output->writeln("{$e->getMessage()}");
116 | $repositoryClass = $e->getClassName();
117 | }
118 | $output->writeln('Generating preferences in di.xml file');
119 | $this->addPreferencesInDI($modelClass, $interface, $repositoryInterface, $repositoryClass);
120 |
121 | $output->writeln('Model generation completed');
122 |
123 | }
124 |
125 | private function addPreferencesInDI(
126 | string $modelClassName,
127 | string $modelInterface,
128 | string $repositoryInterface,
129 | string $repositoryClass,
130 | ): void
131 | {
132 | $modulePath = $this->currentModule->getModulePath();
133 | $diXmlPath = $modulePath . '/etc/di.xml';
134 | $diXmlContent = $this->ioFile->read($diXmlPath);
135 | // we look for "" in the di.xml file
136 | $pattern = '/ $interface) {
141 | $existingPreferences[$interface] = $matches[2][$index];
142 | }
143 | }
144 | $newPreferences = [];
145 | if (!isset($existingPreferences[$modelInterface])) {
146 | $newPreferences[$modelInterface] = $modelClassName;
147 | }
148 | if (!isset($existingPreferences[$repositoryInterface])) {
149 | $newPreferences[$repositoryInterface] = $repositoryClass;
150 | }
151 | $newPreferencesString = '';
152 | foreach ($newPreferences as $interface => $type) {
153 | $newPreferencesString .= "\n";
154 | }
155 | // we add the new preferences in the di.xml file
156 | $diXmlContent = str_replace('', $newPreferencesString . "\n", $diXmlContent);
157 | $this->ioFile->write($diXmlPath, $diXmlContent);
158 | }
159 | }
160 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # MakegentoCli module
2 |
3 | [](https://packagist.org/packages/opengento/module-makegento-cli)
4 | [](./LICENSE)
5 | [](https://packagist.org/packages/opengento/module-makegento-cli/stats)
6 | [](https://packagist.org/packages/opengento/module-makegento-cli/stats)
7 |
8 | This extension allows to automatically generate boilerplate code through the command line interface.
9 |
10 | - [Setup](#setup)
11 | - [Composer installation](#composer-installation)
12 | - [Setup the module](#setup-the-module)
13 | - [Features](#features)
14 | - [Support](#support)
15 | - [Authors](#authors)
16 | - [License](#license)
17 |
18 | ## Setup
19 |
20 | Magento 2 Open Source or Commerce edition is required.
21 |
22 | ### Composer installation
23 |
24 | Run the following composer command:
25 |
26 | ```
27 | composer require opengento/magento2-makegento-cli
28 | ```
29 |
30 | ### Setup the module
31 |
32 | Run the following magento command:
33 |
34 | ```
35 | bin/magento setup:upgrade
36 | ```
37 |
38 | **If you are in production mode, do not forget to recompile and redeploy the static resources.**
39 |
40 | ## Features
41 |
42 | - Generate CRUD entity with all required files ( routes, controllers, models,acl,grids,forms etc. )
43 |
44 | You can find all the related commands by running the following command:
45 |
46 | ```
47 | bin/magento list makegento
48 | ```
49 |
50 | ### Commands documentation
51 | [Make entity docs](docs/make-entity.md)
52 |
53 | ## Support
54 |
55 | Raise a new [request](https://github.com/opengento/magento2-makegento-cli/issues) to the issue tracker.
56 |
57 | ## Authors
58 |
59 | - **Opengento Community** - *Lead* - [](https://twitter.com/opengento)
60 | - **Contributors** - *Contributor* - [](https://github.com/opengento/magento2-makegento-cli/graphs/contributors)
61 |
62 |
63 |
64 |
65 |
--------------------------------------------------------------------------------
/Service/CommandIoProvider.php:
--------------------------------------------------------------------------------
1 | input = $input;
24 | $this->output = $output;
25 | $this->questionHelper = $questionHelper;
26 | }
27 |
28 | /**
29 | * @throws CommandIoNotInitializedException
30 | */
31 | public function getInput(): InputInterface
32 | {
33 | if (null === $this->input) {
34 | throw new CommandIoNotInitializedException('InputInterface is not initialized');
35 | }
36 | return $this->input;
37 | }
38 |
39 | /**
40 | * @throws CommandIoNotInitializedException
41 | */
42 | public function getOutput(): OutputInterface
43 | {
44 | if (null === $this->output) {
45 | throw new CommandIoNotInitializedException('OutputInterface is not initialized');
46 | }
47 | return $this->output;
48 | }
49 |
50 | /**
51 | * @throws CommandIoNotInitializedException
52 | */
53 | public function getQuestionHelper(): QuestionHelper
54 | {
55 | if (null === $this->questionHelper) {
56 | throw new CommandIoNotInitializedException('QuestionHelper is not initialized');
57 | }
58 | return $this->questionHelper;
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/Service/CurrentModule.php:
--------------------------------------------------------------------------------
1 | moduleName = $selectedModule;
37 | $this->modulePath = $modulePath;
38 | $this->moduleNameSnaked = $this->stringTransformationTools->getSnakeCase(explode('_', $selectedModule)[1]);
39 | }
40 |
41 | public function getModuleName(): string
42 | {
43 | return $this->moduleName;
44 | }
45 |
46 | public function getModuleVendor(): string
47 | {
48 | return explode('_', $this->moduleName)[0];
49 | }
50 |
51 | public function getModuleNameWithoutVendor(): string
52 | {
53 | return explode('_', $this->moduleName)[1];
54 | }
55 |
56 | public function getModulePath(): string
57 | {
58 | return $this->modulePath;
59 | }
60 |
61 | public function getModuleNamespace(string $path = ''): string
62 | {
63 | if ($this->defaultNamespace === '') {
64 | $this->defaultNamespace = $this->namespaceGetter->getNamespaceFromPath($this->modulePath, $this->moduleName);
65 | }
66 | return $this->namespaceGetter->getNamespace($this->modulePath, $path, $this->moduleName, $this->defaultNamespace);
67 | }
68 |
69 | public function getModuleNameSnakeCase(): string
70 | {
71 | return $this->moduleNameSnaked;
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/Service/Database/ConstraintDefinition.php:
--------------------------------------------------------------------------------
1 | commandIoProvider->getInput();
52 | $output = $this->commandIoProvider->getOutput();
53 | $questionHelper = $this->commandIoProvider->getQuestionHelper();
54 | if (!$this->yesNoQuestionPerformer->execute(
55 | ['Do you want to add constraint? [y/n]'],
56 | $input,
57 | $output
58 | )) {
59 | return [];
60 | }
61 | $constraintType = $questionHelper->ask($input, $output, new ChoiceQuestion(
62 | 'Choose the constraint type',
63 | ['unique', 'foreign']
64 | ));
65 | $constraintDefinition = ['type' => $constraintType];
66 | if ($constraintType === 'foreign') {
67 | $output->writeln("Note that referenceId will be automatically generated to fit with standards");
68 | $constraintDefinition['table'] = $tableName;
69 |
70 | $fieldSelection = new Question('Choose the column: ' . PHP_EOL);
71 | $fieldSelection->setAutocompleterValues(array_keys($fields));
72 | $constraintDefinition['column'] = $questionHelper->ask($input, $output, $fieldSelection);
73 |
74 | $tableQuestion = new Question('Choose the reference table begin typing to start autocompletion: ' . PHP_EOL);
75 | $tableQuestion->setAutocompleterValues($this->dataTableAutoCompletion->getAllTables());
76 | $constraintDefinition['referenceTable'] = $questionHelper->ask($input, $output, $tableQuestion);
77 |
78 | $tableFields = $this->dataTableAutoCompletion->getTableFields($constraintDefinition['referenceTable']);
79 | $referenceFieldSelection = new Question('Choose the reference field begin typing to start autocompletion: ' . PHP_EOL, $tableFields['identity']);
80 | $referenceFieldSelection->setAutocompleterValues($tableFields['fields']);
81 | $constraintDefinition['referenceColumn'] = $questionHelper->ask($input, $output, $referenceFieldSelection);
82 |
83 | $constraintDefinition['onDelete'] = $questionHelper->ask($input, $output, new ChoiceQuestion(
84 | 'Choose the onDelete action (default : CASCADE)',
85 | ['CASCADE', 'RESTRICT', 'SET NULL', 'NO ACTION'],
86 | 'CASCADE'
87 | ));
88 | $constraintName = $this->getName($constraintDefinition);
89 | } else {
90 | $constraintName = $questionHelper->ask(
91 | $input,
92 | $output,
93 | new Question('Enter the constraint name (leave empty to advance to indexes): ' . PHP_EOL)
94 | );
95 | $columns = [];
96 | $addColumn = true;
97 | while ($addColumn) {
98 | $fieldSelection = new Question('Choose the column (leave empty to stop adding columns to this constraint): ' . PHP_EOL);
99 | $fieldSelection->setAutocompleterValues(array_keys($fields));
100 | $column = $questionHelper->ask($input, $output, $fieldSelection);
101 | if ($column == '') {
102 | $addColumn = false;
103 | } else {
104 | $columns[] = $column;
105 | }
106 | }
107 | if (empty($columns)) {
108 | throw new ConstraintDefinitionException(__('Columns cannot be empty for unique constraint'));
109 | }
110 | $constraintDefinition['columns'] = $columns;
111 | }
112 | if (empty($constraintDefinition['type'])) {
113 | throw new ConstraintDefinitionException(__('Constraint definition must have a type'));
114 | }
115 | return [$constraintName => $constraintDefinition];
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/Service/Database/DataTableAutoCompletion.php:
--------------------------------------------------------------------------------
1 | dataTableInitialized) {
37 | $connection = $this->resourceConnection->getConnection();
38 | $tables = $connection->getTables();
39 | foreach ($tables as $table) {
40 | $this->dataTables[] = $table;
41 | }
42 | }
43 | return $this->dataTables;
44 | }
45 |
46 | /**
47 | * @param string $tableName
48 | * @return void
49 | */
50 | public function addDataTable(string $tableName): void
51 | {
52 | $this->dataTables[] = $tableName;
53 | }
54 |
55 | /**
56 | * Permits to select a table from the data tables of a module.
57 | *
58 | * @param string $selectedModule
59 | * @return mixed
60 | * @throws TableDefinitionException|CommandIoNotInitializedException
61 | */
62 | public function tableSelector(string $selectedModule): mixed
63 | {
64 | $dataTables = $this->dbSchemaParser->getModuleDataTables($selectedModule);
65 |
66 | $tableSelection = new Question('Choose the table: ' . PHP_EOL);
67 | $tableSelection->setAutocompleterValues(array_keys($dataTables));
68 | $this->commandIoProvider->getOutput()->writeln('Table in db_schema.xml');
69 | foreach (array_keys($dataTables) as $entity) {
70 | $this->commandIoProvider->getOutput()->writeln($entity);
71 | }
72 | return $this->commandIoProvider->getQuestionHelper()->ask($this->commandIoProvider->getInput(), $this->commandIoProvider->getOutput(), $tableSelection);
73 | }
74 |
75 | /**
76 | * Returns all the fields of a table to be able to manage autocomplete for foreign key reference field.
77 | *
78 | * @param string $tableName
79 | * @return array
80 | */
81 | public function getTableFields(string $tableName): array
82 | {
83 | if (!isset($this->tableFields[$tableName])) {
84 | $connection = $this->resourceConnection->getConnection();
85 | $table = $connection->describeTable($tableName);
86 | $fields = [];
87 | $primaryKey = '';
88 | foreach ($table as $name => $field) {
89 | if ($field['PRIMARY'] === true) {
90 | $primaryKey = $name;
91 | }
92 | $fields[] = $name;
93 | }
94 | $this->addTableFields($tableName, $fields, $primaryKey);
95 | }
96 | return $this->tableFields[$tableName];
97 | }
98 |
99 | /**
100 | * Adds a table to table fields array
101 | * @param string $tableName
102 | * @param array $fields
103 | * @param string $primaryKey
104 | */
105 | public function addTableFields(string $tableName, array $fields, string $primaryKey): void
106 | {
107 | $this->tableFields[$tableName] = [
108 | 'fields' => $fields,
109 | 'identity' => $primaryKey
110 | ];
111 | }
112 |
113 | public function addFieldToTable(string $tableName, string $fieldName, $isPrimary = false): void
114 | {
115 | if (!isset($this->tableFields[$tableName])) {
116 | $this->tableFields[$tableName] = [
117 | 'fields' => [],
118 | 'identity' => ''
119 | ];
120 | }
121 | $this->tableFields[$tableName]['fields'][] = $fieldName;
122 | if ($isPrimary) {
123 | $this->tableFields[$tableName]['identity'] = $fieldName;
124 | }
125 | }
126 | }
127 |
--------------------------------------------------------------------------------
/Service/Database/DbSchemaCreator.php:
--------------------------------------------------------------------------------
1 | ');
29 | foreach ($dataTables as $tableName => $tableDefinition) {
30 | $this->checkTableDefinition($tableDefinition);
31 | $table = $xml->addChild('table');
32 | $table->addAttribute('name', $tableName);
33 | foreach ($tableDefinition['table_attr'] as $attrName => $attrValue) {
34 | $table->addAttribute($attrName, $attrValue);
35 | }
36 |
37 | $this->addFields($table, $tableDefinition['fields']);
38 |
39 | $this->addIndexes($table, $tableDefinition['indexes']);
40 |
41 | if (isset($tableDefinition['primary'])) {
42 | /** @todo manage many to many */
43 | $primary = $table->addChild('constraint');
44 | $primary->addAttribute('xsi:type', 'primary', 'http://www.w3.org/2001/XMLSchema-instance');
45 | $primary->addAttribute('referenceId', 'PRIMARY');
46 | $primary->addChild('column')->addAttribute('name', $tableDefinition['primary']);
47 | }
48 |
49 | $this->addConstraints($table, $tableDefinition['constraints']);
50 | }
51 | $modulePath = $this->dbSchemaPath->get($selectedModule);
52 | $xml->asXML($modulePath);
53 | }
54 |
55 | /**
56 | * @param \SimpleXMLElement $table
57 | * @param array $fields
58 | * @return void
59 | */
60 | private function addFields(\SimpleXMLElement &$table, array $fields): void
61 | {
62 | foreach ($fields as $fieldName => $fieldAttributes) {
63 | $column = $table->addChild('column');
64 | $column->addAttribute('name', $fieldName);
65 | foreach ($fieldAttributes as $attrName => $attrValue) {
66 | if ($attrName === 'type') {
67 | $column->addAttribute('xsi:type', $attrValue, 'http://www.w3.org/2001/XMLSchema-instance');
68 | continue;
69 | }
70 | $column->addAttribute($attrName, $attrValue);
71 | }
72 | }
73 | }
74 |
75 | private function addIndexes(\SimpleXMLElement &$table, array $indexes): void
76 | {
77 | foreach ($indexes as $indexName => $indexDefinition) {
78 | $index = $table->addChild('index');
79 | $index->addAttribute('referenceId', $indexName);
80 | $index->addAttribute('indexType', $indexDefinition['type']);
81 | foreach ($indexDefinition['fields'] as $fieldName) {
82 | $index->addChild('column')->addAttribute('name', $fieldName);
83 | }
84 | }
85 | }
86 |
87 | /**
88 | * Adds the constraints to the table node.
89 | *
90 | * @param \SimpleXMLElement $table
91 | * @param array $constraints
92 | * @return void
93 | */
94 | private function addConstraints(\SimpleXMLElement &$table, array $constraints): void
95 | {
96 | foreach ($constraints as $constraintName => $constraintAttributes) {
97 | $constraint = $table->addChild('constraint');
98 | $constraint->addAttribute('xsi:type', $constraintAttributes['type'], 'http://www.w3.org/2001/XMLSchema-instance');
99 | $constraint->addAttribute('referenceId', $constraintName);
100 | if ($constraintAttributes['type'] === 'foreign') {
101 | $constraint->addAttribute('table', $constraintAttributes['table']);
102 | $constraint->addAttribute('column', $constraintAttributes['column']);
103 | $constraint->addAttribute('referenceTable', $constraintAttributes['referenceTable']);
104 | $constraint->addAttribute('referenceColumn', $constraintAttributes['referenceColumn']);
105 | $constraint->addAttribute('onDelete', $constraintAttributes['onDelete']);
106 | } else {
107 | foreach ($constraintAttributes['columns'] as $column) {
108 | $constraint->addChild('column')->addAttribute('name', $column);
109 | }
110 | }
111 | }
112 | }
113 |
114 | /**
115 | * Makes some checks before creating the db_schema.xml file.
116 | *
117 | * @param array $tableDefinition
118 | * @return void
119 | * @throws TableDefinitionException
120 | */
121 | private function checkTableDefinition(array $tableDefinition): void
122 | {
123 | if (empty($tableDefinition['fields']) || !is_array($tableDefinition['fields'])) {
124 | throw new TableDefinitionException('fields is a required key in the table definition');
125 | }
126 | if (!isset($tableDefinition['indexes']) || !is_array($tableDefinition['indexes'])) {
127 | throw new TableDefinitionException('indexes is a required key in the table definition and it must be of type array');
128 | }
129 | if (!isset($tableDefinition['constraints']) || !is_array($tableDefinition['constraints'])) {
130 | throw new TableDefinitionException('constraints is a required key in the table definition and it must be of type array');
131 | }
132 | }
133 | }
134 |
--------------------------------------------------------------------------------
/Service/Database/DbSchemaParser.php:
--------------------------------------------------------------------------------
1 | dataTables[$selectedModule])) {
29 | $this->dataTables[$selectedModule] = $this->parseDbSchema($selectedModule);
30 | }
31 | return $this->dataTables[$selectedModule];
32 | }
33 |
34 | /**
35 | * Parses the db_schema.xml file of a module and returns an array with the tables, fields, constraints and indexes.
36 | *
37 | * @param string $selectedModule
38 | * @return array
39 | * @throws TableDefinitionException
40 | */
41 | private function parseDbSchema(string $selectedModule): array
42 | {
43 | $dbSchemaPath = $this->dbSchemaPath->get($selectedModule);
44 | $xml = simplexml_load_file($dbSchemaPath);
45 | /**
46 | * let's try to parse the xml file to find table names, their fields and attributes of the fields
47 | */
48 | $tables = [];
49 | foreach ($xml->table as $table) {
50 | $primary = null;
51 | $tableName = (string)$table['name'];
52 | $tableAttributes = $this->manageAttributes($table);
53 | $columns = [];
54 | foreach ($table->column as $column) {
55 | $columnName = (string)$column['name'];
56 | $type = (string)$column->attributes('xsi', true)['type'];
57 | $columns[$columnName] = $this->manageAttributes($column);
58 | $columns[$columnName]['type'] = $type;
59 | }
60 | $domPrimary = $table->xpath('constraint[@xsi:type="primary"]');
61 | if ($domPrimary) {
62 | $primary = (string)$domPrimary[0]->column['name'];
63 | }
64 | $constraints = $this->parseConstraints($table);
65 |
66 | $indexes = $this->parseIndexes($table);
67 |
68 | $tables[$tableName] =
69 | [
70 | 'fields' => $columns,
71 | 'constraints' => $constraints,
72 | 'primary' => $primary,
73 | 'indexes' => $indexes,
74 | 'table_attr' => $tableAttributes
75 | ];
76 | }
77 | return $tables;
78 | }
79 |
80 | /**
81 | * @param \SimpleXMLElement $node
82 | * @return array
83 | */
84 | private function manageAttributes(\SimpleXMLElement $node): array
85 | {
86 | $attributes = [];
87 | foreach ($node->attributes() as $attrName => $attrValue) {
88 | if ($attrName === 'name') {
89 | continue;
90 | }
91 | $attributes[$attrName] = (string)$attrValue;
92 | }
93 | return $attributes;
94 | }
95 |
96 | /**
97 | * Parses the constraints of a table.
98 | *
99 | * @param \SimpleXMLElement $table
100 | * @return array
101 | */
102 | private function parseConstraints(\SimpleXMLElement $table): array
103 | {
104 | if (!isset($table->constraint)) {
105 | return [];
106 | }
107 | $constraints = [];
108 | foreach ($table->constraint as $constraint) {
109 | /** @var \SimpleXMLElement $constraint */
110 | $constraintName = (string)$constraint['name'];
111 | $type = (string)$constraint->attributes('xsi', true)['type'];
112 | $constraintAttributes = [
113 | 'type' => $type
114 | ];
115 | foreach ($constraint->attributes() as $attrName => $attrValue) {
116 | if ($attrName === 'referenceId' && (string)$attrValue === 'PRIMARY') {
117 | continue 2;
118 | }
119 | $constraintAttributes[$attrName] = (string)$attrValue;
120 | }
121 | foreach ($constraint->column ?? [] as $column) {
122 | if (!isset($constraintAttributes['columns'])) {
123 | $constraintAttributes['columns'] = [];
124 | }
125 | $constraintAttributes['columns'][] = $column;
126 | }
127 | $constraints[$constraintName] = $constraintAttributes;
128 | }
129 | return $constraints;
130 | }
131 |
132 | /**
133 | * @param \SimpleXMLElement $table
134 | * @return array
135 | */
136 | private function parseIndexes(\SimpleXMLElement $table): array
137 | {
138 | if (!isset($table->index)) {
139 | return [];
140 | }
141 | $indexes = [];
142 | foreach ($table->index as $index) {
143 | $indexName = (string)$index['referenceId'];
144 | $indexType = (string)$index['indexType'];
145 | $fields = [];
146 | foreach ($index->column as $field) {
147 | $fields[] = (string)$field['name'];
148 | }
149 | $indexes[$indexName] = [
150 | 'type' => $indexType,
151 | 'fields' => $fields
152 | ];
153 | }
154 | return $indexes;
155 | }
156 | }
157 |
--------------------------------------------------------------------------------
/Service/Database/DbSchemaPath.php:
--------------------------------------------------------------------------------
1 | currentModule->getModulePath();
30 | if (!$this->filesystem->getDirectoryReadByPath($modulePath)->isExist('etc/db_schema.xml')) {
31 | throw new TableDefinitionException(__('No db_schema.xml found in module ' . $selectedModule));
32 | }
33 | return $modulePath . '/etc/db_schema.xml';
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/Service/Database/Field.php:
--------------------------------------------------------------------------------
1 |
16 | */
17 | class Field
18 | {
19 | /**
20 | * Array of field types with their properties. The value of the property is the default value. For unsigned fields,
21 | * the default value is an empty string because question is yes/no.
22 | *
23 | * @var array
24 | */
25 | private array $fieldTypes = [
26 | 'int' => [
27 | 'padding' => 6,
28 | 'unsigned' => ''
29 | ],
30 | 'smallint' => [
31 | 'padding' => 6,
32 | 'unsigned' => ''
33 | ],
34 | 'varchar' => [
35 | 'length' => 255
36 | ],
37 | 'boolean' => [],
38 | 'date' => [],
39 | 'datetime' => [],
40 | 'timestamp' => [],
41 | 'float' => [
42 | 'precision' => 10,
43 | 'scale' => 2,
44 | 'unsigned' => ''
45 | ],
46 | 'blob' => [],
47 | 'decimal' => [
48 | 'precision' => 10,
49 | 'scale' => 2,
50 | 'unsigned' => ''
51 | ],
52 | 'json' => [],
53 | 'real' => [
54 | 'precision' => 10,
55 | 'scale' => 2,
56 | 'unsigned' => ''
57 | ],
58 | 'text' => [],
59 | 'varbinary' => [
60 | 'length' => 255
61 | ],
62 | ];
63 | private \Symfony\Component\Console\Helper\QuestionHelper $questionHelper;
64 | private OutputInterface $output;
65 | private InputInterface $input;
66 |
67 | public function __construct(
68 | private readonly YesNo $yesNoQuestionPerformer,
69 | private readonly DataTableAutoCompletion $dataTableAutoCompletion,
70 | private readonly CommandIoProvider $commandIoProvider,
71 | )
72 | {
73 | }
74 |
75 | /**
76 | * This part will ask questions to user to be able to know what type of field he wants to create
77 | *
78 | * @param $primary
79 | * @param string $tableName
80 | * @return array
81 | * @throws ExistingFieldException
82 | * @throws CommandIoNotInitializedException
83 | */
84 | public function create(&$primary, string $tableName = ''): array
85 | {
86 |
87 | $this->output = $this->commandIoProvider->getOutput();
88 | $this->input = $this->commandIoProvider->getInput();
89 | $this->questionHelper = $this->commandIoProvider->getQuestionHelper();
90 | if (!$primary) {
91 | return $this->createPrimary($primary, $tableName);
92 | }
93 | $fieldName = $this->questionHelper->ask(
94 | $this->input,
95 | $this->output,
96 | new Question('Enter the field name (leave empty to advance to constraints): ' . PHP_EOL)
97 | );
98 | if ($fieldName == '') {
99 | return [];
100 | }
101 | $existingFields = $this->dataTableAutoCompletion->getTableFields($tableName);
102 | if (in_array($fieldName, $existingFields['fields'])) {
103 | throw new ExistingFieldException('Field '. $fieldName .' already exists');
104 | }
105 | $fieldTypeQuestion = new Question(
106 | 'Choose the field type (' . implode('|', array_keys($this->fieldTypes)) . '): ' . PHP_EOL,
107 | 'varchar'
108 | );
109 | $fieldTypeQuestion->setAutocompleterValues(array_keys($this->fieldTypes));
110 | $fieldType = $this->questionHelper->ask($this->input, $this->output, $fieldTypeQuestion);
111 | $fieldDefinition['type'] = $fieldType;
112 | /**
113 | * Ask question for all the attributes of the field type. We exclude the unsigned attribute because it's managed
114 | * by a yes/no question.
115 | */
116 | foreach ($this->fieldTypes[$fieldType] as $attribute => $defaultValue) {
117 | if ($attribute === 'unsigned') {
118 | continue;
119 | }
120 | $fieldDefinition[$attribute] = $this->getFieldSpecificDefinition($attribute, $fieldType, $this->input, $this->output);
121 | }
122 | if (isset($this->fieldTypes[$fieldType]['unsigned'])) {
123 | $fieldDefinition['unsigned'] = $this->yesNoQuestionPerformer->execute(
124 | ['Is this field unsigned? [y/n]'],
125 | $this->input,
126 | $this->output
127 | ) ? "true" : "false";
128 | }
129 | /** @todo manage many to many relations */
130 | $fieldDefinition['nullable'] = $this->yesNoQuestionPerformer->execute(
131 | ['Is this field nullable? [y/n]'],
132 | $this->input,
133 | $this->output
134 | ) ? "true" : "false";
135 | if ($fieldDefinition['nullable'] === 'false') {
136 | $defaultValue = $this->yesNoQuestionPerformer->execute(
137 | ['Do you want to set a default value for this field? [y/n]'],
138 | $this->input,
139 | $this->output
140 | );
141 | if ($defaultValue) {
142 | $hasDefault = null;
143 | if ($fieldType === 'datetime' || $fieldType === 'timestamp' || $fieldType === 'date') {
144 | $hasDefault = 'CURRENT_TIMESTAMP';
145 | }
146 | $questionString = 'Enter the default value ';
147 | $questionString .= $hasDefault ? '(default : ' . $hasDefault . '): ' : ': ';
148 | $defaultValueQuestion = new Question($questionString, $hasDefault);
149 | $defaultValue = $this->questionHelper->ask($this->input, $this->output, $defaultValueQuestion);
150 | $fieldDefinition['default'] = $defaultValue;
151 | }
152 | }
153 | $this->dataTableAutoCompletion->addFieldToTable($tableName, $fieldName);
154 |
155 | return [$fieldName => $fieldDefinition];
156 | }
157 |
158 | /**
159 | * This part will ask questions to user to be able to know what type of field needs to be created
160 | *
161 | * @param string $attribute
162 | * @param string $fieldType
163 | * @return string
164 | */
165 | private function getFieldSpecificDefinition(string $attribute, string $fieldType): string
166 | {
167 | $defaultAnswer = $this->fieldTypes[$fieldType][$attribute];
168 | return $this->questionHelper->ask(
169 | $this->input,
170 | $this->output,
171 | new Question('Enter the field ' . $attribute .' (default : '.$defaultAnswer.'): ', $defaultAnswer)
172 | );
173 | }
174 |
175 | /**
176 | * This part will ask questions to user to create the primary key
177 | *
178 | * @param $primary
179 | * @param string $tableName
180 | * @return array
181 | * @throws CommandIoNotInitializedException
182 | */
183 | public function createPrimary(&$primary, string $tableName = '')
184 | {
185 | $this->output = $this->commandIoProvider->getOutput();
186 | $this->input = $this->commandIoProvider->getInput();
187 | $this->questionHelper = $this->commandIoProvider->getQuestionHelper();
188 | $fieldName = $this->questionHelper->ask(
189 | $this->input,
190 | $this->output,
191 | new Question(
192 | 'Please define a primary key default : ' . $tableName . '_id: ' . PHP_EOL,
193 | $tableName . '_id'
194 | )
195 | );
196 | $primary = $fieldName;
197 | $defaultPrimary = $this->yesNoQuestionPerformer->execute(
198 | ['Do you accept this definition int(5)?'],
199 | $this->input,
200 | $this->output
201 | );
202 | $padding = 5;
203 | if (!$defaultPrimary) {
204 | $padding = $this->questionHelper->ask(
205 | $this->input,
206 | $this->output,
207 | new Question('Enter the field padding (length): ', 6)
208 | );
209 | }
210 | $this->dataTableAutoCompletion->addFieldToTable($tableName, $fieldName, true);
211 | return [$fieldName =>
212 | [
213 | 'padding' => $padding,
214 | 'type' => 'int',
215 | 'unsigned' => "true",
216 | 'nullable' => "false",
217 | 'identity' => "true"
218 | ]
219 | ];
220 | }
221 | }
222 |
--------------------------------------------------------------------------------
/Service/Php/ClassGenerator.php:
--------------------------------------------------------------------------------
1 | 'int',
9 | 'smallint' => 'int',
10 | 'varchar' => 'string',
11 | 'boolean' => 'bool',
12 | 'date' => 'string',
13 | 'datetime' => 'string',
14 | 'timestamp' => 'string',
15 | 'float' => 'float',
16 | 'blob' => 'string',
17 | 'decimal' => 'float',
18 | 'json' => 'array',
19 | 'real' => 'float',
20 | 'text' => 'string',
21 | 'varbinary' => 'string',
22 | ];
23 |
24 | /**
25 | * Generates a PHP class.
26 | * Imports have been avoided to keep references safe.
27 | *
28 | * @param string $className
29 | * @param string|null $namespace
30 | * @param array $constants
31 | * @param array $properties
32 | * @param array $methods
33 | * @param string|null $extends
34 | * @param array|null $implements
35 | * @return string
36 | */
37 | public function generate(
38 | string $className,
39 | string $namespace = null,
40 | array $constants = [],
41 | array $properties = [],
42 | array $methods = [],
43 | string $extends = null,
44 | array $implements = null
45 | ): string
46 | {
47 | $namespacePart = $namespace ? "namespace $namespace;\n\n" : "";
48 |
49 | $extendsImplementPart = "";
50 | if ($extends) {
51 | $extendsImplementPart .= " extends $extends";
52 | }
53 |
54 | if ($implements) {
55 | $implements = implode(', ', $implements);
56 | $extendsImplementPart .= " implements $implements";
57 | }
58 |
59 |
60 | $propertiesPart = "";
61 |
62 | if ($extends === '\Magento\Framework\Model\AbstractModel') {
63 | $propertiesPart .= " protected \$_idFieldName = self::ID;\n\n";
64 | }
65 |
66 | $constantsPart = "";
67 |
68 | foreach ($constants as $constName => $name) {
69 | $constantsPart .= " const $constName = '$name';\n";
70 | }
71 |
72 | foreach ($properties as $property => $definition) {
73 | $type = $definition['type'];
74 | $phpType = $this->getPhpTypeFromEntityType($type);
75 | $propertiesPart .= " private $phpType \$$property;\n";
76 | }
77 |
78 | $methodsPart = "";
79 | foreach ($methods as $methodName => $method) {
80 | $methodBody = $method['body'];
81 | if (is_array($methodBody)) {
82 | $methodBody = implode("\n", $methodBody);
83 | }
84 | $methodVisibility = $method['visibility'];
85 | $methodArguments = $method['arguments'] ?? [];
86 | $separator = count($methodArguments) > 3 ? ",\n " : ', ';
87 | $methodArgumentsString = implode($separator, $methodArguments);
88 | if (count($methodArguments) > 3) {
89 | $methodArgumentsString = "\n " . $methodArgumentsString . "\n ";
90 | }
91 | $methodsPart .= " $methodVisibility function $methodName($methodArgumentsString) {\n";
92 | $methodsPart .= " $methodBody\n";
93 | $methodsPart .= " }\n\n";
94 | }
95 |
96 | return "typeMapping[$entityType];
113 | }
114 | }
115 |
--------------------------------------------------------------------------------
/Service/Php/DefaultClassNameGetter.php:
--------------------------------------------------------------------------------
1 | $name) {
29 | $propertiesPart .= " const $constName = '$name';\n";
30 | }
31 |
32 | $methodsPart = "";
33 | foreach ($methods as $methodName => $method) {
34 | if ($methodName === '__construct') {
35 | continue;
36 | }
37 | $methodVisibility = $method['visibility'];
38 | $methodArguments = isset($method['arguments']) ? implode(', ', $method['arguments']) : '';
39 | $methodsPart .= " $methodVisibility function $methodName($methodArguments);\n";
40 | }
41 |
42 | return "getNamespaceFromPath($modulePath, $selectedModule);
30 | }
31 | return str_replace('/', '\\', $namespace . $path);
32 | }
33 |
34 | /**
35 | * Get the namespace from the path
36 | *
37 | * @param string $modulePath
38 | * @param string|null $selectedModule
39 | * @return string
40 | * @throws CommandIoNotInitializedException
41 | */
42 | public function getNamespaceFromPath(string $modulePath, string $selectedModule = null): string
43 | {
44 | if (str_contains($modulePath, 'app/code')) {
45 | $namespace = preg_replace('~.*app/code/~', '', $modulePath);
46 | } elseif (str_contains($modulePath, 'vendor')) {
47 | $namespace = preg_replace('~.*vendor/~', '', $modulePath);
48 | } else {
49 | $output = $this->commandIoProvider->getOutput();
50 | $input = $this->commandIoProvider->getInput();
51 | $questionHelper = $this->commandIoProvider->getQuestionHelper();
52 | $proposedNamespace = str_replace('_', '\\', $selectedModule);
53 | $output->writeln('Namespace not found, please set it manually');
54 | $namespaceQuestion = new Question('Enter the namespace [default : ' . $proposedNamespace . '] : ', $proposedNamespace);
55 | $inputNamespace = $questionHelper->ask($input, $output, $namespaceQuestion);
56 | $namespace = $this->validateNamespaceInput($inputNamespace);
57 | if ($inputNamespace !== $namespace) {
58 | $output->writeln('Namespace corrected to ' . $namespace . '');
59 | }
60 | }
61 | return $namespace;
62 | }
63 |
64 | /**
65 | * Removes spaces, replaces / by \, removes trailing \ and removes leading \
66 | *
67 | * @param string $namespace
68 | * @return string
69 | */
70 | private function validateNamespaceInput(string $namespace): string
71 | {
72 | $namespace = str_replace('/', '\\', $namespace);
73 | $namespace = str_replace(' ', '', $namespace);
74 | $namespace = trim($namespace, '\\');
75 | return $namespace;
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/Test/Unit/Database/Service/ConstraintDefinitionTest.php:
--------------------------------------------------------------------------------
1 | yesNoQuestionPerformer = $this->createMock(YesNo::class);
24 | $this->dataTableAutoCompletion = $this->createMock(DataTableAutoCompletion::class);
25 | $input = $this->createMock(InputInterface::class);
26 | $output = $this->createMock(OutputInterface::class);
27 | $this->questionHelper = $this->createMock(QuestionHelper::class);
28 | $commandIoProvider = $this->createMock(CommandIoProvider::class);
29 | $commandIoProvider->method('getInput')->willReturn($input);
30 | $commandIoProvider->method('getOutput')->willReturn($output);
31 | $commandIoProvider->method('getQuestionHelper')->willReturn($this->questionHelper);
32 | $this->constraintDefinition = new ConstraintDefinition($this->yesNoQuestionPerformer, $this->dataTableAutoCompletion, $commandIoProvider);
33 | }
34 |
35 | /**
36 | * @throws ConstraintDefinitionException
37 | */
38 | public function testDefineForeignKeyConstraint()
39 | {
40 | $this->yesNoQuestionPerformer->method('execute')->willReturn(true);
41 | $this->questionHelper->method('ask')
42 | ->will($this->onConsecutiveCalls(
43 | 'foreign', // Constraint type
44 | 'column1', // Column
45 | 'reference_table', // Reference table
46 | 'reference_column', // Reference column
47 | 'CASCADE' // onDelete action
48 | ));
49 | $this->dataTableAutoCompletion->method('getAllTables')->willReturn(['reference_table']);
50 | $this->dataTableAutoCompletion->method('getTableFields')->willReturn(['identity' => 'id', 'fields' => ['reference_column']]);
51 |
52 | $result = $this->constraintDefinition->define('table_name', ['column1' => 'int']);
53 |
54 | $expected = [
55 | 'TABLE_NAME_COLUMN1_REFERENCE_TABLE_REFERENCE_COLUMN' => [
56 | 'type' => 'foreign',
57 | 'table' => 'table_name',
58 | 'column' => 'column1',
59 | 'referenceTable' => 'reference_table',
60 | 'referenceColumn' => 'reference_column',
61 | 'onDelete' => 'CASCADE'
62 | ]
63 | ];
64 |
65 | $this->assertEquals($expected, $result);
66 | }
67 |
68 | /**
69 | * @throws ConstraintDefinitionException
70 | */
71 | public function testDefineUniqueConstraint()
72 | {
73 | $this->yesNoQuestionPerformer->method('execute')->willReturn(true);
74 | $this->questionHelper->method('ask')
75 | ->will($this->onConsecutiveCalls(
76 | 'unique', // Constraint type
77 | 'constraint_name', // Constraint name
78 | 'column1', // Column
79 | '' // End of columns
80 | ));
81 |
82 | $result = $this->constraintDefinition->define('table_name', ['column1' => 'int']);
83 |
84 | $expected = [
85 | 'constraint_name' => [
86 | 'type' => 'unique',
87 | 'columns' => ['column1']
88 | ]
89 | ];
90 |
91 | $this->assertEquals($expected, $result);
92 | }
93 |
94 | /**
95 | * @throws ConstraintDefinitionException
96 | */
97 | public function testDefineNoConstraint()
98 | {
99 | $this->yesNoQuestionPerformer->method('execute')->willReturn(false);
100 |
101 | $result = $this->constraintDefinition->define('table_name', ['column1' => 'int']);
102 |
103 | $this->assertEmpty($result);
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/Test/Unit/Database/Service/DataTableAutoCompletionTest.php:
--------------------------------------------------------------------------------
1 | resourceConnection = $this->createMock(ResourceConnection::class);
24 | $this->dbSchemaParser = $this->createMock(DbSchemaParser::class);
25 | $input = $this->createMock(InputInterface::class);
26 | $output = $this->createMock(OutputInterface::class);
27 | $this->questionHelper = $this->createMock(QuestionHelper::class);
28 | $commandIoProvider = $this->createMock(CommandIoProvider::class);
29 | $commandIoProvider->init($input, $output, $this->questionHelper);
30 | $commandIoProvider->method('getInput')->willReturn($input);
31 | $commandIoProvider->method('getOutput')->willReturn($output);
32 | $commandIoProvider->method('getQuestionHelper')->willReturn($this->questionHelper);
33 | $this->dataTableAutoCompletion = new DataTableAutoCompletion($this->resourceConnection, $this->dbSchemaParser, $commandIoProvider);
34 | }
35 |
36 | public function testGetAllTables()
37 | {
38 | $connection = $this->createMock(\Magento\Framework\DB\Adapter\AdapterInterface::class);
39 | $this->resourceConnection->method('getConnection')->willReturn($connection);
40 | $connection->method('getTables')->willReturn(['table1', 'table2']);
41 |
42 | $result = $this->dataTableAutoCompletion->getAllTables();
43 |
44 | $this->assertEquals(['table1', 'table2'], $result);
45 | }
46 |
47 | public function testGetTableFields()
48 | {
49 | $connection = $this->createMock(\Magento\Framework\DB\Adapter\AdapterInterface::class);
50 | $this->resourceConnection->method('getConnection')->willReturn($connection);
51 | $connection->method('describeTable')->willReturn([
52 | 'id' => ['PRIMARY' => true],
53 | 'field1' => ['PRIMARY' => false],
54 | 'field2' => ['PRIMARY' => false]
55 | ]);
56 |
57 | $result = $this->dataTableAutoCompletion->getTableFields('table1');
58 |
59 | $expected = [
60 | 'fields' => ['id', 'field1', 'field2'],
61 | 'identity' => 'id'
62 | ];
63 |
64 | $this->assertEquals($expected, $result);
65 | }
66 |
67 | public function testTableSelector()
68 | {
69 | $input = $this->createMock(InputInterface::class);
70 | $output = $this->createMock(OutputInterface::class);
71 | $this->dbSchemaParser->method('getModuleDataTables')->willReturn(['table1' => 'Table 1', 'table2' => 'Table 2']);
72 | $this->questionHelper->method('ask')->willReturn('table1');
73 |
74 | $result = $this->dataTableAutoCompletion->tableSelector('module_name');
75 |
76 | $this->assertEquals('table1', $result);
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/Test/Unit/Database/Service/DbSchemaCreatorTest.php:
--------------------------------------------------------------------------------
1 | dbSchemaPath = $this->createMock(DbSchemaPath::class);
19 | $this->dbSchemaCreator = new DbSchemaCreator($this->dbSchemaPath);
20 | }
21 |
22 | public function testCreateDbSchemaWithForeignKey()
23 | {
24 | $selectedModule = 'Test_Module';
25 | $dataTables = [
26 | 'test_table' => [
27 | 'fields' => [
28 | 'id' => ['type' => 'int', 'nullable' => 'false', 'primary' => 'true'],
29 | 'name' => ['type' => 'varchar', 'nullable' => 'false'],
30 | 'test_table_2_id' => ['type' => 'int', 'nullable' => 'false']
31 | ],
32 | 'constraints' => [
33 | 'PRIMARY' => ['type' => 'primary', 'columns' => ['id']],
34 | 'FK_TEST_TABLE_TEST_TABLE_2' => [
35 | 'type' => 'foreign',
36 | 'table' => 'test_table',
37 | 'column' => 'test_table_2_id',
38 | 'referenceTable' => 'test_table_2',
39 | 'referenceColumn' => 'id',
40 | 'onDelete' => 'CASCADE'
41 | ]
42 | ],
43 | 'primary' => 'id',
44 | 'indexes' => [
45 | 'IDX_NAME' => ['type' => 'btree', 'fields' => ['name']]
46 | ],
47 | 'table_attr' => [
48 | 'engine' => 'innodb',
49 | 'resource' => 'default',
50 | 'comment' => 'Test Table'
51 | ]
52 | ],
53 | 'test_table_2' => [
54 | 'fields' => [
55 | 'id' => ['type' => 'int', 'nullable' => 'false', 'primary' => 'true'],
56 | 'description' => ['type' => 'text', 'nullable' => 'true']
57 | ],
58 | 'constraints' => [
59 | 'PRIMARY' => ['type' => 'primary', 'columns' => ['id']]
60 | ],
61 | 'primary' => 'id',
62 | 'indexes' => [],
63 | 'table_attr' => [
64 | 'engine' => 'innodb',
65 | 'resource' => 'default',
66 | 'comment' => 'Test Table 2'
67 | ]
68 | ]
69 | ];
70 |
71 | $tempFile = tempnam(sys_get_temp_dir(), 'db_schema');
72 | $this->dbSchemaPath->method('get')->with($selectedModule)->willReturn($tempFile);
73 |
74 | $this->dbSchemaCreator->createDbSchema($selectedModule, $dataTables);
75 |
76 | $this->assertFileExists($tempFile);
77 | $xmlContent = file_get_contents($tempFile);
78 | $this->assertStringContainsString('', $xmlContent);
79 | $this->assertStringContainsString('', $xmlContent);
80 | $this->assertStringContainsString('', $xmlContent);
81 | $this->assertStringContainsString('', $xmlContent);
82 | $this->assertStringContainsString('', $xmlContent);
83 | $this->assertStringContainsString('', $xmlContent);
84 | $this->assertStringContainsString('', $xmlContent);
85 | $this->assertStringContainsString('', $xmlContent);
86 |
87 | unlink($tempFile);
88 | }
89 |
90 | private function invokeCheckTableDefinition(array $tableDefinition)
91 | {
92 | $reflection = new \ReflectionClass($this->dbSchemaCreator);
93 | $method = $reflection->getMethod('checkTableDefinition');
94 | $method->setAccessible(true);
95 | $method->invoke($this->dbSchemaCreator, $tableDefinition);
96 | }
97 |
98 | public function testCheckTableDefinitionValid()
99 | {
100 | $validTableDefinition = [
101 | 'fields' => [
102 | 'id' => ['type' => 'int', 'nullable' => 'false', 'primary' => 'true']
103 | ],
104 | 'indexes' => [],
105 | 'constraints' => []
106 | ];
107 |
108 | $this->invokeCheckTableDefinition($validTableDefinition);
109 |
110 | $this->assertTrue(true); // If no exception is thrown, the test passes
111 | }
112 |
113 | public function testCheckTableDefinitionMissingFields()
114 | {
115 | $this->expectException(TableDefinitionException::class);
116 | $this->expectExceptionMessage('fields is a required key in the table definition');
117 |
118 | $invalidTableDefinition = [
119 | 'indexes' => [],
120 | 'constraints' => []
121 | ];
122 |
123 | $this->invokeCheckTableDefinition($invalidTableDefinition);
124 | }
125 |
126 | public function testCheckTableDefinitionMissingIndexes()
127 | {
128 | $this->expectException(TableDefinitionException::class);
129 | $this->expectExceptionMessage('indexes is a required key in the table definition and it must be of type array');
130 |
131 | $invalidTableDefinition = [
132 | 'fields' => [
133 | 'id' => ['type' => 'int', 'nullable' => 'false', 'primary' => 'true']
134 | ],
135 | 'constraints' => []
136 | ];
137 |
138 | $this->invokeCheckTableDefinition($invalidTableDefinition);
139 | }
140 |
141 | public function testCheckTableDefinitionMissingConstraints()
142 | {
143 | $this->expectException(TableDefinitionException::class);
144 | $this->expectExceptionMessage('constraints is a required key in the table definition and it must be of type array');
145 |
146 | $invalidTableDefinition = [
147 | 'fields' => [
148 | 'id' => ['type' => 'int', 'nullable' => 'false', 'primary' => 'true']
149 | ],
150 | 'indexes' => []
151 | ];
152 |
153 | $this->invokeCheckTableDefinition($invalidTableDefinition);
154 | }
155 | }
156 |
--------------------------------------------------------------------------------
/Test/Unit/Database/Service/DbSchemaParserTest.php:
--------------------------------------------------------------------------------
1 | dbSchemaPath = $this->createMock(DbSchemaPath::class);
18 | $this->dbSchemaParser = new DbSchemaParser($this->dbSchemaPath);
19 | }
20 |
21 | public function testGetModuleDataTables()
22 | {
23 | $selectedModule = 'Test_Module';
24 | $tempFile = tempnam(sys_get_temp_dir(), 'db_schema.xml');
25 | $xmlContent = <<
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 | XML;
42 |
43 | $this->dbSchemaPath->method('get')->with($selectedModule)->willReturn($tempFile);
44 | file_put_contents($tempFile, $xmlContent);
45 |
46 | $expectedDataTables = [
47 | 'test_table' => [
48 | 'fields' => [
49 | 'id' => [
50 | 'nullable' => 'false',
51 | 'primary' => 'true',
52 | 'type' => 'int'
53 | ],
54 | 'name' => [
55 | 'nullable' => 'false',
56 | 'type' => 'varchar'
57 | ]
58 | ],
59 | 'constraints' => [
60 | 'PRIMARY' => [
61 | 'type' => 'primary',
62 | 'name' => 'PRIMARY',
63 | 'columns' => [
64 | [
65 | '@attributes' => [
66 | 'name' => 'id'
67 | ]
68 | ]
69 | ]
70 | ],
71 | 'FK_TEST_TABLE_ANOTHER_TABLE_ID' => [
72 | 'type' => 'foreign',
73 | 'name' => 'FK_TEST_TABLE_ANOTHER_TABLE_ID',
74 | 'referenceTable' => 'another_table',
75 | 'referenceColumn' => 'id',
76 | 'columns' => [
77 | [
78 | '@attributes' => [
79 | 'name' => 'another_table_id'
80 | ]
81 | ]
82 | ]
83 | ]
84 | ],
85 | 'primary' => 'id',
86 | 'indexes' => [
87 | 'IDX_NAME' => [
88 | 'type' => 'btree',
89 | 'fields' => ['name']
90 | ]
91 | ],
92 | 'table_attr' => [
93 | 'engine' => 'innodb',
94 | 'resource' => 'default',
95 | 'comment' => 'Test Table'
96 | ]
97 | ]
98 | ];
99 |
100 | $actualDataTables = json_decode(json_encode($this->dbSchemaParser->getModuleDataTables($selectedModule)), true);
101 | $this->assertEquals($expectedDataTables, $actualDataTables);
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/Utils/ConsoleCrudEntitySelector.php:
--------------------------------------------------------------------------------
1 | currentModule->getModuleName();
39 | $tableName = $this->dataTableAutoCompletion->tableSelector($moduleName);
40 |
41 | $modelClassName = $this->defaultClassNameGetter->get($tableName, $moduleName);
42 |
43 | return $this->commandIoProvider->getQuestionHelper()->ask(
44 | $this->commandIoProvider->getInput(),
45 | $this->commandIoProvider->getOutput(),
46 | new Question('Enter the class name [default : '. $modelClassName .']: ', $modelClassName)
47 | );
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/Utils/ConsoleModuleSelector.php:
--------------------------------------------------------------------------------
1 | getInstalledModules($includeVendor);
40 |
41 | // Ask for which module you want ot generate stuff
42 | $question = $this->questionFactory->create([
43 | 'question' => 'Please enter the name of a module (begin to type for completion)' . PHP_EOL,
44 | 'default' => 1
45 | ]
46 | );
47 |
48 | $question->setAutocompleterValues($modules);
49 |
50 | $moduleName = $this->commandIoProvider->getQuestionHelper()->ask(
51 | $this->commandIoProvider->getInput(),
52 | $this->commandIoProvider->getOutput(),
53 | $question
54 | );
55 |
56 | if (!is_string($moduleName)) {
57 | throw new \InvalidArgumentException('You did not choose any module');
58 | }
59 |
60 | $modulePath = $this->getModulePath($moduleName);
61 |
62 | $this->currentModule->setCurrentModule($moduleName, $modulePath);
63 | }
64 |
65 | /**
66 | * Get installed modules
67 | *
68 | * @param bool $includeVendor
69 | * @return array
70 | */
71 | private function getInstalledModules($includeVendor = false): array
72 | {
73 | $modules = $this->moduleList->getNames();
74 |
75 | foreach ($modules as $module){
76 | $dir = $this->moduleReader->getModuleDir(null, $module);
77 |
78 | if (!$includeVendor && !str_contains($dir, 'app/code')) {
79 | continue;
80 | }
81 | $this->modulePaths[$module] = $dir;
82 | }
83 |
84 | return array_keys($this->modulePaths);
85 | }
86 |
87 | /**
88 | * @param string $moduleName
89 | * @return string
90 | * @throws \InvalidArgumentException
91 | */
92 | private function getModulePath(string $moduleName): string
93 | {
94 | if (!isset($this->modulePaths[$moduleName])) {
95 | throw new \InvalidArgumentException('The module ' . $moduleName . ' does not exist');
96 | }
97 | return $this->modulePaths[$moduleName];
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/Utils/StringTransformationTools.php:
--------------------------------------------------------------------------------
1 | getCamelCase($string), 'UTF-8');
63 | }
64 |
65 | /**
66 | * Return kebab case formatted string
67 | *
68 | * @param string $string
69 | * @return string
70 | */
71 | public function getKebabCase(string $string): string
72 | {
73 | $string = mb_ucfirst($string, 'UTF-8');
74 |
75 | $result = '';
76 |
77 | for ($i = 0, $iMax = strlen($string); $i < $iMax; $i++) {
78 | $char = $string[$i];
79 |
80 | if (ctype_upper($char)) {
81 | $result .= '-' . strtolower($char);
82 | } elseif ($char === '_') {
83 | $result .= '-';
84 | } else {
85 | $result .= $char;
86 | }
87 | }
88 |
89 | return ltrim($result, '-');
90 | }
91 |
92 | public function sanitizeString(string $string): string
93 | {
94 | $string = str_replace(' ', '-', $string);
95 |
96 | // Remplace all accuented letters by non accuented equivalents
97 | $string = iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $string);
98 |
99 | // Delete all non-alphanumeric characters except dash and underscore
100 | return preg_replace('/[^a-zA-Z0-9-_]/', '', $string);
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "opengento/module-makegento-cli",
3 | "description": "Unleash CLI power to ease magento dumb work",
4 | "keywords": [
5 | "php",
6 | "magento",
7 | "magento2",
8 | "module",
9 | "extension",
10 | "free",
11 | "cli"
12 | ],
13 | "require": {
14 | "php": "^8.2",
15 | "ext-json": "*",
16 | "magento/framework": "^103.0",
17 | "magento/module-config": "*",
18 | "magento/module-store": "*",
19 | "ext-simplexml": "*"
20 | },
21 | "require-dev": {
22 | "magento/magento-coding-standard": "^5",
23 | "magento/marketplace-eqp": "^4.0",
24 | "roave/security-advisories": "dev-master",
25 | "phpunit/phpunit": "*"
26 | },
27 | "type": "magento2-module",
28 | "license": [
29 | "MIT"
30 | ],
31 | "homepage": "https://github.com/opengento/magento2-makegento-cli",
32 | "authors": [
33 | {
34 | "name": "Opengento Team",
35 | "email": "opengento@gmail.com",
36 | "homepage": "https://opengento.fr/",
37 | "role": "lead"
38 | },
39 | {
40 | "name": "Xavier Guenel",
41 | "email": "xavier.guenel@gmail.com",
42 | "homepage": "https://www.linkedin.com/in/%F0%9F%8E%A9-xavier-guenel-255174198/",
43 | "role": "maintainer"
44 | },
45 | {
46 | "name": "Christophe Ferreboeuf",
47 | "email": "christophe@crealoz.fr",
48 | "homepage": "https://www.linkedin.com/in/cferreboeuf/",
49 | "role": "maintainer"
50 | }
51 | ],
52 | "support": {
53 | "source": "https://github.com/opengento/magento2-makegento-cli",
54 | "issues": "https://github.com/opengento/magento2-makegento-cli/issues"
55 | },
56 | "autoload": {
57 | "files": [
58 | "registration.php"
59 | ],
60 | "psr-4": {
61 | "Opengento\\MakegentoCli\\": ""
62 | }
63 | },
64 | "archive": {
65 | "exclude": ["/docs"]
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/docs/images/make-entity-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/opengento/magento2-makegento-cli/237fa52665de5ff77f1c56cc02028700b8b17ed1/docs/images/make-entity-1.png
--------------------------------------------------------------------------------
/docs/make-entity.md:
--------------------------------------------------------------------------------
1 | # Make entity
2 |
3 | This command will generate a db_schema.xml file for a new table. In the future, it will also generate the model, resource
4 | model, and collection.
5 |
6 | ## Command
7 |
8 | ```bash
9 | bin/magento make:entity
10 | ```
11 |
12 | You will then be prompted questions regarding the entity you want to create. You can have look at the example below:
13 |
14 | 
15 |
16 | ## Result
17 |
18 | The command will generate a db_schema.xml file in the `etc/` directory of your module or update existing one.
19 |
20 | ```xml
21 |
22 |
24 |
25 |
27 |
28 |
29 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
51 |
52 |
53 | ```
54 |
55 | This example is based on https://github.com/crealoz/easy-audit-free/blob/master/etc/db_schema.xml
56 |
57 | To get your database updated, you will need to run the following command:
58 |
59 | ```bash
60 | bin/magento setup:upgrade
61 | ```
62 |
63 | ### Disclaimer
64 |
65 | Please note that this command is still in development and may not work as expected.
66 |
67 |
--------------------------------------------------------------------------------
/etc/di.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
10 |
11 |
12 |
13 |
14 |
15 | - Opengento\MakegentoCli\Console\Command\Module
16 | - Opengento\MakegentoCli\Console\Command\Crud
17 | - Opengento\MakegentoCli\Console\Command\Model
18 | - Opengento\MakegentoCli\Console\Command\Database\Entity
19 | - Opengento\MakegentoCli\Console\Command\Database\Constraint
20 | - Opengento\MakegentoCli\Console\Command\Database\Field
21 |
22 |
23 |
24 |
25 |
26 | Opengento\MakegentoCli\Maker\MakeEntity\Proxy
27 | Opengento\MakegentoCli\Utils\ConsoleModuleSelector\Proxy
28 |
29 |
30 |
31 |
32 | Opengento\MakegentoCli\Maker\MakeField\Proxy
33 | Opengento\MakegentoCli\Utils\ConsoleModuleSelector\Proxy
34 |
35 |
36 |
37 |
38 | Opengento\MakegentoCli\Maker\MakeConstraint\Proxy
39 | Opengento\MakegentoCli\Utils\ConsoleModuleSelector\Proxy
40 |
41 |
42 |
43 |
44 | Opengento\MakegentoCli\Maker\MakeModel\Proxy
45 | Opengento\MakegentoCli\Utils\ConsoleModuleSelector\Proxy
46 |
47 |
48 |
49 |
50 | Opengento\MakegentoCli\Maker\MakeCrud\Proxy
51 | Opengento\MakegentoCli\Utils\ConsoleModuleSelector\Proxy
52 | Opengento\MakegentoCli\Utils\ConsoleCrudEntitySelector\Proxy
53 |
54 |
55 |
56 |
--------------------------------------------------------------------------------
/etc/module.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/registration.php:
--------------------------------------------------------------------------------
1 |