# Magento2 Backend Dev

> |

- **Type:** Skill
- **Install:** `agentstack add skill-ddtcorex-dev-skills-hub-magento2-backend-dev`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [ddtcorex](https://agentstack.voostack.com/s/ddtcorex)
- **Installs:** 0
- **Category:** [Agent Skills](https://agentstack.voostack.com/c/agent-skills)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [ddtcorex](https://github.com/ddtcorex)
- **Source:** https://github.com/ddtcorex/dev-skills-hub/tree/master/skills/magento2-backend-dev

## Install

```sh
agentstack add skill-ddtcorex-dev-skills-hub-magento2-backend-dev
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# Magento 2 Backend Developer

This skill covers API development (REST, SOAP, GraphQL), CLI commands, cron jobs, and message queues.

## Related Skills

**REQUIRED BACKGROUND:** Load `magento2-dev-core` first — it defines the DI, repository, and security patterns (constructor injection, service contracts, escaping, discouraged functions) this skill assumes without repeating.

Pairs with `magento2-security-scan` when the API/resolver you're building touches authentication, ACL, or user input, and with `magento2-performance-audit` for queue/consumer and N+1 concerns once the endpoint is built. In a Govard environment, use `govard-magento` for the CLI/container side (`bin/magento`, cache, indexers).

## REST API

### Service Contract Structure

```
Vendor/Module/
├── Api/
│   ├── ProductRepositoryInterface.php    # Declaration
│   └── Data/
│       └── ProductInterface.php          # Data entity
└── Model/
    └── ProductRepository.php              # Implementation
```

### Data Interface

```php
resource->save($product);
        return $product;
    }

    public function getById(int $id): ProductInterface
    {
        $product = $this->productFactory->create();
        $this->resource->load($product, $id);
        if (!$product->getId()) {
            throw new \Magento\Framework\Exception\NoSuchEntityException(
                __('Product with ID %1 does not exist', $id)
            );
        }
        return $product;
    }

    public function get(SearchCriteriaInterface $searchCriteria): ProductSearchResultsInterface
    {
        $searchResults = $this->searchResultsFactory->create();
        $searchResults->setSearchCriteria($searchCriteria);
        $collection = $this->productCollection->create();
        $this->applySearchCriteria($collection, $searchCriteria);
        $searchResults->setItems($collection->getItems());
        $searchResults->setTotalCount($collection->getSize());
        return $searchResults;
    }
}
```

### WebAPI Configuration

```xml

    
    
        
        
            
        
    

    
    
        
        
            
        
    

    
    
        
        
            
        
    

```

### ACL Configuration

```xml

    
        
            
                
                    
                    
                    
                
            
        
    

```

## GraphQL

### Schema Definition

```graphql
# etc/schema.graphqls

type Query {
    products(filter: ProductFilterInput, pageSize: Int = 20, currentPage: Int = 1): Products
    @doc(description: "Get products list")
    @resolver(class: "Vendor\\Module\\Model\\Resolver\\ProductList")
    @cache(cacheable: false)
}

type Mutation {
    createProduct(input: ProductInput!): Product
    @doc(description: "Create a new product")
    @resolver(class: "Vendor\\Module\\Model\\Resolver\\CreateProduct")
    @cache(cacheable: false)
}

input ProductFilterInput {
    entity_id: FilterTypeInput
    name: FilterTypeInput
    sku: FilterTypeInput
    price: FilterTypeInput
}

type Product {
    entity_id: Int
    name: String
    sku: String
    price: Float
}

