Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how
## [Unreleased]
- Added: Generator command for a ViewModel class
- Added: Generator command for data patches
- Added: Generator command for cron jobs
- Added: Jump-to-definition for magento modules (in module.xml and routes.xml)
- Fixed: Method plugin hover messages are now grouped and include a link to di.xml

Expand Down
9 changes: 9 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,11 @@
"title": "Generate Data Patch",
"category": "Magento Toolbox"
},
{
"command": "magento-toolbox.generateCronJob",
"title": "Generate Cron Job",
"category": "Magento Toolbox"
},
{
"command": "magento-toolbox.jumpToModule",
"title": "Jump to Module",
Expand Down Expand Up @@ -337,6 +342,10 @@
{
"command": "magento-toolbox.generateDataPatch",
"when": "resourcePath =~ /app\\/code\\/.+\\/.+/i"
},
{
"command": "magento-toolbox.generateCronJob",
"when": "resourcePath =~ /app\\/code\\/.+\\/.+/i"
}
]
}
Expand Down
62 changes: 62 additions & 0 deletions src/command/GenerateCronJobCommand.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { Command } from 'command/Command';
import CronJobWizard, { CronJobWizardData } from 'wizard/CronJobWizard';
import WizzardClosedError from 'webview/error/WizzardClosedError';
import FileGeneratorManager from 'generator/FileGeneratorManager';
import Common from 'util/Common';
import { Uri, window } from 'vscode';
import CronJobClassGenerator from 'generator/cronJob/CronJobClassGenerator';
import CronJobXmlGenerator from 'generator/cronJob/CronJobXmlGenerator';
import IndexManager from 'indexer/IndexManager';
import ModuleIndexer from 'indexer/module/ModuleIndexer';

export default class GenerateCronJobCommand extends Command {
constructor() {
super('magento-toolbox.generateCronJob');
}

public async execute(uri?: Uri): Promise<void> {
const moduleIndex = IndexManager.getIndexData(ModuleIndexer.KEY);
let contextModule: string | undefined;

const contextUri = uri || window.activeTextEditor?.document.uri;

if (moduleIndex && contextUri) {
const module = moduleIndex.getModuleByUri(contextUri);

if (module) {
contextModule = module.name;
}
}

const cronJobWizard = new CronJobWizard();

let data: CronJobWizardData;

try {
data = await cronJobWizard.show(contextModule);
} catch (error) {
if (error instanceof WizzardClosedError) {
return;
}

throw error;
}

const manager = new FileGeneratorManager([
new CronJobClassGenerator(data),
new CronJobXmlGenerator(data),
]);

const workspaceFolder = Common.getActiveWorkspaceFolder();

if (!workspaceFolder) {
window.showErrorMessage('No active workspace folder');
return;
}

await manager.generate(workspaceFolder.uri);
await manager.writeFiles();
await manager.refreshIndex(workspaceFolder);
manager.openAllFiles();
}
}
3 changes: 2 additions & 1 deletion src/command/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,6 @@ export { default as GenerateWidgetXmlFileCommand } from './GenerateWidgetXmlFile
export { default as GenerateExtensionAttributesXmlFileCommand } from './GenerateExtensionAttributesXmlFileCommand';
export { default as GenerateSystemXmlFileCommand } from './GenerateSystemXmlFileCommand';
export { default as GenerateConfigXmlFileCommand } from './GenerateConfigXmlFileCommand';
export { default as JumpToModuleCommand } from './JumpToModuleCommand';
export { default as GenerateDataPatchCommand } from './GenerateDataPatchCommand';
export { default as GenerateCronJobCommand } from './GenerateCronJobCommand';
export { default as JumpToModuleCommand } from './JumpToModuleCommand';
56 changes: 56 additions & 0 deletions src/generator/cronJob/CronJobClassGenerator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import FileHeader from 'common/php/FileHeader';
import PhpNamespace from 'common/PhpNamespace';
import GeneratedFile from 'generator/GeneratedFile';
import FileGenerator from 'generator/FileGenerator';
import { PhpFile, PsrPrinter } from 'node-php-generator';
import { Uri } from 'vscode';
import { CronJobWizardData } from 'wizard/CronJobWizard';
import Magento from 'util/Magento';
import * as fs from 'fs';
import * as path from 'path';

