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 @@ -6,6 +6,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: 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 @@ -195,6 +195,11 @@
"title": "Generate Sample config.xml",
"category": "Magento Toolbox"
},
{
"command": "magento-toolbox.generateDataPatch",
"title": "Generate Data Patch",
"category": "Magento Toolbox"
},
{
"command": "magento-toolbox.jumpToModule",
"title": "Jump to Module",
Expand Down Expand Up @@ -328,6 +333,10 @@
{
"command": "magento-toolbox.generateConfigXmlFile",
"when": "resourcePath =~ /app\\/code\\/.+\\/.+/i"
},
{
"command": "magento-toolbox.generateDataPatch",
"when": "resourcePath =~ /app\\/code\\/.+\\/.+/i"
}
]
}
Expand Down
58 changes: 58 additions & 0 deletions src/command/GenerateDataPatchCommand.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { Command } from 'command/Command';
import DataPatchWizard, { DataPatchWizardData } from 'wizard/DataPatchWizard';
import WizzardClosedError from 'webview/error/WizzardClosedError';
import FileGeneratorManager from 'generator/FileGeneratorManager';
import Common from 'util/Common';
import { Uri, window } from 'vscode';
import DataPatchGenerator from 'generator/dataPatch/DataPatchGenerator';
import IndexManager from 'indexer/IndexManager';
import ModuleIndexer from 'indexer/module/ModuleIndexer';

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

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 dataPatchWizard = new DataPatchWizard();

let data: DataPatchWizardData;

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

throw error;
}

