Skip to content

Commit 73ede93

Browse files
committed
feat: cron job generator
1 parent f7bfe35 commit 73ede93

File tree

19 files changed

+583
-3
lines changed

19 files changed

+583
-3
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how
77
## [Unreleased]
88
- Added: Generator command for a ViewModel class
99
- Added: Generator command for data patches
10+
- Added: Generator command for cron jobs
1011
- Added: Jump-to-definition for magento modules (in module.xml and routes.xml)
1112
- Fixed: Method plugin hover messages are now grouped and include a link to di.xml
1213

package.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,11 @@
200200
"title": "Generate Data Patch",
201201
"category": "Magento Toolbox"
202202
},
203+
{
204+
"command": "magento-toolbox.generateCronJob",
205+
"title": "Generate Cron Job",
206+
"category": "Magento Toolbox"
207+
},
203208
{
204209
"command": "magento-toolbox.jumpToModule",
205210
"title": "Jump to Module",
@@ -337,6 +342,10 @@
337342
{
338343
"command": "magento-toolbox.generateDataPatch",
339344
"when": "resourcePath =~ /app\\/code\\/.+\\/.+/i"
345+
},
346+
{
347+
"command": "magento-toolbox.generateCronJob",
348+
"when": "resourcePath =~ /app\\/code\\/.+\\/.+/i"
340349
}
341350
]
342351
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { Command } from 'command/Command';
2+
import CronJobWizard, { CronJobWizardData } from 'wizard/CronJobWizard';
3+
import WizzardClosedError from 'webview/error/WizzardClosedError';
4+
import FileGeneratorManager from 'generator/FileGeneratorManager';
5+
import Common from 'util/Common';
6+
import { Uri, window } from 'vscode';
7+
import CronJobClassGenerator from 'generator/cronJob/CronJobClassGenerator';
8+
import CronJobXmlGenerator from 'generator/cronJob/CronJobXmlGenerator';
9+
import IndexManager from 'indexer/IndexManager';
10+
import ModuleIndexer from 'indexer/module/ModuleIndexer';
11+
12+
export default class GenerateCronJobCommand extends Command {
13+
constructor() {
14+
super('magento-toolbox.generateCronJob');
15+
}
16+
17+
public async execute(uri?: Uri): Promise<void> {
18+
const moduleIndex = IndexManager.getIndexData(ModuleIndexer.KEY);
19+
let contextModule: string | undefined;
20+
21+
const contextUri = uri || window.activeTextEditor?.document.uri;
22+
23+
if (moduleIndex && contextUri) {
24+
const module = moduleIndex.getModuleByUri(contextUri);
25+
26+
if (module) {
27+
contextModule = module.name;
28+
}
29+
}
30+
31+
const cronJobWizard = new CronJobWizard();
32+
33+
let data: CronJobWizardData;
34+
35+
try {
36+
data = await cronJobWizard.show(contextModule);
37+
} catch (error) {
38+
if (error instanceof WizzardClosedError) {
39+
return;
40+
}
41+
42+
throw error;
43+
}
44+
45+
const manager = new FileGeneratorManager([
46+
new CronJobClassGenerator(data),
47+
new CronJobXmlGenerator(data),
48+
]);
49+
50+
const workspaceFolder = Common.getActiveWorkspaceFolder();
51+
52+
if (!workspaceFolder) {
53+
window.showErrorMessage('No active workspace folder');
54+
return;
55+
}
56+
57+
await manager.generate(workspaceFolder.uri);
58+
await manager.writeFiles();
59+
await manager.refreshIndex(workspaceFolder);
60+
manager.openAllFiles();
61+
}
62+
}

