Building a Scoped Product Field with PrestaShop 9.2 Properties

36 Views

A useful first Extra Properties integration follows one field from definition to every surface that uses it. This catches a more important class of problems than registering many fields at once: values that save in the wrong shop, appear in the wrong place or disappear during a module lifecycle operation.

The example here is a shop-specific product date used internally by a catalog team. It is a narrow integration exercise for 9.2 RC1, not a complete module tutorial. The official developer notes mark the new system as experimental, so pin and inspect the exact implementation before making a long-term compatibility promise. 9.2 developer notes.

Before implementation, settle the Extra Properties data model; correct integration code cannot repair an ambiguous ownership or translation model.

Use the official example as a versioned reference

Start with demoextrafield in the official example-modules repository. Its source demonstrates registration through registerExtraProperty() and definitions using the Extra Property classes. The repository’s default branch can move; record the commit used for your experiment and compare its constructor arguments with the RC installation. Example source.

The following shell commands are read-only inspection steps inside your already downloaded checkouts. They do not install a module or alter a store:

rg -n 'registerExtraProperty|ExtraPropertyDefinition' demoextrafield
rg -n 'associatedForms|associatedGrids|associatedApis' demoextrafield
rg -n 'unregisterExtraProperty|uninstall' demoextrafield

Run the same kind of search in the candidate core for the definition class and registration method. Check the signature, validation path and return behavior before adapting the sample. This is especially important when an example contains compatibility work newer than the archive being tested.

Do not patch core classes to make an incompatible example load. Either use a matching example revision or adapt only the small integration surface after understanding the difference.

Write a field contract before registration

For this exercise, define a product review date that can differ by shop. It is optional, internal and interpreted as a calendar date rather than an instant in a timezone. A missing value means no date has been assigned. It does not mean today.

That contract immediately determines useful cases: save a date, clear it, reject an invalid value and preserve a different value in another shop. It also prevents an import from substituting a timestamp without an explicit conversion policy.

The structure below is a module-owned specification example, not a PrestaShop registration API. Its purpose is to make the requirements reviewable before mapping them to the verified constructor:

<?php
return [
    'business_name' => 'catalog_review_date',
    'entity' => 'product',
    'value_semantics' => 'calendar_date',
    'varies_by_shop' => true,
    'optional' => true,
    'public_output' => false,
    'owner' => 'catalog_team',
];

Keep this distinction clear in your implementation. Copying a plausible configuration array into a module does not register anything unless you explicitly implement the adapter that consumes it.

Map the contract to the supported definition

The official source imports ExtraPropertyDefinition, ExtraPropertyScope and ExtraPropertyType from PrestaShop\PrestaShop\Core\ExtraProperty\Definition. Use the matching sample’s shop-scoped product date as the integration reference rather than inventing a field type or method name. Registration example.

Adapt one definition at a time. Select the entity, property name, type and scope supported by the candidate. Define the intended administrative associations and visibility settings from the verified signature. Give labels and descriptions a translation domain belonging to your module.

Handle registration failure explicitly in the module’s existing lifecycle logic. A partial installation should not be reported as successful merely because the module record exists. Decide how previously registered definitions are handled if a later registration fails, and test a repeated installation attempt on a disposable database.

Do not assume the headline list of supported entities establishes automatic integration with every form or grid. Entity identity, form identifiers and grid identifiers are separate contracts. An unsupported association needs deliberate integration work, not a guessed identifier.

A focused registration fragment

After confirming that the example’s constructor matches your candidate, this fragment can be incorporated into the existing module class. The imports belong at file scope; the method belongs inside that class. It adapts the official date-field pattern to an internal review field and intentionally does not associate API resources.

use PrestaShop\PrestaShop\Core\ExtraProperty\Definition\ExtraPropertyDefinition;
use PrestaShop\PrestaShop\Core\ExtraProperty\Definition\ExtraPropertyScope;
use PrestaShop\PrestaShop\Core\ExtraProperty\Definition\ExtraPropertyType;
use PrestaShopBundle\Form\Admin\Type\DatePickerType;
use Symfony\Component\Validator\Constraints as Assert;