const manager = new FileGeneratorManager([new DataPatchGenerator(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();
}
}
1 change: 1 addition & 0 deletions src/command/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,4 @@ export { default as GenerateExtensionAttributesXmlFileCommand } from './Generate
export { default as GenerateSystemXmlFileCommand } from './GenerateSystemXmlFileCommand';
export { default as GenerateConfigXmlFileCommand } from './GenerateConfigXmlFileCommand';
export { default as JumpToModuleCommand } from './JumpToModuleCommand';
export { default as GenerateDataPatchCommand } from './GenerateDataPatchCommand';
91 changes: 91 additions & 0 deletions src/generator/dataPatch/DataPatchGenerator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
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 { DataPatchWizardData } from 'wizard/DataPatchWizard';
import Magento from 'util/Magento';
import * as fs from 'fs';
import * as path from 'path';

export default class DataPatchGenerator extends FileGenerator {
private static readonly DATA_PATCH_INTERFACE =
'Magento\\Framework\\Setup\\Patch\\DataPatchInterface';
private static readonly PATCH_REVERTABLE_INTERFACE =
'Magento\\Framework\\Setup\\Patch\\PatchRevertableInterface';

public constructor(protected data: DataPatchWizardData) {
super();
}

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

// Create setup directory if it doesn't exist
const setupDirPath = path.join(moduleDirectory.fsPath, setupDir);
if (!fs.existsSync(setupDirPath)) {
fs.mkdirSync(setupDirPath, { 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());
namespace.addUse(DataPatchGenerator.DATA_PATCH_INTERFACE);

if (this.data.revertable) {
namespace.addUse(DataPatchGenerator.PATCH_REVERTABLE_INTERFACE);
}

const patchClass = namespace.addClass(this.data.className);
patchClass.addImplement(DataPatchGenerator.DATA_PATCH_INTERFACE);

if (this.data.revertable) {
patchClass.addImplement(DataPatchGenerator.PATCH_REVERTABLE_INTERFACE);
}

// Add apply method
const applyMethod = patchClass.addMethod('apply');
applyMethod.addComment('@inheritdoc');
applyMethod.setReturnType('self');
applyMethod.setBody('// TODO: Implement apply() method.\n\nreturn $this;');

// Add revert method if revertable
if (this.data.revertable) {
const revertMethod = patchClass.addMethod('revert');
revertMethod.addComment('@inheritdoc');
revertMethod.setReturnType('void');
revertMethod.setBody('// TODO: Implement revert() method.');
}

// Add getAliases method
const getAliasesMethod = patchClass.addMethod('getAliases');
getAliasesMethod.addComment('@inheritdoc');
getAliasesMethod.setReturnType('array');
getAliasesMethod.setBody('return [];');

// Add getDependencies method
const getDependenciesMethod = patchClass.addMethod('getDependencies');
getDependenciesMethod.setStatic(true);
getDependenciesMethod.addComment('@inheritdoc');
getDependenciesMethod.setReturnType('array');
getDependenciesMethod.setBody('return [];');

const printer = new PsrPrinter();

return new GeneratedFile(
Uri.joinPath(moduleDirectory, setupDir, `${this.data.className}.php`),
printer.printFile(phpFile)
);
}
}
89 changes: 89 additions & 0 deletions src/test/generator/dataPatch/DataPatchGenerator.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { DataPatchWizardData } from 'wizard/DataPatchWizard';
import * as assert from 'assert';
import { Uri } from 'vscode';
import DataPatchGenerator from 'generator/dataPatch/DataPatchGenerator';
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('DataPatchGenerator Tests', () => {
const basePatchWizardData: DataPatchWizardData = {
module: 'Foo_Bar',
className: 'TestDataPatch',
revertable: false,
};

const revertablePatchWizardData: DataPatchWizardData = {
module: 'Foo_Bar',
className: 'TestRevertableDataPatch',
revertable: true,
};

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

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

it('should generate a standard data patch 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 DataPatchGenerator(basePatchWizardData);

// 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/dataPatch/patch.php');

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

it('should generate a revertable data patch 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 DataPatchGenerator(revertablePatchWizardData);

// 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/dataPatch/patchRevertable.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 DataPatchGenerator(basePatchWizardData);

// 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/Setup/Patch/Data/TestDataPatch.php'
).fsPath;

assert.strictEqual(generatedFile.uri.fsPath, expectedPath);
});
});
65 changes: 65 additions & 0 deletions src/wizard/DataPatchWizard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import IndexManager from 'indexer/IndexManager';
import ModuleIndexer from 'indexer/module/ModuleIndexer';
import { GeneratorWizard } from 'webview/GeneratorWizard';
import { WizardFieldBuilder } from 'webview/WizardFieldBuilder';
import { WizardFormBuilder } from 'webview/WizardFormBuilder';
import { WizardTabBuilder } from 'webview/WizardTabBuilder';
import Validation from 'common/Validation';

export interface DataPatchWizardData {
module: string;
className: string;
revertable: boolean;
}

export default class DataPatchWizard extends GeneratorWizard {
public async show(contextModule?: string): Promise<DataPatchWizardData> {
const moduleIndexData = IndexManager.getIndexData(ModuleIndexer.KEY);

if (!moduleIndexData) {
throw new Error('Module index data not found');
}

const modules = moduleIndexData.getModuleOptions(module => module.location === 'app');

const builder = new WizardFormBuilder();

builder.setTitle('Generate a new Data Patch');
builder.setDescription('Generates a new Data Patch for a module.');

const tab = new WizardTabBuilder();
tab.setId('dataPatch');
tab.setTitle('Data Patch');

tab.addField(
WizardFieldBuilder.select('module', 'Module')
.setDescription(['Module where data patch will be generated in'])
.setOptions(modules)
.setInitialValue(contextModule || modules[0].value)
.build()
);

tab.addField(
WizardFieldBuilder.text('className', 'Class Name')
.setDescription(['The class name for the data patch'])
.setPlaceholder('YourPatchName')
.build()
);

tab.addField(
WizardFieldBuilder.checkbox('revertable', 'Revertable').setInitialValue(false).build()
);

builder.addTab(tab.build());

builder.addValidation('module', 'required');
builder.addValidation('className', [
'required',
`regex:/${Validation.CLASS_NAME_REGEX.source}/`,
]);

const data = await this.openWizard<DataPatchWizardData>(builder.build());

return data;
}
}
40 changes: 40 additions & 0 deletions test-resources/reference/generator/dataPatch/patch.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

/**
* Foo_Bar
*/

declare(strict_types=1);

namespace Foo\Bar\Setup\Patch\Data;

use Magento\Framework\Setup\Patch\DataPatchInterface;

class TestDataPatch implements DataPatchInterface
{
/**
* @inheritdoc
*/
public function apply(): self
{
// TODO: Implement apply() method.

return $this;
}

/**
* @inheritdoc
*/
public function getAliases(): array
{
return [];
}

/**
* @inheritdoc
*/
public static function getDependencies(): array
{
return [];
}
}
Loading