Repository navigation
Handle non-conventional entity naming, default value typing and the cart entity in extra properties - #42538
Conversation
…ry key Extra properties can now target entities whose ObjectModel table breaks the naming convention (PrestaShop#41779): 'order'/'Order'/'orders' and 'combination'/'Combination'/'product_attribute'/'ProductAttribute' all converge on one canonical logical entity name ('order', 'combination'), while the resolved physical table ('orders', 'product_attribute') is persisted in the new table_name registry column and drives every storage concern (extra table names, base-table checks, grid joins, the writer's shop-association lookup). The canonical map fires BEFORE class resolution on purpose: the modern ProductAttribute class is the renamed Attribute entity (table 'attribute'), so class-resolving that spelling would silently store combination properties on the attribute table. An explicit tableName constructor parameter remains as the escape hatch for third-party ObjectModels whose entity name differs from their table. The entity primary key is no longer deduced as id_{entity}: the repository introspects the live extra table's PRI column at hydration (the schema manager mirrored the base table's real PK at creation), with the ObjectModel class then the naming convention as fallbacks — fixing every BO/API surface that used to build the unknown id_orders column (silent defaults on read, exceptions on write). The ObjectModel/FO matching goes through the new filterByTableName(); API resolveId gains logical-name and PK-derived candidates (combination → combinationId, order → orderId); the grid toggle permission subject is derived through the inflector with an override map (order → AdminOrders instead of the 403ing AdminOrderses); the registry refuses two entity names claiming the same storage column (STORAGE_CONFLICT); grids the query modifier must skip (id-first pagination, e.g. the order grid — see PrestaShop#42536) now log a warning instead of silently rendering an empty column. This commit also carries the shared ExtraPropertyValueCaster groundwork (canonical default stringification, JSON encode/decode, writer-side declared-type coercion) that the definition VO, repository, reader and writer interlock with — the next commit completes the default-value scope on top of it. Fixes PrestaShop#41779
Completes the default-value scope (PrestaShop#41829) on the caster groundwork of the previous commit: - the CQRS layer carries the default with its scalar type (AddExtraPropertyDefinitionCommand and EditableExtraPropertyDefinition widen ?string to int|float|string|bool|null, the lossy (string) cast leaves the query handler) — a BOOL false default used to be lost on reload, since its naive stringification produced '' which hydration reads as "no default"; - the BO form boundary stringifies with the same canonical mapping and no longer drops a falsy '0' default; defaultValue stays immutable after creation (disabled BO field, no Update command field); - registration refuses a default that does not fit the declared type (INVALID_DEFAULT_VALUE: non-numeric INT/FLOAT, invalid DATE — which hydration used to erase silently —, CHOICE value outside the enum, invalid JSON); - a row-less entity reads back the declared default on every reader-served surface (FO, BO entity form/grid seeds, Admin API), and the entity create form prefills it. TEXT-backed types (HTML, JSON) cannot carry a DDL DEFAULT clause in MySQL: their default is registry-only and served purely by that read injection; - JSON values are exposed DECODED on reads (the Admin API returns real objects matching its documented type: object schema, FO templates iterate arrays) and re-encoded at the two BO presentation boundaries; the value validator normalizes only that case before running constraints, so Assert\Json sees the encoded string while Assert\Type('bool') still sees a bool; - the OpenAPI schema emits each property's default. Fixes PrestaShop#41829
The cart wiring already worked end to end (PrestaShop#41424): CartLazyArray — the $cart Smarty global of every FO page — exposes the values under its snake_case extra_properties index, and the native ObjectModel path persists them through $cart->save(). This commit pins all of it with an integration test (register on 'cart', ObjectModel write/reload round-trip, presented-cart access, LANG/SHOP scopes cleanly rejected — the cart is a COMMON-only entity, its id_lang/id_shop being plain columns with no cart_lang/cart_shop base table) and closes the one real gap: CartFilter now whitelists extra_properties so the JS prestashop.cart object carries the FO-visible values, consistent with the cart-AJAX update JSON that already did. Cart values die with the cart at checkout: a module needing post-order visibility copies them onto the order at actionValidateOrder — the demoextrafield example module demonstrates the full pattern (cart delivery note seeded on actionCartSave, displayed on the checkout summary, copied to the order, shown on the order detail page). Fixes PrestaShop#41424
Records in the ExtraProperty component context: the logical entity name vs physical table split and its resolution rules (canonical map, class resolution, explicit tableName escape hatch, introspected primary key), the default-value semantics (canonical stringification, registration validation, read-time injection, TEXT-backed types being registry-only), the JSON read/write shape, and the cart as the canonical COMMON-only entity.
Companion of PrestaShop/PrestaShop#42538: the registry now persists the resolved physical entity table (non-conventional naming support), so the upgrade DDL must create the column for shops upgrading to 9.2.0.
Review feedback: the reader/writer entity parameter holds the PHYSICAL table name, so it is now called $tableName across ExtraPropertyReader/Writer, their interfaces (writeAll, deleteAll, deleteForShops), the grid query modifier and the API subscriber — the verbose docblocks warning about the distinction shrink to one line. Also: the redundant AdminOrders override map is gone (the inflector already yields it, the unit test keeps pinning the output), a bool is no longer accepted as an INT default (the BOOL type exists for that), the entity-name help text gains the attribute example, the OpenAPI schema test now covers the emitted defaults (typed per scalar, decoded for JSON, absent when none declared), and two review-flagged comments are trimmed.
Presenting a cart builds the voucher delete URLs, whose CSRF token hashes the context customer (Tools::getToken) — a FO context always carries one, but this test never set it, so every run emitted a silent 'Attempt to read property "id" on null' PHP warning (the Warnings: 2 of every integration totals line). With a KernelTestCase now preceding it in the suite, CI's stricter error handler promotes that warning to an ErrorException and fails the test. Setting the fixture customer fixes the warning at its root; the suite-wide warning count drops to zero.
|
This pull request seems to contain new translation strings. I have summarized them below to ease up review:
(Note: this is an automated message, but answering it will reach a real human) |
|
Hi, thanks for this contribution! I found some issues with the Pull Request description:
Would you mind having a look at it? This will help us understand how interesting your contribution is, thank you very much! (Note: this is an automated message, but answering it will reach a real human) |
| protected readonly LanguageContext $languageContext, | ||
| protected readonly ShopContext $shopContext, | ||
| protected readonly ExtraPropertyDefinitionShopFilterInterface $definitionShopFilter, | ||
| protected readonly ?LoggerInterface $logger = null, |
There was a problem hiding this comment.
Hmm, this one is only used in SF container for the BO so it should always be present no?
| * @internal exposed for unit testing only | ||
| */ | ||
| private static function legacyControllerFromEntityName(string $entityName): string | ||
| public static function legacyControllerFromEntityName(string $entityName): string |
There was a problem hiding this comment.
This shouldn't be a public methods, this shouldn't even be a static one The only reason you made this method accessible is to allow unit testing it but tests shouldn't force the implementation
| public static function legacyControllerFromEntityName(string $entityName): string | |
| protected function legacyControllerFromEntityName(string $entityName): string |
There was a problem hiding this comment.
The method doesn't carry that much knowledge, the unit test can be removed, it will be tested indirectly via UI tests
If you really want to test the method then it should be moved into one of our other classes in the ExtraProperty namespace I don't know which one but ideally one that already exists, because creating a new dedicated class just for this seems overkill
| global $kernel; | ||
| $kernel = self::$kernel; | ||
| Context::getContext()->currency = Currency::getDefaultCurrency(); | ||
| // Presenting a cart builds the voucher delete URLs, whose CSRF token hashes the |
There was a problem hiding this comment.
Maybe we should use the ContextMockerTrait or a class that includes it by default like SymfonyIntegrationTestCase ?
| $this->assertStringContainsString('demoextrafield_reference_code', $common['sql']); | ||
| $this->assertStringContainsString('demoextrafield_is_dangerous', $common['sql']); | ||
| $this->assertSame([7, 'REF-1', true], $common['params']); | ||
| // The writer is the castForDb() choke point: the submitted PHP true is bound as |
There was a problem hiding this comment.
Not sure this extra comment is relevant
| $this->assertSame($expected, CommonController::legacyControllerFromEntityName($entityName)); | ||
| } | ||
|
|
||
| public static function entityNameProvider(): iterable |
There was a problem hiding this comment.
As explained in another comment this simple test shouldn't for the implementation of the method as a public static one, especially since there's not that much intelligence in it
If this test is postponed for later UI tests it's acceptable
There was a problem hiding this comment.
Tested locally on 9.2.x (docker, MySQL 8.4, PHP 8.1). The three areas do what the description says, and I could verify each one end to end. Full local run below, then four points, one of which I would like fixed before merge.
What I verified locally
Naming (#41779). Every spelling converges and the physical storage lands next to the base table:
| registered as | entity | table | extra table | PK |
|---|---|---|---|---|
order / orders / Order |
order |
orders |
orders_extra |
id_order |
combination / ProductAttribute / product_attribute |
combination |
product_attribute |
product_attribute_extra |
id_product_attribute |
cart |
cart |
cart |
cart_extra |
id_cart |
attribute |
attribute |
attribute |
attribute_extra |
id_attribute |
attribute correctly stays on the attribute table, so the ProductAttribute trap you documented is really closed. The introspected PKs match the base tables. Write plus read back through the reader/writer works on orders_extra and product_attribute_extra, so the old id_orders column is gone. Cart LANG and SHOP registrations are refused with the new explicit BASE_TABLE_NOT_FOUND message.
Defaults (#41829). Registry row and DDL both carry the canonical form: BOOL false stores '0' with DEFAULT 0 on the column, INT 0 survives, 1.5 becomes DECIMAL(20,6) DEFAULT 1.500000, and a JSON default stays registry only with COLUMN_DEFAULT NULL on the LONGTEXT, as documented. A row-less product reads back the whole typed set (false, 0, 1.5, the normalised datetime, and the JSON default decoded to an array). Incompatible defaults are refused before any DDL with code 9: abc on INT, not-a-date on DATE, {oops on JSON. A relative DATE default such as tomorrow is normalised to a real datetime rather than reaching MySQL raw, which I had expected to be a hole and is not.
The BO symptom of the issue is fixed on the exact form path: for a BOOL definition defaulting to false, the CQRS layer now carries false and the form data provider yields '0' instead of the empty string that used to read back as "no default". Same for INT 0.
Cart (#41424). Round trip through the ObjectModel bag works (cart_extra row written on update(), value read back on reload). extra_properties in the CartFilter whitelist is in the right alphabetical slot.
Guards. STORAGE_CONFLICT fires with a clear message when an explicit tableName makes a second entity claim an existing column.
Test suites, all green locally: 609 unit, 106 integration (ExtraProperty|CartExtraProperties|CartVoucherSummary|CQRSOpenApiFactory), 22 Behat scenarios / 106 steps in extra-property-definition. CI is green too.
1. [Blocking] hasStorageChanges() ignores tableName, so a table change relocates storage silently
ExtraPropertyRegistry::hasStorageChanges() compares type, scope, size, nullable and enum values, but not getTableName(). Since the STORAGE_CONFLICT guard added in this PR is gated on null === $existingDefinition, re-registering an existing definition with a different explicit tableName passes every check and is treated as non-destructive drift.
Repro on a clean install, module-code side only:
$def = fn (?string $t) => new ExtraPropertyDefinition(
entityName: 'store', propertyName: 'note', type: ExtraPropertyType::STRING,
scope: ExtraPropertyScope::COMMON, labelWording: 'QA', tableName: $t,
);
$registry->register($def(null)); // stores in store_extra
// ... a value is saved in ps_store_extra.note ...
$registry->register($def('supplier')); // accepted, no exceptionResulting state:
ps_extra_property_definition.table_name : store -> supplier
ps_store_extra columns : id_store, note (data still there, now orphaned)
ps_supplier_extra columns : id_supplier, note (new, empty)
definition now reads from : supplier_extra (pk id_supplier)
The stored values are still on disk but no longer reachable, and nothing warned. This is strictly the same class of change as a scope change, which the registry already refuses precisely because it moves the table. Reachability is module code only (there is no BO field for tableName, and entity/type/scope stay read-only), but that explicit parameter is exactly the escape hatch the PR documents for third-party ObjectModels, and a module correcting a wrong tableName on upgrade lands here.
Adding $incoming->getTableName() !== $existing->getTableName() to hasStorageChanges() would close it and reuse the existing DESTRUCTIVE_SCHEMA_CHANGE message, which already tells the caller to unregister and re-register.
2. [Needs a decision] Derived permission subject misses the entities whose grid actually exists
CommonController::legacyControllerFromEntityName() is now 'Admin' . pluralize(classify($entityName)), and the last commit dropped the override map as redundant. It is redundant for order, but three entities that have a real Symfony grid still derive to a tab that does not exist, checked against ps_tab on a fresh install:
| entity | derived subject | real tab | grid exists |
|---|---|---|---|
attribute |
AdminAttributes |
AdminAttributesGroups |
yes (attribute) |
attribute_group |
AdminAttributeGroups |
AdminAttributesGroups |
yes (attribute_group) |
cms_page |
AdminCmsPages |
AdminCmsContent |
yes (cms_page) |
A BOOL extra property placed on one of those grids gets a toggle that is permanently 403, which is the same symptom as the AdminOrderses bug this PR fixes for orders. The docblock calls an unknown subject deny-safe, and that is the right default, but here the deny is silent and permanent rather than a real permission decision.
It is not a regression (the old string-based version derived AdminAttributes too), so I am not asking you to fix the pages themselves in this PR. What tips it for me is that the same commit widens the BO help text to product, customer, order, combination, attribute, so the field now suggests attribute while its toggle cannot work. Either keep a small override map for these three, or drop attribute from the help text and note the limitation next to the #42536 one.
3. [Nit] CONTEXT.md still documents the override map that was removed
.ai/Component/ExtraProperty/CONTEXT.md line 114 describes the toggle subject as override map + pluralize(classify()). Commit f8ffa52 removed the map, so the doc committed in this same PR describes code that no longer exists. Worth a quick edit since that file is the entry point for the component.
4. [Nit] Small inaccuracy in the $tableName docblock
Persisted in the registry (table_name column) so hydration never re-resolves
The constructor calls resolveObjectModelDefinition() unconditionally, before the ?? chain, so hydration does re-resolve and then discards the result. Harmless in practice since getAllDefinitions() is served from the serialized filesystem cache and the constructor only runs on a cache miss, but the sentence claims something the code does not do. A null === $tableName || null === $primaryKeyName short circuit would make it true, or just reword to "the stored value always wins".
Nothing above blocks the naming, defaults or cart work, which all hold up under testing. Point 1 is the one I would like closed before merge; 2 needs a decision more than a fix, and 3 and 4 are one-liners.
|
Recap of my review above, since the blocking point was only stated in prose. Only point 1 blocks the merge.
Everything else held up: the naming, default typing and cart work are verified end to end locally (details in the review), 609 unit / 106 integration / 22 Behat scenarios green, and CI is green. |
…the toggle permission
…nical entity naming
|
Re-tested
My observation (d) from the previous comment is fully closed too: the docblock now describes what the code does, and an unknown definition returns the same 403 as a denied one, so the probe is gone. One new point: the "every regular write" claim only holds on the Admin API
and
The only consumer of On the API path the rule does bite, as intended: The same two values through So both symptoms the comment says are gone survive outside the API: Two ways to square it, your call:
I have no strong preference between the two, I just would not leave the docblock claiming the broad version while the narrow one is what ships. Everything else from my earlier comments still checks out on this head, and the suites stay green locally (unit 682, integration 108, behat 22 scenarios / 106 steps). |
mattgoud
left a comment
There was a problem hiding this comment.
Approving: the four points from my initial review are closed and verified locally on f5a242e, and the naming mapping holds under an exhaustive check (13/13 canonical names, 7/7 canonical tables, 21/21 controller entries against tab.xml).
Suites green locally on this head: unit 682, integration 108, behat 22 scenarios / 106 steps.
What stays open is in my comments above and none of it needs to hold the merge:
- the
isValueCompatible()docblock claiming "every regular write" when only the Admin API path goes through the validator, which is either a reword or awriteAll()follow-up tax_rulemissing from the controller mapcombination→AdminCombinationsto settle against theshipment→AdminOrdersprecedent
Thanks for the two rounds, the table relocation guard and moving the permission subject onto the definition both came out better than what I had suggested.
|
Hello @jolelievre, |
6e1b998
6e1b998 to
83e0bbd
Compare
|
Thanks @mattgoud Good catch on the coverage — two different situations though:
|
|
So the proper solution was NOT to integrate the validation in the deeper level of the writer, because it's too late (core data already persisted at this point), but to correctly include this new default validation properly in all three layers |
|
Correction on my previous comment: the claim was wrong, the docblock was right. Please disregard the "every regular write" point, and in particular do not reword those docblocks on my account.
Both pieces of evidence I posted were bad, which is what let the wrong conclusion through:
With and a real So with The only two gaps left are both already documented behaviour rather than oversights: a direct Sorry for the noise, and for proposing a docblock change that would have made the documentation less accurate than it already was. |
There was a problem hiding this comment.
Re-approving after the push dismissed the previous approvals.
Verified on 83e0bbdc: the type-compatibility rule now lands on all four write surfaces (registration defaults, Admin API, ObjectModel, and the BO form inline through the new constraint), and my objection from earlier today was mine, not the code's, see the correction in the comments.
Suites green locally on this head: unit 685, integration 114, behat 22 scenarios / 106 steps.
Nothing left open on my side. The tax_rule and combination points I had listed here were already fixed in f5a242e (combination → AdminProducts, tax_rule → AdminTaxRulesGroup, plus shop_group/shop_url/feature_flag); I had missed that part of the commit. The controller map now resolves 26/26 against install-dev/data/xml/tab.xml.
|
QA OK Tested on: Automated UI tests: none for this feature ( Steps followedEverything below was done through the back office, following the PR's manual procedure. The 1. Naming (#41779) — created three definitions from Advanced Parameters > Extra Properties, typing the entity name the way a merchant would: Each one normalised to its canonical entity and stored next to the physical base table:
2. Defaults (#41829) — the Read back from the edit form after the round trip (DOM, since a back-office page cannot be curl-ed): {"entity_name": {"value": "combination", "disabled": true},
"type": {"value": "int", "disabled": true},
"default_value": {"value": "0", "disabled": true}}and on the column itself: So A default of 3. Row-less entity reads the declared default — combination #1 has no row at all in 4. Cart (#41424) —
// empty cart, no value stored yet
extra_properties: {"_core": {"qa_cart_note": null}}
// after writing a value on cart 6
extra_properties: {"_core": {"qa_cart_note": "Livraison le vendredi"}}The key also appears in the server-rendered inline JSON on an anonymous request, right where it belongs in the cart object, so it is not a client-side artefact: ResultAll four checks behave as the description says. Before this PR a Regression passFiles changed point at the registry, the reader/writer, the grids and forms, and Coverage note
Environment noteThe database was upgraded in place rather than reinstalled: the two new registry columns ( Disclosure: I also reviewed this PR, so a second pair of eyes on the QA is welcome if the process calls for it. |
|
QA follow-up: the coverage gap from my previous comment is now closed. I installed the demoextrafield companion ( The module registers cleanly, and it exercises the naming map on real module code18 definitions across 9 entities, which is a much better surface than the three I had created by hand. The irregular ones land where they should:
Cart note, on screen at the checkout
extra_properties: {"demoextrafield": {"delivery_note": "Leave the parcel at the pickup point (demo note seeded on 2026-09-07 14:55)."}}Cart to order copy, through the real hookNew cart for the demo customer, note written through Worth noting what this specifically proves: the write and the read both go through Combination defaults on the Admin API
{"combinationId": 1, "extra_demoextrafield_ean_verified": false}
{"combinationId": 2, "extra_demoextrafield_ean_verified": false}
{"combinationId": 3, "extra_demoextrafield_ean_verified": false}A typed boolean I also tried a JSON decoded on readsStored encoded, read back as a real structure: So a Smarty template iterates it, which is what the my-account rendering relies on. What is still not coveredTwo visual renderings only, both needing a logged-in front-office customer session, which I could not open myself:
Their data paths are both verified above (order #6 reads its note back, and the JSON comes back as an array), so what is missing is the template output, not the mechanism. Cheap for anyone with a customer session to confirm in a minute. Verdict unchanged: QA OK. |
|
The last two gaps are closed: both front-office renderings are now verified with a real logged-in customer session. Cart note carried onto the customer's orderOrder So the full chain holds on the customer-visible side, not just in the database: cart value → Customer JSON rendered as a structureThe value was written through the writer as a PHP array (so it went through
The nested URL note for whoever retestsThe order detail page is reachable at Coverage is now completeEvery step of the recommended manual procedure has been run, plus the Admin API default read and the registry/DDL checks from my earlier comments. Verdict stays QA OK. Environment restored afterwards: module uninstalled and removed, the 12 storage tables it leaves behind on purpose ( |
Companion of PrestaShop/PrestaShop#42538, one example per newly supported entity: - cart/delivery_note (COMMON — the cart is a COMMON-only entity): seeded by actionCartSave, displayed on the checkout summary (displayCheckoutSummaryTop reading {$cart.extra_properties.demoextrafield.delivery_note}), and copied onto the order at actionValidateOrder — the intended pattern for cart values that must survive checkout; - order/delivery_note (COMMON), registered with the natural entity name 'order' (the core resolves the orders table and id_order primary key), displayed on the customer's order detail page; deliberately no order-grid placement (id-first pagination limitation, core #42536); - combination/ean_verified (COMMON, exposed on the combinations API list) and combination/restock_note (SHOP), registered as 'combination' (resolved to product_attribute); - the customer extra_json field becomes the JSON showcase: the module writes a PHP structure (encoded by the core for storage) and the shared template iterates the decoded structure reads return; - README refreshed: full entity inventory, the snake_case extra_properties Smarty key (extraProperties is Admin-API-JSON-only), the COMMON-only cart caveat.






orders, Combination →product_attribute), end-to-end typing of values and default values, and the cart entity. Details below.tests/Unit/Core/ExtraProperty/,tests/Integration/Core/ExtraProperty/,tests/Integration/Adapter/Cart/CartExtraPropertiesTest.php,tests/Integration/ApiPlatform/EndPoint/ExtraPropertyEndpointTest.php, behat suiteextra-property-definition.extra_property_definitionregistry — now including thetable_nameandcontroller_namecolumns introduced here — and its_shopassociation table toupgrade/sql/9.2.0.sql); PrestaShop/example-modules#258 (demoextrafield cart/order/combination fixtures — the recommended manual-test vehicle, see below).1. Non-conventional entity naming (#41779)
The entity name stays logical (
order,combination— every spelling of an irregular entity converges on one canonical name), while the resolved physical table (orders,product_attribute) is persisted in a newtable_nameregistry column and drives all storage concerns. Resolution: canonical map → explicittableNameconstructor parameter (escape hatch for third-party ObjectModels) → the entity's own ObjectModel class$definition['table']→ the entity name itself. The canonical map deliberately fires before class resolution: the modernProductAttributeclass is the renamed Attribute entity (tableattribute), so class-resolving that spelling would hijack combination storage.A repository sweep extended the canonical map beyond
orders/product_attribute. Renamed CQRS domains follow the combination rule — the modern name is canonical, the entity being headed for that rename, while the physical table stays the legacy one (mapped in a newCANONICAL_ENTITY_TABLESconst, since no ObjectModel class carries the modern names yet):cart_rule→discount,cms→cms_page,cms_category→cms_page_category,gender→title,order_slip→credit_slip,specific_price_rule→catalog_price_rule,request_sql→sql_request. Table spellings of class↔table disparities converge likeordersdoes (connections→connection,lang→language,webservice_account→webservice_key), and so does the BO grid spellingmerchandise_return→order_return. Re-registering an existing definition with a differenttableNameis now refused as a destructive change (it would silently relocate storage and orphan stored values).The primary key is no longer deduced as
id_{entity}: the repository introspects the live extra table's PK at hydration (the schema manager mirrored the base table's real PK at creation), fixing every BO/API surface that used to build the unknownid_orderscolumn. Also fixed: API item-id resolution (combination→combinationId,order→orderId), and a registry guard refusing two entity names claiming the same storage column. The BO permission subject (used by the grid toggle, and by any future permission check) is now a definition-level concern:ExtraPropertyDefinition::getControllerName()resolves an explicitcontrollerNameregistration override (persisted override-only in the newcontroller_namecolumn — a value matching the deduction collapses to null so deductions keep following core code as BO tabs evolve) → an irregular-tab map covering every migrated grid whose real tab breaks theAdmin+ pluralized-entity convention (attribute→AdminAttributesGroups,cms→AdminCmsContent,order_state→AdminStatuses, …, each verified against the page's own routing_legacy_controller) → the inflector convention (order→AdminOrdersinstead of the always-403AdminOrderses); unknown subjects stay deny-safe.Documented limitation: grids using id-first pagination (the order grid) cannot take extra columns yet — the modifier skips them with a warning log; follow-up in #42536.
2. Default value typing (#41829)
Broadens the scope of #42343 (CQRS-only) to every layer:
falseused to be persisted as''and read back as "no default"); the BO form no longer drops a falsy'0'default;tomorroware never interpreted —, CHOICE outside the enum, invalid JSON). The same rule-set (ExtraPropertyValidator::isValueCompatible()) now also applies implicitly to every regular write (ObjectModel, Admin API, and BO forms via an auto-attachedExtraPropertyTypeCompatibilityconstraint reporting inline field errors), even with zero declared constraints — a value that cannot be a default cannot be stored either ('abc'on an INT used to be silently stored as0); declared constraints keep message priority;castForDb()choke point (form, ObjectModel, Admin API and module writes get identical declared-type coercion); JSON values are stored encoded and exposed decoded on reads — the Admin API returns real objects matching its documentedtype: objectschema and FO templates iterate arrays — with re-encoding at the two BO presentation boundaries;default.3. Cart entity (#41424)
The cart wiring already worked (CartLazyArray exposes
extra_properties— snake_case, the camelCaseextraPropertiesspelling in the issue belongs to the Admin API JSON only). This PR pins it with integration tests, whitelistsextra_propertiesinCartFilterso the JSprestashop.cartobject carries the FO-visible values (the cart-AJAX JSON already did), and documents the cart as a COMMON-only entity (nocart_lang/cart_shopbase table — LANG/SHOP registrations are cleanly rejected). Cart values die at checkout: modules copy them onto the order atactionValidateOrder— the demoextrafield example demonstrates the full pattern.Manual test procedure
Recommended setup: install the demoextrafield companion update — its fixtures exercise all three issues.
cart,orderandcombinationentities; the storage tables sit next to the physical base tables:orders_extra(id_order),product_attribute_extra(id_product_attribute),cart_extra(id_cart).Combination, type Integer, default0: saved ascombination/product_attributewith default0(previously dropped as falsy), shown on the edit form; a defaultabcon an Integer type is refused. A product with no stored value reads the default back (e.g. the module'sean_verifiedisfalseon the combinations Admin API list before any write).