/**
* Get Magento Pattern Tool
* Returns the correct Magento way to accomplish a task.
* This is the main tool for "vibe coding" - writing compliant code naturally.
*/
import {
INSECURE_FUNCTIONS,
DISCOURAGED_FUNCTIONS,
RESTRICTED_CLASSES,
XSS_ESCAPE_METHODS,
TEMPLATE_STRUCTURE_EXAMPLE,
VIEWMODEL_EXAMPLE,
LAYOUT_VIEWMODEL_EXAMPLE,
MAGENTO_JS_PATTERNS,
} from '../knowledge/index.js';
import { getActiveTheme } from '../themes/index.js';
export interface MagentoPatternResult {
task: string;
correctPattern: string;
example: string;
avoidPatterns: Array<{ pattern: string; reason: string }>;
explanation: string;
relatedPatterns?: Array<{ name: string; use: string }>;
documentation?: string;
}
/**
* Pattern database for common Magento tasks
*/
const TASK_PATTERNS: Record<string, () => MagentoPatternResult> = {
// === FILE OPERATIONS ===
'read file': () => ({
task: 'Read a file',
correctPattern: 'Magento\\Framework\\Filesystem\\DriverInterface::fileGetContents()',
example: `<?php
use Magento\\Framework\\Filesystem\\DriverInterface;
class MyClass
{
private DriverInterface $driver;
public function __construct(DriverInterface $driver)
{
$this->driver = $driver;
}
public function readFile(string $path): string
{
return $this->driver->fileGetContents($path);
}
}`,
avoidPatterns: [
{ pattern: 'file_get_contents()', reason: 'Not compatible with remote storage' },
{ pattern: 'fopen() + fread()', reason: 'Use Magento abstraction layer' },
],
explanation: 'Use DriverInterface for all file operations to ensure compatibility with cloud/remote storage and proper abstraction.',
}),
'write file': () => ({
task: 'Write to a file',
correctPattern: 'Magento\\Framework\\Filesystem\\DriverInterface::filePutContents()',
example: `<?php
use Magento\\Framework\\Filesystem\\DriverInterface;
class MyClass
{
private DriverInterface $driver;
public function __construct(DriverInterface $driver)
{
$this->driver = $driver;
}
public function writeFile(string $path, string $content): int
{
return $this->driver->filePutContents($path, $content);
}
}`,
avoidPatterns: [
{ pattern: 'file_put_contents()', reason: 'Not compatible with remote storage' },
{ pattern: 'fopen() + fwrite()', reason: 'Use Magento abstraction layer' },
],
explanation: 'Use DriverInterface for all file write operations.',
}),
'check file exists': () => ({
task: 'Check if file exists',
correctPattern: 'Magento\\Framework\\Filesystem\\DriverInterface::isExists()',
example: `<?php
if ($this->driver->isExists($path)) {
// File exists
}`,
avoidPatterns: [
{ pattern: 'file_exists()', reason: 'Not compatible with remote storage' },
],
explanation: 'Use isExists() method from DriverInterface.',
}),
'create directory': () => ({
task: 'Create a directory',
correctPattern: 'Magento\\Framework\\Filesystem\\DriverInterface::createDirectory()',
example: `<?php
$this->driver->createDirectory($path, 0775);`,
avoidPatterns: [
{ pattern: 'mkdir()', reason: 'Use Magento filesystem abstraction' },
],
explanation: 'Use createDirectory() method from DriverInterface.',
}),
// === ESCAPING / XSS ===
'escape html': () => ({
task: 'Escape HTML content',
correctPattern: '$escaper->escapeHtml()',
example: `<?php
// In template (phtml):
<?= $escaper->escapeHtml($block->getName()) ?>
// In PHP class, inject Escaper:
use Magento\\Framework\\Escaper;
private Escaper $escaper;
public function __construct(Escaper $escaper)
{
$this->escaper = $escaper;
}
public function getEscapedName(string $name): string
{
return $this->escaper->escapeHtml($name);
}`,
avoidPatterns: [
{ pattern: 'htmlspecialchars()', reason: 'Use Magento Escaper for consistency' },
{ pattern: 'strip_tags()', reason: 'Not sufficient for XSS protection' },
],
explanation: 'Always escape output using Magento\\Framework\\Escaper. In templates, $escaper is available automatically.',
relatedPatterns: Object.entries(XSS_ESCAPE_METHODS).map(([name, info]) => ({
name: `$escaper->${name}()`,
use: info.use
})),
}),
'escape url': () => ({
task: 'Escape URL',
correctPattern: '$escaper->escapeUrl()',
example: `<a href="<?= $escaper->escapeUrl($block->getUrl()) ?>">Link</a>`,
avoidPatterns: [
{ pattern: 'urlencode()', reason: 'Use escapeUrl for HTML context' },
],
explanation: 'Use escapeUrl() for URLs in href/src attributes.',
}),
'escape javascript': () => ({
task: 'Escape for JavaScript',
correctPattern: '$escaper->escapeJs()',
example: `<script>var name = "<?= $escaper->escapeJs($name) ?>";</script>`,
avoidPatterns: [
{ pattern: 'json_encode() alone', reason: 'Use escapeJs for string embedding' },
],
explanation: 'Use escapeJs() when embedding PHP values in JavaScript strings.',
}),
// === JSON / SERIALIZATION ===
'json encode': () => ({
task: 'Encode data to JSON',
correctPattern: 'Magento\\Framework\\Serialize\\Serializer\\Json',
example: `<?php
use Magento\\Framework\\Serialize\\Serializer\\Json;
class MyClass
{
private Json $jsonSerializer;
public function __construct(Json $jsonSerializer)
{
$this->jsonSerializer = $jsonSerializer;
}
public function encode(array $data): string
{
return $this->jsonSerializer->serialize($data);
}
public function decode(string $json): array
{
return $this->jsonSerializer->unserialize($json);
}
}`,
avoidPatterns: [
{ pattern: 'json_encode()/json_decode()', reason: 'Use Magento serializer for consistency' },
{ pattern: 'serialize()/unserialize()', reason: 'SECURITY RISK - object injection vulnerability' },
{ pattern: 'Zend_Json', reason: 'Deprecated - use Magento Json serializer' },
],
explanation: 'Use Magento Json serializer. NEVER use PHP serialize/unserialize.',
}),
// === VALIDATION ===
'validate email': () => ({
task: 'Validate email address',
correctPattern: 'Magento\\Framework\\Validator\\EmailAddress',
example: `<?php
use Magento\\Framework\\Validator\\EmailAddress;
$validator = new EmailAddress();
if ($validator->isValid($email)) {
// Email is valid
}`,
avoidPatterns: [
{ pattern: 'Zend_Validate_EmailAddress', reason: 'Deprecated - use Magento validator' },
{ pattern: 'filter_var() alone', reason: 'Use Magento validator for consistency' },
],
explanation: 'Use Magento Framework validators for input validation.',
}),
// === TEMPLATES ===
'template structure': () => ({
task: 'Create a PHTML template',
correctPattern: 'Use $block and $escaper, prefer ViewModels',
example: TEMPLATE_STRUCTURE_EXAMPLE,
avoidPatterns: [
{ pattern: '$this->', reason: 'Deprecated - use $block instead' },
{ pattern: '$this->helper()', reason: 'Use ViewModel instead of helpers' },
{ pattern: 'ObjectManager::getInstance()', reason: 'FORBIDDEN in templates' },
],
explanation: 'Templates should use $block for block methods, $escaper for escaping, and ViewModels for complex logic.',
}),
'create viewmodel': () => ({
task: 'Create a ViewModel',
correctPattern: 'Implement ArgumentInterface',
example: VIEWMODEL_EXAMPLE,
avoidPatterns: [
{ pattern: 'Helper classes in templates', reason: 'Use ViewModel instead' },
{ pattern: 'Complex logic in Block', reason: 'Move to ViewModel for testability' },
],
explanation: 'ViewModels are the preferred way to provide data to templates. They are more testable than Blocks.',
}),
'add viewmodel to layout': () => ({
task: 'Add ViewModel to layout XML',
correctPattern: 'Use argument with xsi:type="object"',
example: LAYOUT_VIEWMODEL_EXAMPLE,
avoidPatterns: [],
explanation: 'Attach ViewModel to block via layout XML arguments.',
}),
// === HTTP CLIENT ===
'http request': () => ({
task: 'Make HTTP request',
correctPattern: 'Magento\\Framework\\HTTP\\ClientInterface',
example: `<?php
use Magento\\Framework\\HTTP\\ClientInterface;
class MyClass
{
private ClientInterface $httpClient;
public function __construct(ClientInterface $httpClient)
{
$this->httpClient = $httpClient;
}
public function fetchData(string $url): string
{
$this->httpClient->get($url);
return $this->httpClient->getBody();
}
}`,
avoidPatterns: [
{ pattern: 'curl_*', reason: 'Use Magento HTTP client' },
{ pattern: 'file_get_contents() for URLs', reason: 'Use Magento HTTP client' },
{ pattern: 'Zend_Http_Client', reason: 'Deprecated - use LaminasClient' },
{ pattern: 'Magento\\Framework\\HTTP\\ZendClient', reason: 'Deprecated - use LaminasClient' },
],
explanation: 'Use Magento HTTP ClientInterface for all external HTTP requests.',
}),
// === JAVASCRIPT / REQUIREJS ===
'requirejs module': () => ({
task: 'Create RequireJS module',
correctPattern: 'Use define() with proper dependencies',
example: MAGENTO_JS_PATTERNS.requirejs_module,
avoidPatterns: [
{ pattern: 'Global variables', reason: 'Use RequireJS module pattern' },
{ pattern: '$.bind()', reason: 'Use $.on() instead' },
],
explanation: 'All JavaScript in Magento should use RequireJS AMD pattern.',
}),
'ui component': () => ({
task: 'Create UI Component',
correctPattern: 'Extend uiComponent',
example: MAGENTO_JS_PATTERNS.uiComponent,
avoidPatterns: [],
explanation: 'UI Components use Knockout.js observables and Magento UI framework.',
}),
'jquery widget': () => ({
task: 'Create jQuery widget',
correctPattern: 'Use $.widget() factory',
example: MAGENTO_JS_PATTERNS.widget,
avoidPatterns: [
{ pattern: 'Direct jQuery plugin', reason: 'Use widget factory for Magento compatibility' },
],
explanation: 'jQuery widgets should use the $.widget() factory pattern.',
}),
// === TRANSLATION ===
'translate string': () => ({
task: 'Translate a string',
correctPattern: '__() function or Phrase class',
example: `<?php
// In PHP:
__('Your order has been placed.');
__('Hello %1', $name);
// With Phrase class:
use Magento\\Framework\\Phrase;
new Phrase('Your order has been placed.');
// In template:
<?= $escaper->escapeHtml(__('Welcome')) ?>
// In JavaScript:
require(['mage/translate'], function($t) {
var text = $t('Add to Cart');
});`,
avoidPatterns: [
{ pattern: '__($constant)', reason: 'First argument must be a string literal, not a constant' },
{ pattern: 'gettext()', reason: 'Use Magento __() function' },
],
explanation: 'Use __() function with string literals. Constants are not allowed as the first argument.',
}),
// === LOGGING ===
'log message': () => ({
task: 'Log a message',
correctPattern: 'Psr\\Log\\LoggerInterface',
example: `<?php
use Psr\\Log\\LoggerInterface;
class MyClass
{
private LoggerInterface $logger;
public function __construct(LoggerInterface $logger)
{
$this->logger = $logger;
}
public function doSomething(): void
{
$this->logger->info('Processing started');
$this->logger->error('An error occurred', ['exception' => $e]);
$this->logger->debug('Debug info', ['data' => $data]);
}
}`,
avoidPatterns: [
{ pattern: 'var_dump()', reason: 'NEVER in production code' },
{ pattern: 'print_r()', reason: 'NEVER in production code' },
{ pattern: 'error_log()', reason: 'Use PSR Logger' },
],
explanation: 'Use PSR LoggerInterface for all logging. Inject via constructor.',
}),
// === DATABASE ===
'database query': () => ({
task: 'Query the database',
correctPattern: 'Use Repositories, Collections, or Resource Models',
example: `<?php
// PREFERRED: Use Repository
use Vendor\\Module\\Api\\ItemRepositoryInterface;
class MyClass
{
private ItemRepositoryInterface $itemRepository;
public function getItem(int $id): ItemInterface
{
return $this->itemRepository->getById($id);
}
}
// Alternative: Use Collection
use Vendor\\Module\\Model\\ResourceModel\\Item\\CollectionFactory;
class MyClass
{
private CollectionFactory $collectionFactory;
public function getItems(): array
{
$collection = $this->collectionFactory->create();
$collection->addFieldToFilter('status', 1);
return $collection->getItems();
}
}`,
avoidPatterns: [
{ pattern: 'Raw SQL queries', reason: 'Use repositories or collections' },
{ pattern: '$connection->query()', reason: 'Use query builder or repositories' },
{ pattern: 'Zend_Db_Select', reason: 'Use Magento\\Framework\\DB\\Select' },
],
explanation: 'Always use Repositories (preferred) or Collections for database operations. Avoid raw SQL.',
}),
};
/**
* Apply active theme pattern overrides to a result.
* If the theme has an override for the matched task keyword, replace the base result.
*/
function applyThemeOverride(baseResult: MagentoPatternResult, matchedKey: string): MagentoPatternResult {
const theme = getActiveTheme();
if (!theme) return baseResult;
const taskLower = matchedKey.toLowerCase();
// Find matching theme override
const override = theme.patternOverrides.find(o =>
taskLower.includes(o.taskKeyword.toLowerCase()) ||
o.taskKeyword.toLowerCase().includes(taskLower)
);
if (!override) {
// No override but theme is active — append theme context
return {
...baseResult,
explanation: `${baseResult.explanation}\n\n⚠️ Active Theme: ${theme.name}\nNote: This is the base Magento pattern. Check if your ${theme.name} theme has different conventions for this task.`,
};
}
// Replace with theme-specific pattern
return {
task: baseResult.task,
correctPattern: override.correctPattern,
example: override.example,
avoidPatterns: [
...override.avoidPatterns,
...baseResult.avoidPatterns,
],
explanation: override.explanation,
relatedPatterns: baseResult.relatedPatterns,
documentation: baseResult.documentation,
};
}
/**
* Find the best matching pattern for a task
*/
export function getMagentoPattern(task: string): MagentoPatternResult {
const taskLower = task.toLowerCase();
// Direct match
for (const [key, getPattern] of Object.entries(TASK_PATTERNS)) {
if (taskLower.includes(key)) {
return applyThemeOverride(getPattern(), key);
}
}
// Keyword matching
const keywords: Record<string, string> = {
'file': 'read file',
'read': 'read file',
'write': 'write file',
'directory': 'create directory',
'folder': 'create directory',
'escape': 'escape html',
'xss': 'escape html',
'html': 'escape html',
'url': 'escape url',
'javascript': 'escape javascript',
'js': 'escape javascript',
'json': 'json encode',
'serialize': 'json encode',
'email': 'validate email',
'template': 'template structure',
'phtml': 'template structure',
'viewmodel': 'create viewmodel',
'view model': 'create viewmodel',
'http': 'http request',
'api': 'http request',
'curl': 'http request',
'requirejs': 'requirejs module',
'amd': 'requirejs module',
'ui component': 'ui component',
'knockout': 'ui component',
'widget': 'jquery widget',
'translate': 'translate string',
'i18n': 'translate string',
'__': 'translate string',
'log': 'log message',
'logger': 'log message',
'debug': 'log message',
'database': 'database query',
'query': 'database query',
'repository': 'database query',
'collection': 'database query',
};
for (const [keyword, patternKey] of Object.entries(keywords)) {
if (taskLower.includes(keyword)) {
const getPattern = TASK_PATTERNS[patternKey];
if (getPattern) {
return applyThemeOverride(getPattern(), patternKey);
}
}
}
// No match found - return generic guidance
return {
task,
correctPattern: 'Use Magento Framework classes with dependency injection',
example: `<?php
// General pattern:
// 1. Inject dependencies via constructor
// 2. Use Magento Framework classes instead of PHP native functions
// 3. Follow PSR-4 autoloading and Magento coding standards
use Magento\\Framework\\...\\RequiredInterface;
class MyClass
{
private RequiredInterface $dependency;
public function __construct(RequiredInterface $dependency)
{
$this->dependency = $dependency;
}
}`,
avoidPatterns: [
{ pattern: 'ObjectManager::getInstance()', reason: 'Always use dependency injection' },
{ pattern: 'new ClassName()', reason: 'Use factories for object creation' },
{ pattern: 'Global functions', reason: 'Use Magento Framework equivalents' },
],
explanation: `Could not find specific pattern for "${task}". Follow these general Magento principles:
1. Use dependency injection for all dependencies
2. Use Magento Framework classes instead of native PHP functions
3. Follow the service contract pattern with interfaces
4. Use repositories for data access
5. Keep business logic in Models/Services, not in Controllers/Blocks`,
};
}