export default class CronJobClassGenerator extends FileGenerator {
public constructor(protected data: CronJobWizardData) {
super();
}

public async generate(workspaceUri: Uri): Promise<GeneratedFile> {
const [vendor, module] = this.data.module.split('_');
const cronDir = 'Cron';
const namespaceParts = [vendor, module, 'Cron'];
const moduleDirectory = Magento.getModuleDirectory(vendor, module, workspaceUri);

// Create cron directory if it doesn't exist
const cronDirPath = path.join(moduleDirectory.fsPath, cronDir);
if (!fs.existsSync(cronDirPath)) {
fs.mkdirSync(cronDirPath, { recursive: true });
}

const phpFile = new PhpFile();
phpFile.setStrictTypes(true);

const header = FileHeader.getHeader(this.data.module);

if (header) {
phpFile.addComment(header);
}

const namespace = phpFile.addNamespace(PhpNamespace.fromParts(namespaceParts).toString());

const cronClass = namespace.addClass(this.data.className);

// Add execute method
const executeMethod = cronClass.addMethod('execute');
executeMethod.addComment('Execute the cron');
executeMethod.addComment('\n@return void');
executeMethod.setReturnType('void');
executeMethod.setBody('// TODO: Implement execute() method.');

const printer = new PsrPrinter();

return new GeneratedFile(
Uri.joinPath(moduleDirectory, cronDir, `${this.data.className}.php`),
printer.printFile(phpFile)
);
}
}
104 changes: 104 additions & 0 deletions src/generator/cronJob/CronJobXmlGenerator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import GeneratedFile from 'generator/GeneratedFile';
import FileGenerator from 'generator/FileGenerator';
import FindOrCreateCrontabXml from 'generator/util/FindOrCreateCrontabXml';
import { Uri } from 'vscode';
import { CronJobWizardData } from 'wizard/CronJobWizard';
import indentString from 'indent-string';
import Magento from 'util/Magento';
import HandlebarsTemplateRenderer from 'generator/HandlebarsTemplateRenderer';
import { MagentoScope } from 'types/global';
import { TemplatePath } from 'types/handlebars';

export default class CronJobXmlGenerator extends FileGenerator {
public constructor(protected data: CronJobWizardData) {
super();
}

public async generate(workspaceUri: Uri): Promise<GeneratedFile> {
const [vendor, module] = this.data.module.split('_');
const moduleDirectory = Magento.getModuleDirectory(vendor, module, workspaceUri);
const etcDirectory = Uri.joinPath(moduleDirectory, 'etc');
const crontabFile = Uri.joinPath(etcDirectory, 'crontab.xml');

// Prepare cron job data
const jobName = this.data.cronName;
const jobInstance = `${vendor}\\${module}\\Cron\\${this.data.className}`;

// Get or create crontab.xml content
const crontabXml = await FindOrCreateCrontabXml.execute(
workspaceUri,
vendor,
module,
MagentoScope.Global
);

// Create template renderer
const renderer = new HandlebarsTemplateRenderer();

// Generate job XML using the template
const jobXml = await renderer.render(TemplatePath.XmlCronJob, {
jobName,
jobInstance,
cronSchedule: this.data.cronSchedule,
});

// Check if group exists
const groupExists = this.checkIfGroupExists(crontabXml, this.data.cronGroup);

let insertXml: string;

if (groupExists) {
// Group exists, just use the job XML
insertXml = indentString(jobXml, 4);
} else {
// Group doesn't exist, create it with the job XML
insertXml = await renderer.render(
TemplatePath.XmlCronGroup,
{
groupId: this.data.cronGroup,
},
{
groupContent: jobXml,
}
);
}

// Find insertion position
const insertPosition = this.getInsertPosition(crontabXml, this.data.cronGroup);

// Insert new group
const newCrontabXml =
crontabXml.slice(0, insertPosition) +
'\n' +
indentString(insertXml, 4) +
'\n' +
crontabXml.slice(insertPosition);

return new GeneratedFile(crontabFile, newCrontabXml, false);
}

/**
* Check if group with given ID already exists
*/
private checkIfGroupExists(crontabXml: string, groupId: string): boolean {
const groupRegex = new RegExp(`<group\\s+id=["']${groupId}["']`, 'i');
return groupRegex.test(crontabXml);
}

/**
* Get position to insert new content
*/
private getInsertPosition(crontabXml: string, groupId: string): number {
if (this.checkIfGroupExists(crontabXml, groupId)) {
// If group exists, find position after group opening tag
const groupRegex = new RegExp(`<group\\s+id=["']${groupId}["'][^>]*>`, 'i');
const match = groupRegex.exec(crontabXml);
if (match) {
return match.index + match[0].length;
}
}

// Otherwise insert before </config>
return crontabXml.indexOf('</config>');
}
}
31 changes: 31 additions & 0 deletions src/generator/util/FindOrCreateCrontabXml.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import Magento from 'util/Magento';
import { Uri } from 'vscode';
import { MagentoScope } from 'types/global';
import { TemplatePath } from 'types/handlebars';
import HandlebarsTemplateRenderer from 'generator/HandlebarsTemplateRenderer';
import FileSystem from 'util/FileSystem';
import FileHeader from 'common/xml/FileHeader';