src/command/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,5 +26,6 @@ export { default as GenerateWidgetXmlFileCommand } from './GenerateWidgetXmlFile
2626
export { default as GenerateExtensionAttributesXmlFileCommand } from './GenerateExtensionAttributesXmlFileCommand';
2727
export { default as GenerateSystemXmlFileCommand } from './GenerateSystemXmlFileCommand';
2828
export { default as GenerateConfigXmlFileCommand } from './GenerateConfigXmlFileCommand';
29-
export { default as JumpToModuleCommand } from './JumpToModuleCommand';
3029
export { default as GenerateDataPatchCommand } from './GenerateDataPatchCommand';
30+
export { default as GenerateCronJobCommand } from './GenerateCronJobCommand';
31+
export { default as JumpToModuleCommand } from './JumpToModuleCommand';
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import FileHeader from 'common/php/FileHeader';
2+
import PhpNamespace from 'common/PhpNamespace';
3+
import GeneratedFile from 'generator/GeneratedFile';
4+
import FileGenerator from 'generator/FileGenerator';
5+
import { PhpFile, PsrPrinter } from 'node-php-generator';
6+
import { Uri } from 'vscode';
7+
import { CronJobWizardData } from 'wizard/CronJobWizard';
8+
import Magento from 'util/Magento';
9+
import * as fs from 'fs';
10+
import * as path from 'path';
11+
12+
export default class CronJobClassGenerator extends FileGenerator {
13+
public constructor(protected data: CronJobWizardData) {
14+
super();
15+
}
16+
17+
public async generate(workspaceUri: Uri): Promise<GeneratedFile> {
18+
const [vendor, module] = this.data.module.split('_');
19+
const cronDir = 'Cron';
20+
const namespaceParts = [vendor, module, 'Cron'];
21+
const moduleDirectory = Magento.getModuleDirectory(vendor, module, workspaceUri);
22+
23+
// Create cron directory if it doesn't exist
24+
const cronDirPath = path.join(moduleDirectory.fsPath, cronDir);
25+
if (!fs.existsSync(cronDirPath)) {
26+
fs.mkdirSync(cronDirPath, { recursive: true });
27+
}
28+
29+
const phpFile = new PhpFile();
30+
phpFile.setStrictTypes(true);
31+
32+
const header = FileHeader.getHeader(this.data.module);
33+
34+
if (header) {
35+
phpFile.addComment(header);
36+
}
37+
38+
const namespace = phpFile.addNamespace(PhpNamespace.fromParts(namespaceParts).toString());
39+
40+
const cronClass = namespace.addClass(this.data.className);
41+
42+
// Add execute method
43+
const executeMethod = cronClass.addMethod('execute');
44+
executeMethod.addComment('Execute the cron');
45+
executeMethod.addComment('\n@return void');
46+
executeMethod.setReturnType('void');
47+
executeMethod.setBody('// TODO: Implement execute() method.');
48+
49+
const printer = new PsrPrinter();
50+
51+
return new GeneratedFile(
52+
Uri.joinPath(moduleDirectory, cronDir, `${this.data.className}.php`),
53+
printer.printFile(phpFile)
54+
);
55+
}
56+
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import GeneratedFile from 'generator/GeneratedFile';
2+
import FileGenerator from 'generator/FileGenerator';
3+
import FindOrCreateCrontabXml from 'generator/util/FindOrCreateCrontabXml';
4+
import { Uri } from 'vscode';
5+
import { CronJobWizardData } from 'wizard/CronJobWizard';
6+
import indentString from 'indent-string';
7+
import Magento from 'util/Magento';
8+
import HandlebarsTemplateRenderer from 'generator/HandlebarsTemplateRenderer';
9+
import { MagentoScope } from 'types/global';
10+
import { TemplatePath } from 'types/handlebars';
11+
12+
export default class CronJobXmlGenerator extends FileGenerator {
13+
public constructor(protected data: CronJobWizardData) {
14+
super();
15+
}
16+
17+
public async generate(workspaceUri: Uri): Promise<GeneratedFile> {
18+
const [vendor, module] = this.data.module.split('_');
19+
const moduleDirectory = Magento.getModuleDirectory(vendor, module, workspaceUri);
20+
const etcDirectory = Uri.joinPath(moduleDirectory, 'etc');
21+
const crontabFile = Uri.joinPath(etcDirectory, 'crontab.xml');
22+
23+
// Prepare cron job data
24+
const jobName = this.data.cronName;
25+
const jobInstance = `${vendor}\\${module}\\Cron\\${this.data.className}`;
26+
27+
// Get or create crontab.xml content
28+
const crontabXml = await FindOrCreateCrontabXml.execute(
29+
workspaceUri,
30+
vendor,
31+
module,
32+
MagentoScope.Global
33+
);
34+
35+
// Create template renderer
36+
const renderer = new HandlebarsTemplateRenderer();
37+
38+
// Generate job XML using the template
39+
const jobXml = await renderer.render(TemplatePath.XmlCronJob, {
40+
jobName,
41+
jobInstance,
42+
cronSchedule: this.data.cronSchedule,
43+
});
44+
45+
// Check if group exists
46+
const groupExists = this.checkIfGroupExists(crontabXml, this.data.cronGroup);
47+
48+
let insertXml: string;
49+
50+
if (groupExists) {
51+
// Group exists, just use the job XML
52+
insertXml = indentString(jobXml, 4);
53+
} else {
54+
// Group doesn't exist, create it with the job XML
55+
insertXml = await renderer.render(
56+
TemplatePath.XmlCronGroup,
57+
{
58+
groupId: this.data.cronGroup,
59+
},
60+
{
61+
groupContent: jobXml,
62+
}
63+
);
64+
}
65+
66+
// Find insertion position
67+
const insertPosition = this.getInsertPosition(crontabXml, this.data.cronGroup);
68+
69+
// Insert new group
70+
const newCrontabXml =
71+
crontabXml.slice(0, insertPosition) +
72+
'\n' +
73+
indentString(insertXml, 4) +
74+
'\n' +
75+
crontabXml.slice(insertPosition);
76+
77+
return new GeneratedFile(crontabFile, newCrontabXml, false);
78+
}
79+
80+
/**
81+
* Check if group with given ID already exists
82+
*/
83+
private checkIfGroupExists(crontabXml: string, groupId: string): boolean {
84+
const groupRegex = new RegExp(`<group\\s+id=["']${groupId}["']`, 'i');
85+
return groupRegex.test(crontabXml);
86+
}
87+
88+
/**
89+
* Get position to insert new content
90+
*/
91+
private getInsertPosition(crontabXml: string, groupId: string): number {
92+
if (this.checkIfGroupExists(crontabXml, groupId)) {
93+
// If group exists, find position after group opening tag
94+
const groupRegex = new RegExp(`<group\\s+id=["']${groupId}["'][^>]*>`, 'i');
95+
const match = groupRegex.exec(crontabXml);
96+
if (match) {
97+
return match.index + match[0].length;
98+
}
99+
}
100+
101+
// Otherwise insert before </config>
102+
return crontabXml.indexOf('</config>');
103+
}
104+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import Magento from 'util/Magento';
2+
import { Uri } from 'vscode';
3+
import { MagentoScope } from 'types/global';
4+
import { TemplatePath } from 'types/handlebars';
5+
import HandlebarsTemplateRenderer from 'generator/HandlebarsTemplateRenderer';
6+
import FileSystem from 'util/FileSystem';
7+
import FileHeader from 'common/xml/FileHeader';
8+
9+
export default class FindOrCreateCrontabXml {
10+
public static async execute(
11+
workspaceUri: Uri,
12+
vendor: string,
13+
module: string,
14+
area: MagentoScope = MagentoScope.Global
15+
): Promise<string> {
16+
const modulePath = Magento.getModuleDirectory(vendor, module, workspaceUri, 'etc');
17+
const crontabFile = Magento.getUriWithArea(modulePath, 'crontab.xml', area);
18+
19+
if (await FileSystem.fileExists(crontabFile)) {
20+
return await FileSystem.readFile(crontabFile);
21+
}
22+
23+
const fileHeader = FileHeader.getHeader(module);
24+
25+
const renderer = new HandlebarsTemplateRenderer();
26+
27+
return await renderer.render(TemplatePath.XmlBlankCrontab, {
28+
fileHeader,
29+
});
30+
}
31+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { CronJobWizardData } from 'wizard/CronJobWizard';
2+
import * as assert from 'assert';
3+
import { Uri } from 'vscode';
4+
import CronJobClassGenerator from 'generator/cronJob/CronJobClassGenerator';
5+
import { describe, it, before, afterEach } from 'mocha';
6+
import { setup } from 'test/setup';
7+
import { getReferenceFile, getTestWorkspaceUri } from 'test/util';
8+
import FileHeader from 'common/php/FileHeader';
9+
import sinon from 'sinon';
10+
11+
describe('CronJobClassGenerator Tests', () => {
12+
const cronJobWizardData: CronJobWizardData = {
13+
module: 'Foo_Bar',
14+
className: 'TestCronJob',
15+
cronName: 'foo_bar_test_cron_job',
16+
cronGroup: 'default',
17+
cronSchedule: '* * * * *',
18+
};
19+
20+
before(async () => {
21+
await setup();
22+
});
23+
24+
afterEach(() => {
25+
sinon.restore();
26+
});
27+
28+
it('should generate cron job class file', async () => {
29+
// Mock the FileHeader.getHeader method to return a consistent header
30+
sinon.stub(FileHeader, 'getHeader').returns('Foo_Bar');
31+
32+
// Create the generator with test data
33+
const generator = new CronJobClassGenerator(cronJobWizardData);
34+
35+
// Use a test workspace URI
36+
const workspaceUri = getTestWorkspaceUri();
37+
38+
// Generate the file
39+
const generatedFile = await generator.generate(workspaceUri);
40+
41+
// Get the reference file content
42+
const referenceContent = getReferenceFile('generator/cronJob/TestCronJob.php');
43+
44+
// Compare the generated content with reference
45+
assert.strictEqual(generatedFile.content, referenceContent);
46+
});
47+
48+
it('should generate file in correct location', async () => {
49+
// Create the generator with test data
50+
const generator = new CronJobClassGenerator(cronJobWizardData);
51+
52+
// Use a test workspace URI
53+
const workspaceUri = getTestWorkspaceUri();
54+
55+
// Generate the file
56+
const generatedFile = await generator.generate(workspaceUri);
57+
58+
// Expected path
59+
const expectedPath = Uri.joinPath(workspaceUri, 'app/code/Foo/Bar/Cron/TestCronJob.php').fsPath;
60+
61+
assert.strictEqual(generatedFile.uri.fsPath, expectedPath);
62+
});
63+
});

0 commit comments

Comments
 (0)