private function registerCatalogReviewDate(): bool
{
    $definition = new ExtraPropertyDefinition(
        entityName: 'product',
        propertyName: 'catalog_review_date',
        type: ExtraPropertyType::DATE,
        scope: ExtraPropertyScope::SHOP,
        nullable: true,
        displayFront: false,
        associatedForms: ['product'],
        associatedGrids: ['product'],
        formType: DatePickerType::class,
        constraints: [new Assert\Date()],
        labelWording: 'Catalog review date',
        labelDomain: 'Modules.Catalogreview.Admin'
    );

    return $this->registerExtraProperty($definition);
}

The translation domain in this example belongs to a module named catalogreview; use your module’s actual domain. Call the method from its existing installation or update flow and handle a false result according to that flow’s rollback policy. Do not present this fragment as a complete installable module.

SHOP expresses value scope, while displayFront: false expresses presentation intent. Neither replaces the two-shop and exposure tests described above. Registration must be exercised on the matching candidate; this article does not report a runtime test of the fragment.

Verify form persistence and grid behavior separately

Create a product, choose shop A, save a review date and reopen the product. The returned value must equal the submitted date. Repeat with an existing product and with an empty value.

Next, verify invalid input through the supported write path. A browser date picker may prevent ordinary users from typing a malformed value, but that is not proof of server-side validation. Exercise the module’s supported submission or integration route and confirm that an invalid value is rejected without corrupting the prior value.

If the field is associated with a grid, check display, filtering and sorting only where those operations are actually supported. A visible cell does not prove filtering uses the same shop scope as the form. Use two products with distinct dates and an unset value so ordering and empty-value behavior are observable.

Test employee-language changes separately from stored-value changes. Translating a label must not rewrite the date. If you later add a language-scoped property, give its values a separate set of tests rather than treating label translation as sufficient coverage.

Use a two-shop fixture to expose scope mistakes

Associate the test product with shops A and B. Save one date in A and another in B. Reopen each shop independently and inspect any API or export reader that consumes the field.

Then test a product associated with only one shop. An absent association should not be silently treated as permission to use a default shop’s value. Document how the integration responds when a caller selects a scope that does not apply.

Definition availability is another dimension. RC work adds restrictions that can limit a definition to specified shops. Test this independently from per-shop values: first confirm where the field exists, then confirm which value is read within each allowed context. Definition shop restrictions.

An all-shops administrative context deserves its own case. Do not infer whether it should overwrite, display a shared value or require an explicit selection. Read the candidate behavior and decide whether your module can safely expose that operation. If the business action only makes sense for one shop, require one.

Inspect API exposure as a separate contract

A field intended to remain internal should be checked against every response surface you enable. If API exposure is part of the feature, inspect the generated schema and actual response for the installed resource module. Use the candidate’s documented routes; do not derive endpoints from the property name.

Record read and write behavior for a valid value, an empty value and an invalid value. Verify that the correct shop context reaches the entity operation. Also confirm that a client without the parent operation’s permission cannot perform it simply because the value is an extra property.

Avoid treating an API response captured from another module version as universal. Preserve a small contract fixture with the version pair and expected shape. A future update should compare the schema and behavior before regenerating a client.

For storefront access, prove the negative if the field is internal: inspect the rendered page and relevant data structures for accidental exposure. Administrative intent alone does not guarantee absence from every presentation path.

Constraints and removal need upgrade tests

During the RC cycle, the project changed constraint storage toward a DSL representation instead of PHP serialization. That is a reason to use the native definition lifecycle rather than maintaining hand-written serialized values in storage. Constraint persistence change.

Test a module update with existing values. If the field becomes more restrictive, identify records that no longer satisfy the rule before enforcing it. Do not make a routine product edit fail unexpectedly because an old value was never migrated.

Renaming a property is a data migration, not merely a label change. Plan how values move and how readers transition. If external clients consume the property, the change affects their contract as well.

Finally, test disable, re-enable and uninstall independently. A commercial module should have a documented data-retention policy. Export the fixture values, exercise the intended removal behavior and verify what remains. Do not copy a demo’s destructive cleanup solely because it makes a test installation tidy.

The result of this exercise should be a small integration with explicit scope, verified persistence and a known lifecycle. Once that one field behaves correctly, additional properties can reuse the design discipline without repeating the discovery work or relying on unverified API assumptions.

For the underlying platform context, see our PrestaShop 9 module development guide and apply the version-specific checks above to the installation you are evaluating.

When selecting an extension, browse PrestaSuite’s PrestaShop 9 modules and check the documented version support and integration requirements against your own test results.