export default class FindOrCreateCrontabXml {
public static async execute(
workspaceUri: Uri,
vendor: string,
module: string,
area: MagentoScope = MagentoScope.Global
): Promise<string> {
const modulePath = Magento.getModuleDirectory(vendor, module, workspaceUri, 'etc');
const crontabFile = Magento.getUriWithArea(modulePath, 'crontab.xml', area);

if (await FileSystem.fileExists(crontabFile)) {
return await FileSystem.readFile(crontabFile);
}

const fileHeader = FileHeader.getHeader(module);

const renderer = new HandlebarsTemplateRenderer();

return await renderer.render(TemplatePath.XmlBlankCrontab, {
fileHeader,
});
}
}
63 changes: 63 additions & 0 deletions src/test/generator/cronJob/CronJobClassGenerator.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { CronJobWizardData } from 'wizard/CronJobWizard';
import * as assert from 'assert';
import { Uri } from 'vscode';
import CronJobClassGenerator from 'generator/cronJob/CronJobClassGenerator';
import { describe, it, before, afterEach } from 'mocha';
import { setup } from 'test/setup';
import { getReferenceFile, getTestWorkspaceUri } from 'test/util';
import FileHeader from 'common/php/FileHeader';
import sinon from 'sinon';

describe('CronJobClassGenerator Tests', () => {
const cronJobWizardData: CronJobWizardData = {
module: 'Foo_Bar',
className: 'TestCronJob',
cronName: 'foo_bar_test_cron_job',
cronGroup: 'default',
cronSchedule: '* * * * *',
};

before(async () => {
await setup();
});

afterEach(() => {
sinon.restore();
});

it('should generate cron job class file', async () => {
// Mock the FileHeader.getHeader method to return a consistent header
sinon.stub(FileHeader, 'getHeader').returns('Foo_Bar');

// Create the generator with test data
const generator = new CronJobClassGenerator(cronJobWizardData);

// Use a test workspace URI
const workspaceUri = getTestWorkspaceUri();

// Generate the file
const generatedFile = await generator.generate(workspaceUri);

// Get the reference file content
const referenceContent = getReferenceFile('generator/cronJob/TestCronJob.php');

// Compare the generated content with reference
assert.strictEqual(generatedFile.content, referenceContent);
});

it('should generate file in correct location', async () => {
// Create the generator with test data
const generator = new CronJobClassGenerator(cronJobWizardData);

// Use a test workspace URI
const workspaceUri = getTestWorkspaceUri();

// Generate the file
const generatedFile = await generator.generate(workspaceUri);

// Expected path
const expectedPath = Uri.joinPath(workspaceUri, 'app/code/Foo/Bar/Cron/TestCronJob.php').fsPath;

assert.strictEqual(generatedFile.uri.fsPath, expectedPath);
});
});
Loading