input ProductInput {
    name: String!
    sku: String!
    price: Float!
}
```

### Resolver Implementation

```php
searchCriteriaBuilder
            ->setPageSize($args['pageSize'])
            ->setCurrentPage($args['currentPage'] ?? 1)
            ->create();

        $searchResults = $this->productRepository->get($searchCriteria);

        return [
            'total_count' => $searchResults->getTotalCount(),
            'items' => $this->convertProducts($searchResults->getItems())
        ];
    }

    private function convertProducts(array $products): array
    {
        return array_map(function ($product) {
            return [
                'entity_id' => $product->getId(),
                'name' => $product->getName(),
                'sku' => $product->getSku(),
                'price' => $product->getPrice()
            ];
        }, $products);
    }
}
```

For cacheable GraphQL types, implement `IdentityInterface` on the resolver (or a dedicated identity provider) so Magento can tag the response for full-page cache invalidation — without it, `@cache(cacheable: true)` has nothing to key on and the type is effectively never cached correctly.

## CLI Commands

### Command Class

```php
commandName);
    }

    protected function configure(): void
    {
        $this->setDescription($this->commandDescription);
        $this->addOption(
            'dry-run',
            'd',
            InputOption::VALUE_NONE,
            'Run without making changes'
        );
        $this->addOption(
            'limit',
            'l',
            InputOption::VALUE_REQUIRED,
            'Limit number of products',
            100
        );
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $io = new SymfonyStyle($input, $output);
        $io->title('Product Synchronization');

        $limit = (int) $input->getOption('limit');
        $dryRun = $input->getOption('dry-run');

        if ($dryRun) {
            $io->note('Running in dry-run mode');
        }

        try {
            $externalProducts = $this->apiClient->fetchProducts($limit);
            $io->progressStart(count($externalProducts));

            foreach ($externalProducts as $externalProduct) {
                if (!$dryRun) {
                    $this->syncProduct($externalProduct);
                }
                $io->progressAdvance();
            }

            $io->progressFinish();
            $io->success(sprintf('Synchronized %d products', count($externalProducts)));

            return Command::SUCCESS;
        } catch (\Exception $e) {
            $io->error('Synchronization failed: ' . $e->getMessage());
            return Command::FAILURE;
        }
    }
}
```

### Register Command

```xml

    
        
            Vendor\Module\Console\Command\SyncProductsCommand
        
    

state->setAreaCode(Area::AREA_ADMINHTML)` (or `AREA_FRONTEND`/`AREA_GLOBAL`) — a bare CLI command defaults to no area, and area-dependent services throw a `LocalizedException` otherwise.

## Cron Jobs

### Cron Class

```php
logger->info('Running expired product cleanup');

        try {
            $expiredProducts = $this->findExpiredProducts();
            foreach ($expiredProducts as $product) {
                $product->setStatus(Status::STATUS_DISABLED);
                $this->productRepository->save($product);
            }

            $this->logger->info(sprintf('Cleaned up %d expired products', count($expiredProducts)));
        } catch (\Exception $e) {
            $this->logger->error('Cleanup failed: ' . $e->getMessage());
        }
    }
}
```

### Cron Configuration

```xml

    
        
            0 2 * * *
        
    
    
        
            */5 * * * *
        
    

```

### Cron Groups (for large scale)

```xml

    
        1
        4
        2
        10
        1440
        60
        1
    

```

## Message Queue

Not every project needs all four queue XML files — `communication.xml` (topic schema) is the one that's always required. Add `queue_topology.xml`, `queue_publisher.xml`, `queue_consumer.xml` only for what the use case actually needs (e.g. just `queue_publisher.xml` when publishing to a queue a third party already owns).

### Publisher Configuration

```xml

    
        
    

```

### Queue Consumer

```php
handler->process($data);
    }
}
```

### Message Class

```php

    

```

## Pitfalls recap

- The `resource ref` in `webapi.xml` must match an actual `id` declared in `acl.xml` — a typo here fails silently with a 403, not a config error.
- Always throw `NoSuchEntityException` (not return `null`) when a repository can't find an entity — the WebAPI framework maps it to a proper 404.
- A configured consumer (`queue.xml`) does nothing on its own — it must actually be running as a process via cron or a supervisor (`bin/magento queue:consumers:start`), or messages just pile up in the `queue_message` tables. See `magento2-performance-audit`.
- GraphQL resolvers get no automatic ACL check — validate the customer/admin context explicitly inside `resolve()` if the field exposes anything sensitive.

`cron_schedule` rows move through `pending` → `running` → `success`/`error`/`missed`. Never `TRUNCATE cron_schedule` to "fix" a stuck cron — query it (`WHERE status IN ('error','missed')`) to diagnose the actual cause instead, since truncating destroys the run history you'd need to find it.

## Verification

```bash
# Test CLI command
bin/magento vendor:products:sync --dry-run --limit=10

# List registered commands
bin/magento list | grep vendor

# Run cron manually
bin/magento cron:run --group=custom

# Check queue consumers
bin/magento queue:consumers:list

# Start message queue consumer
bin/magento queue:consumers:start vendor.product.update.consumer

# Test REST API
curl -X GET "http://localhost/V1/vendor/product/1" \
     -H "Authorization: Bearer $TOKEN" \
     -H "Content-Type: application/json"

# Clear API cache
bin/magento cache:clean config
```

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [ddtcorex](https://github.com/ddtcorex)
- **Source:** [ddtcorex/dev-skills-hub](https://github.com/ddtcorex/dev-skills-hub)
- **License:** MIT

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** yes
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** no
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-ddtcorex-dev-skills-hub-magento2-backend-dev
- Seller: https://agentstack.voostack.com/s/ddtcorex
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
