| name | ps-api-endpoint |
| description | Generates new PrestaShop Admin API endpoints (ApiResource PHP class + integration test) for the ps_apiresources module, following CQRS patterns and contribution guidelines. Invoke this skill whenever the user wants to add a new REST endpoint, expose a new entity via the Admin API, contribute a new resource to ps_apiresources, or asks how to wire up a CQRS command/query to an API endpoint in PrestaShop 9. Use it even when the user just says "I want to add X to the API" or "how do I create an endpoint for Y".
|
PrestaShop Admin API Endpoint Generator
This skill walks through creating a new endpoint in ps_apiresources, from discovery to generated code.
Step 1: Gather requirements
Ask the user (all in one message if not already specified):
- Entity name — e.g.
TaxRule, Warehouse (PascalCase, singular)
- Operations needed — GET single, POST (create), PATCH (partial update), PUT (full update), DELETE, GET list (paginated). Also ask about bulk operations (e.g. bulk delete) or custom sub-resource actions (e.g. status toggle).
- PrestaShop core path — absolute path to the PS root, e.g.
/home/user/prestashop-90x. Needed to look up CQRS classes. Offer to skip this step if the user already knows the class names.
Step 2: Discover CQRS classes
Search the PS core for the entity's domain classes. The standard layout is:
{PS_ROOT}/src/Core/Domain/{Entity}/
Command/Add{Entity}Command.php
Command/Edit{Entity}Command.php
Command/Delete{Entity}Command.php
Command/BulkDelete{Entity}Command.php
Query/Get{Entity}ForEditing.php
Query/Get{Entity}ListForEditing.php ← may not exist; Grid is used for lists
QueryResult/{Entity}ForEditing.php ← shows what fields the query returns
Exception/{Entity}NotFoundException.php
Exception/{Entity}ConstraintException.php
Use Glob/Grep to find these files. Then read the QueryResult class (e.g. {Entity}ForEditing.php) — its constructor arguments and getters reveal the exact field names returned by the query. This is the ground truth for QUERY_MAPPING.
Also read the Command constructors to learn the parameter names needed for CQRSCommandMapping.
If some of these classes don't exist (e.g. no Add command), note it — only include operations that have backing CQRS classes.
Step 3: Gather field information
After reading the QueryResult, confirm with the user:
- Which fields should be exposed in the API (not all internal fields need to be public)
- Which fields are localized (arrays keyed by locale like
'en-US' => 'value') → need #[LocalizedValue]
- Which localized fields are required in the default language on create → need
#[DefaultLanguage(groups: ['Create'], fieldName: '...')]
- Which localized fields are optional on update →
#[DefaultLanguage(groups: ['Update'], fieldName: '...', allowNull: true)]
Step 4: Generate the ApiResource class
File location: src/ApiPlatform/Resources/{Entity}/{Entity}.php
Use the template below. Adapt based on the selected operations.
<?php
namespace PrestaShop\Module\APIResources\ApiPlatform\Resources\{Entity};
use ApiPlatform\Metadata\ApiProperty;
use ApiPlatform\Metadata\ApiResource;
use PrestaShop\PrestaShop\Core\Domain\{Entity}\Command\Add{Entity}Command;
use PrestaShop\PrestaShop\Core\Domain\{Entity}\Command\Edit{Entity}Command;
use \\\\{}\\{};
\\\\{}\\{};
\\\\{}\\{};
\\\\{}\\{};
\\\;
\\\;
\\\;
\\\;
\\\;
\\\;
\\\ ;
(
: [
(
: ,
: Get{Entity}::,
: [],
: self::,
),
(
: ,
: [ => [, ]],
: Add{Entity}::,
: Get{Entity}::,
: [],
: self::,
: self::,
),
(
: ,
: [ => [, ]],
: Edit{Entity}::,
: Get{Entity}::,
: [],
: self::,
: self::,
),
(
: ,
: [ => ],
: Delete{Entity}::,
: [],
),
],
: [
{Entity}:: => ::,
{Entity}:: => ::,
],
)
{Entity}
{
(: )
${entityId};
= [
];
= [
];
= [
];
}
Key rules while filling in the template
See references/conventions.md for the full ruleset. The most important ones:
- URI: plural, lowercase, kebab-case.
featureId in URI, not id. E.g. /tax-rules/{taxRuleId}.
- Scopes:
{entity_snake_case}_read and {entity_snake_case}_write. Multi-word entities: tax_rule_read.
- Boolean properties: no
is prefix. Use $enabled, not $isEnabled.
- Localized properties: no "localized" prefix. Use
$names, not $localizedNames. Mark with #[LocalizedValue].
- Mapping:
QUERY_MAPPING maps [queryResultFieldName] => [apiFieldName]. CREATE_COMMAND_MAPPING maps [apiFieldName] => [commandParamName]. When Create and Update commands share the same param names, use a single COMMAND_MAPPING constant.
- Forbidden: no custom normalizers, no custom processors, no Value Objects as properties (only scalar types and arrays).
- Strict typing: every property needs an explicit type.
List endpoints (PaginatedList)
List operations live in a separate class file named {Entity}List.php next to the main resource (e.g. src/ApiPlatform/Resources/Contact/ContactList.php, src/ApiPlatform/Resources/Attribute/AttributeGroupList.php). Use PaginatedList with the Grid's data factory service and a filtersClass. The URI is the plural base path (e.g. /contacts) without an ID. Field mapping is done via ApiResourceMapping.
Bulk operations
Bulk operations also live in a separate class file named Bulk{Entities}.php (or BulkDelete{Entities}.php / BulkUpdateStatus{Entities}.php when more specific) next to the main resource. Real examples in the repo:
src/ApiPlatform/Resources/Attribute/BulkAttributeGroups.php
src/ApiPlatform/Resources/Category/BulkDeleteCategories.php
src/ApiPlatform/Resources/Category/BulkUpdateStatusCategories.php
URI uses bulk- prefix + plural: /tax-rules/bulk-delete. The single public property is the array of IDs, named with the singular entity + "Ids" (e.g. $taxRuleIds, $attributeGroupIds), typed as array, and annotated with #[ApiProperty(openapiContext: ['type' => 'array', 'items' => ['type' => 'integer']])] and #[Assert\NotBlank].
Step 5: Generate the integration test
File location: tests/Integration/ApiPlatform/{Entity}EndpointTest.php
<?php
declare(strict_types=1);
namespace PsApiResourcesTest\Integration\ApiPlatform;
use Symfony\Component\HttpFoundation\Response;
use Tests\Resources\DatabaseDump;
use Tests\Resources\Resetter\LanguageResetter;
class {Entity}EndpointTest extends ApiTestCase
{
public static function setUpBeforeClass(): void
{
parent::setUpBeforeClass();
LanguageResetter::resetLanguages();
self::addLanguageByLocale('fr-FR');
self::resetTables();
self::createApiClient(['{entity}_read', '{entity}_write']);
}
public static function tearDownAfterClass(): void
{
::();
::();
::();
}
{
::([
, // add all related tables
, // entity has localized fields
, // entity has shop association
]);
}
{
=> [, ];
=> [, ];
=> [, ];
=> [, ];
=> [, ];
}
{Entity}():
{
= [
];
= ->(, , []);
->(, );
[];
}
{Entity}( ${entityId}):
{
= ->( . ${entityId}, []);
->(${entityId}, []);
${entityId};
}
{Entity}( ${entityId}):
{
= [
];
= ->( . ${entityId}, , []);
= ->( . ${entityId}, []);
${entityId};
}
{Entity}( ${entityId}):
{
->( . ${entityId}, []);
->( . ${entityId}, [], ::);
}
{Entity}():
{
= [
];
= ->(
,
,
[],
::
);
->([
[ => , => ],
], );
}
}
Test rules
- Tests must assert complete response data — all fields, not just the identifier.
- Chain tests using
@depends so each test builds on the created entity.
- Always include a
testInvalid* test that covers validation constraints.
- Include
'skip_null_values' => false in assertions when using assertEquals on the full response array.
- Drop
LanguageResetter::resetLanguages() from setUp/tearDown if the entity has no localized fields.
Step 6: Write the files
Create both files at their correct paths. Then tell the user:
- Where the files were written
- How to register the resource — if the ApiResource isn't auto-discovered, a service definition may be needed (usually not required for standard resources)
- How to run the tests:
composer setup-local-tests
composer run-module-tests
- What to double-check: mappings between CQRS fields and API fields — these are the most common source of errors. Ask the user to run the endpoint manually via Swagger UI to verify.
Common pitfalls to flag
- If the
Add*Command takes its ID from an EntityId value object in the result (not a raw int), the CQRSCreate block may need to fetch the result via a separate CQRSQuery after creation.
- If a command and its edit counterpart share the same constructor signature, use one
COMMAND_MAPPING constant.
- For entities without shop association, omit
shopIds and its mapping.
#[DefaultLanguage] needs the fieldName argument set to the API field name (e.g. fieldName: 'names'), not the internal query field name.
Reference files
references/conventions.md — full naming and structural conventions
../../../CONTEXT.md (repo root) — module-wide AI context: purpose, architecture, Do/Don't, canonical examples. This skill must stay aligned with it.