First implementation of the extra fields management structure#41090
First implementation of the extra fields management structure#41090Jeremie-Kiwik wants to merge 1 commit into
Conversation
|
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) |
| `display_bo` tinyint(1) unsigned NOT NULL DEFAULT 1, | ||
| `display_grid` tinyint(1) unsigned NOT NULL DEFAULT 0, |
There was a problem hiding this comment.
what's the difference between display_bo and display_grid?
There was a problem hiding this comment.
ok, display_bo is a bit vague, don't you think? I know it's a nitpick, but since it's early and we need to think carefuly about the implementation because it will be hard to change it in the future, I'm sharing my feedback
display_form
display_listing
display_api
This is a bit clearer to me. And will the tab you showed in the screenshot be automatically added whenever we have a custom field? I think that won't be the case, right? Because only the product has such capabilities.
I admit that I haven't read all the specifications just yet, I'm sorry, it's probably faster for me to just ask you, I hope you don't mind 🙏🏻
There was a problem hiding this comment.
No problem, it’s quite a massive commit to go through 🙂
For the names, you're right. If you have questions on your first read, then it means it is confusing and needs to be consolidated ^^
So we would have something like this?
display_front: display in FOdisplay_listing: display in BO griddisplay_form: display in BO formdisplay_api: display in APIlisting_position: choose position for BO gridform_position: choose position for BO form
Regarding form positioning, it works for both types of forms, whether they have tabs or not.
In case of tabbed forms:
- by default, it adds an "Extra fields" tab and places the fields inside it
- however, you can choose to place fields in another tab using
property_path - the "Extra fields" tab is only added if there are actually fields to display
To detect whether a form is tabbed, I traverse the form type hierarchy using getInnerType() and check whether one of the parent types is a NavigationTabType.
| `display_api` tinyint(1) unsigned NOT NULL DEFAULT 0, | ||
| `display_bo` tinyint(1) unsigned NOT NULL DEFAULT 1, | ||
| `display_grid` tinyint(1) unsigned NOT NULL DEFAULT 0, | ||
| `grid_position` int(10) unsigned DEFAULT NULL, |
There was a problem hiding this comment.
That one's tricky, I think, would it be computed automatically somehow?
There was a problem hiding this comment.
This makes it easy to quickly filter the scopes in which fields are visible, and helps separate responsibilities. But you're right, it does result in quite a few columns in the database 😅:
- FO
display_frontto display
- BO Grid
display_gridto displaygrid_positionto define its position (optional)
- BO Form
display_boto display/editproperty_pathto define its position (optional)
- Admin API
display_apito display/edit
So we can absolutely imagine a different approach.
This is just a first version, with the advantage of being simple and working without introducing additional complexity 🙂. I'm open to every idea
There was a problem hiding this comment.
@kpodemski I think you are confusing this "position" thing with the principle of ordering elements in a grid right? In which case the "position" field is the one containing the order indexed
Actually here it's the position of the fields inside the columns, based on which columns are already present. Do you see the idea?
For example grid_position = name -> my field will be added after the name column
I understood the naming because I was aware of the feature, but I understand why it's confusing from your point of view Sadaly I don't have a better alternative naming to match what we aim for 🤔 Do you have some?
| | **DB table suffix** | `_extra`, `_extra_lang`, `_extra_shop` | | ||
| | **Column naming** | `{module_name}_{field_name}` | | ||
| | **Column name max length** | 64 characters (MariaDB identifier limit) | | ||
|
|
There was a problem hiding this comment.
No diff here (except blank linces), should be reverted
| | `Common` | `{entity}_extra` | Same value across all shops and languages | | ||
| | `Lang` | `{entity}_extra_lang` | Value varies per language (and per shop if multilang_shop) | | ||
| | `Shop` | `{entity}_extra_shop` | Value varies per shop | | ||
|
|
| `id_extra_property_definition` int(10) unsigned NOT NULL AUTO_INCREMENT, | ||
| `entity_name` varchar(64) NOT NULL, | ||
| `module_name` varchar(64) NOT NULL, | ||
| `module_name` varchar(64) NOT NULL DEFAULT '', |
There was a problem hiding this comment.
Is the empty value for core properties? If so, I think we should use null instead of an empty string to identify the core values
| `validator` varchar(64) DEFAULT NULL, | ||
| `choices` text DEFAULT NULL, | ||
| `api_visible` tinyint(1) unsigned NOT NULL DEFAULT 1, | ||
| `display_front` tinyint(1) unsigned NOT NULL DEFAULT 0, |
There was a problem hiding this comment.
I'm not sure what display_front is used for? I thought the extra fields were added by modules via hooks or template overrides
If display_front is true does it mean the field is displayed automatically in the FO? Where/how would that be?
There was a problem hiding this comment.
We will see later for this filtering feature that raises a lot of questions
| `default_value` varchar(255) DEFAULT NULL, | ||
| `size` int(10) unsigned DEFAULT NULL, | ||
| `validate` varchar(64) DEFAULT NULL, | ||
| `storage_column_name` varchar(64) NOT NULL, |
There was a problem hiding this comment.
Why the extra storage prefix? 🤔 I find it confusing, I'm not even sure to understand what this column is supposed to contain anymore? Is it the DB column name in the extra table?
Isn't it redundant with the field_name ? I though column name were automatically computed with module_name + _ + field_name if this is always the same convention then we don't need to store this info, it can be computed all the times with a simple function
| ### 3.6. ExtraPropertySchemaManager | ||
| > **[impl]** Registry methods return raw associative arrays (not `ExtraPropertyDefinitionCollection` / `ExtraPropertyDefinition` VOs). Three interfaces exist for different read granularities: | ||
| > - `EntityExtraFieldRegistryInterface` — public-facing facade (register/unregister + `getByEntityNameAllScopes()`) | ||
| > - `ExtraPropertyRegistryInterface` — full write + read interface (extends the above) |
There was a problem hiding this comment.
I'm not sure it's a good idea to mix read and write, maybe a dedicated service should handle the write operations
There was a problem hiding this comment.
We should split, at least the interfaces, have the read and the write separated
|
|
||
| > **[impl]** The original proposal returned a flat `['column_name' => value]` map. The actual implementation returns values **grouped by module**: `['_core' => ['field' => value], 'mymodule' => ['field' => value]]`. The method also accepts `primaryKeyName` (not assumed from entity name), `isLangMultishop`, `displayFrontOnly`, and `preloadedDefinitionRows` (to avoid double DB reads when definitions are already loaded). | ||
|
|
||
| **Performance**: reads definitions from the registry (cached); if no definitions exist for the entity, returns an empty array immediately without DB query. |
There was a problem hiding this comment.
So we drop the bulk read for grids?
There was a problem hiding this comment.
Ok handled differently via the query builder, b etter approach
| /** | ||
| * Bulk write: entity values + all lang rows + shop row in one call. | ||
| */ | ||
| public function writeAll( |
There was a problem hiding this comment.
Good call to add something to write them all, especially for the API I guess, which will be mostly automated (not handled via a hook)
But it will probably be used for other use cases as well
|
|
||
| - `getByEntityNameAllScopes(string $entityName): array` — returns raw rows for all scopes | ||
| - `findDefinitionByModuleAndField(string $entity, string $module, string $field, string $scope): ?array` | ||
| - `save(array $definitionData): int` |
There was a problem hiding this comment.
Why a flat array and not a dedicated object?
| # Registry: base implementation + cache decorator | ||
| PrestaShop\PrestaShop\Core\ExtraProperty\Registry\ExtraPropertyRegistry: | ||
| # Registry: base + cache decorator | ||
| PrestaShop\PrestaShop\Adapter\ExtraProperty\ExtraPropertyRegistry: |
There was a problem hiding this comment.
Why put all this in Adapter? It seems legitimate to have it in the Core no?
| `field_type` ENUM('int','bool','string','float','date','html','json','choice') NOT NULL, | ||
| `field_scope` ENUM('common','lang','shop') NOT NULL DEFAULT 'common', | ||
| `symfony_field_type` varchar(255) DEFAULT NULL, | ||
| `property_path` varchar(255) DEFAULT NULL, |
There was a problem hiding this comment.
| `property_path` varchar(255) DEFAULT NULL, | |
| `form_position` varchar(255) DEFAULT NULL, |
| `default_value` varchar(255) DEFAULT NULL, | ||
| `size` int(10) unsigned DEFAULT NULL, | ||
| `validate` varchar(64) DEFAULT NULL, | ||
| `storage_column_name` varchar(64) NOT NULL, |
There was a problem hiding this comment.
| `storage_column_name` varchar(64) NOT NULL, |
| `size` int(10) unsigned DEFAULT NULL, | ||
| `validate` varchar(64) DEFAULT NULL, | ||
| `storage_column_name` varchar(64) NOT NULL, | ||
| `field_type` ENUM('int','bool','string','float','date','html','json','choice') NOT NULL, |
There was a problem hiding this comment.
| `field_type` ENUM('int','bool','string','float','date','html','json','choice') NOT NULL, | |
| `type` ENUM('int','bool','string','float','date','html','json','choice') NOT NULL, |
| `validate` varchar(64) DEFAULT NULL, | ||
| `storage_column_name` varchar(64) NOT NULL, | ||
| `field_type` ENUM('int','bool','string','float','date','html','json','choice') NOT NULL, | ||
| `field_scope` ENUM('common','lang','shop') NOT NULL DEFAULT 'common', |
There was a problem hiding this comment.
| `field_scope` ENUM('common','lang','shop') NOT NULL DEFAULT 'common', | |
| `scope` ENUM('common','lang','shop') NOT NULL DEFAULT 'common', |
| `storage_column_name` varchar(64) NOT NULL, | ||
| `field_type` ENUM('int','bool','string','float','date','html','json','choice') NOT NULL, | ||
| `field_scope` ENUM('common','lang','shop') NOT NULL DEFAULT 'common', | ||
| `symfony_field_type` varchar(255) DEFAULT NULL, |
There was a problem hiding this comment.
| `symfony_field_type` varchar(255) DEFAULT NULL, | |
| `form_field_type` varchar(255) DEFAULT NULL, |
| ```php | ||
| namespace PrestaShop\PrestaShop\Core\ExtraProperty; | ||
|
|
||
| class ExtraPropertyDefinition |
There was a problem hiding this comment.
This object should not be removed, it will be the content of the ExtraPropertyDefinitionCollection, it will be cleanr to have this object instead of a flatten array
| **Cache decorator**: `CachedExtraPropertyRegistry` decorates `ExtraPropertyRegistryInterface`, using Symfony's `cache.app` pool to cache results. It exposes an additional `invalidateCache(): void` method (not part of the interface) for cache invalidation. | ||
|
|
||
| ### 3.6. ExtraPropertySchemaManager | ||
| > **[impl]** Registry methods return raw associative arrays (not `ExtraPropertyDefinitionCollection` / `ExtraPropertyDefinition` VOs). Three interfaces exist for different read granularities: |
There was a problem hiding this comment.
As we said this should contain a ExtraPropertyDefinition object
| ### 3.6. ExtraPropertySchemaManager | ||
| > **[impl]** Registry methods return raw associative arrays (not `ExtraPropertyDefinitionCollection` / `ExtraPropertyDefinition` VOs). Three interfaces exist for different read granularities: | ||
| > - `EntityExtraFieldRegistryInterface` — public-facing facade (register/unregister + `getByEntityNameAllScopes()`) | ||
| > - `ExtraPropertyRegistryInterface` — full write + read interface (extends the above) |
There was a problem hiding this comment.
We should split, at least the interfaces, have the read and the write separated
|
|
||
| > **[impl]** The original proposal returned a flat `['column_name' => value]` map. The actual implementation returns values **grouped by module**: `['_core' => ['field' => value], 'mymodule' => ['field' => value]]`. The method also accepts `primaryKeyName` (not assumed from entity name), `isLangMultishop`, `displayFrontOnly`, and `preloadedDefinitionRows` (to avoid double DB reads when definitions are already loaded). | ||
|
|
||
| **Performance**: reads definitions from the registry (cached); if no definitions exist for the entity, returns an empty array immediately without DB query. |
There was a problem hiding this comment.
Ok handled differently via the query builder, b etter approach
|
I close this-one. Duplicate of #41092 |


See the .md diff to review the differences between the proposed architecture and the initial implementation.