
Properly extending Prestashop Objects
Extending Prestashop Objects in the proper way can be a hit-and-miss process because of the uneven core structure. In this tutorial we will add a whole new custom field to the product class and see how to make it editable from the back office.
Download Project Files
Introduction
Prestashop offers quite enough options to satisfy most of the e-merchants. However, there are situations where we really need an extra field for a type of object, and we can’t achieve the result with the different base funcionalities. In today’s tutorial we will be adding a new custom field to the product object, and then display it in the product page in the front office. Not only that, but we will also see how to make the field editable from the back office (and the exceptions beyond the product object type).
We will basically do this:
- Add the proper database field
- Add the Product Class override
- Edit the Product admin page template
- Edit the product.tpl file of our theme to display the new field
Note that Prestashop allows you to override without the need of adding your files manually, by simply creating your overrides in a module’s folder, it would add them automatically when the module is installed. However, I find the automated process to cause more troubles than benefits, therefore I’ll go for a manual override
Step 1 – Database and Product Override
Let’s start off by adding the new field in the database. You can use any tool you prefer, I will go for phpmyAdmin.
Access your Prestashop database and locate the *prefix*product table. The prifix should be ps_, if you didn’t change it. I will stay generic, so I will add a simple test field to hold any kind of content. We will see how to create translatable fields later on in the tutorial. Create a new column with the following settings:
- Name: extrafield
- Type: text
Notice that the column name should reflect the name of the variable you will assign to the product in the class override. Keep this in mind when you ad your own fields.
Now that we’re done with the table, let’s move to the override. Create a new file named Product.php (with the capital P) in override/classes. Open it up in a code editor and add the following snippet:
<?php Class Product extends ProductCore { } ?>
Note: I added the closing php tag for my syntax highliter to work properly, but you can feel free to leave it out.
This is the usual way to override an object in Prestashop: ObjectName extends ObjectNameCore. Not to hard to remember, is it?
At this point you’ll need to erase the class_index.php file located in the cache/ folder. This wouldn’t have been necessary if we used an auto override generated by a module
To be sure the override is working, add some garbage text like ‘sdfafgyusar’ inside that file so that you’re sure it will break, and reload the Prestashop page. If it breaks, the override is being loaded.
Get rid of the garbage text and instead chenge your class as follows:
<?php Class Product extends ProductCore { public $extrafield; public function __construct($id_product = null, $full = false, $id_lang = null, $id_shop = null, Context $context = null) { self::$definition['fields']['extrafield'] = array('type' => self::TYPE_STRING, 'validate' => 'isGenericName', 'size' => 64); parent::__construct($id_product, $full, $id_lang, $id_shop, $context); } } ?>
Explanation: What we are doing here is basically letting the class file know it has a new database column to take into consideration. Note that if you don’t need to edit the field in the back office, the construct part is not needed as it’s meant to validate the input field when adding it in the Product admin panel. We are telling Prestashop it must be a generic string, and it can be no longer than 64 characters. You can leave the validate and size parts away, if you want, but always be sure the extra field is added to the definitions list before calling the parent constructor. Also, pay attention at the original class contructor as that and yours must have the same arguments, or at least the same ones. This means you can add another optional parameter, but the original ones must be there
Here are the various types a property can be
- TYPE_INT
- TYPE_BOOL
- TYPE_STRING
- TYPE_FLOAT
- TYPE_DATE
- TYPE_HTML
- TYPE_NOTHING
If you want to add a whole object as property (in a case like categories or manufacturers) you can also use the HAS_ONE and HAS_MANY object types. We won’t dive deep into this kind of extension, at the moment.
As for the validation, there are many things you can check the input against. For a complete list you can check all the methods of the Validate class.
Step 2 – Creating the new Back Office input
Okay, we have our field. And as I said that’s it if you are happy with changing the value via CSV or every time in the database. Since this is not the case in most of the situations, we now need a user friendly way to edit the new field. This is where editing the back office template becomes necessary. BUT be careful: this is not always the case. It’s true for products, since the interface of this type of object is built with .tpls like the front office. However, many other objects (I would say, basically all of them) don’t make extensive use of templates, but relay on controller to manage the way they display data. We will see how to deal with this kind of exception later on in the tut.
For now, reach the admin folder, then go to /themes/default/template/controllers/products. In this folder, you’ll see a bunch of tpl files whose names are equal to the name of the sections the product page is divided into. For simplicity, I will choose the first one, informations. Copy informations.tpl and go back to the main folder. Now access override/controllers/admin/templates/. We need to recreate the controller name folder structure inside, in our case, simply products. THe final directory structure is override/controllers/admin/templates/products/. Once you’re done, paste the information template file inside.
Open it, then locate the following snippet
<tr> <td class="col-left"><label>{$bullet_common_field} {l s='UPC:'}</label></td> <td style="padding-bottom:5px;"> <input size="55" maxlength="12" type="text" name="upc" value="{$product->upc|escape:html:'UTF-8'}" style="width: 130px; margin-right: 5px;" /> <span class="small">{l s='(US, Canada)'}</span> </td> </tr>
It’s the one that displays the UPC code for the product. As we are creating a non-translatable generic field, a good position for it would be after the UPC code input. Therefore, after the previous table row, add this one (before the table closing tag of course!)
<tr> <td class="col-left"><label>{$bullet_common_field} {l s='Extra Field:'}</label></td> <td style="padding-bottom:5px;"> <input size="55" maxlength="12" type="text" name="extrafield" value="{$product->extrafield|escape:html:'UTF-8'}" style="width: 130px; margin-right: 5px;" /> <span class="small">{l s='(Our Extra FIeld)'}</span> </td> </tr>
Explanation: we are basically replicating the UPC box, with some adjustments to display that field ($product->extrafield instead of $product->upc). Pay attention to the name of the input as it must be equal to the name of the object property we used when defining the class and constructor.
And that’s basically it, add some content and save it, it will be added in the database!
Step 3 – Displaying the new Property in the template
After all we did, this part will be a piece of cake. It couldn’t be easier: open up product.tpl located in your theme’s folder, and add the following snippet where you want the extra field to appear:
<p>{l s='Our extra field'}: {$product->extrafield}</p>
Piece o’cake! Now let’s move to something trickier, creating a translatable field
Part 4 – Translatable fields
The process of creating a translatable field instead of a generic one slightly differs from what we did before, but not that much. Instead of adding the new column to the product table, let’s add it to product_lang with the following properties:
- Name: extra_description
- Type: text
Open up our beloved product override again, and change it as follows:
<?php Class Product extends ProductCore { public $extrafield; public $extra_description; public function __construct($id_product = null, $full = false, $id_lang = null, $id_shop = null, Context $context = null) { self::$definition['fields']['extrafield'] = array('type' => self::TYPE_STRING, 'validate' => 'isReference', 'size' => 64); self::$definition['fields']['extra_description'] = array('type' => self::TYPE_STRING, 'validate' => 'isGenericName', 'size' => 64, 'lang' => true, 'required' => true); parent::__construct($id_product, $full, $id_lang, $id_shop, $context); } } ?>
Just some slight modifications as you can see. We added the property the same way, but then slightly modified it’s definition: we added ‘lang=>true’ to let Prestashop know this field must be taken from the product language table, and it’s required when saving the product.
Now onto adding it to the back office. Same informations.tpl file we used before, but this time the right place is after the product name. Here’s how that table row reads:
<tr> <td class="col-left"> {include file="controllers/products/multishop/checkbox.tpl" field="name" type="default" multilang="true"} <label>{l s='Name:'}</label> </td> <td style="padding-bottom:5px;" class="translatable"> {foreach from=$languages item=language} <div class="lang_{$language.id_lang}" style="{if !$language.is_default}display: none;{/if} float: left;"> <input class="{$class_input_ajax}{if !$product->id}copy2friendlyUrl{/if} updateCurrentText" size="43" type="text" {if !$product->id}disabled="disabled"{/if} id="name_{$language.id_lang}" name="name_{$language.id_lang}" value="{$product->name[$language.id_lang]|htmlentitiesUTF8|default:''}"/><sup> *</sup> <span class="hint" name="help_box">{l s='Invalid characters:'} <>;=#{}<span class="hint-pointer"> </span> </span> </div> {/foreach} </td> </tr>
Right after it, add:
<tr> <td class="col-left"> {include file="controllers/products/multishop/checkbox.tpl" field="name" type="default" multilang="true"} <label>{l s='Extra Description:'}</label> </td> <td style="padding-bottom:5px;" class="translatable"> {foreach from=$languages item=language} <div class="lang_{$language.id_lang}" style="{if !$language.is_default}display: none;{/if} float: left;"> <input size="43" type="text" {if !$product->id}disabled="disabled"{/if} id="extra_description_{$language.id_lang}" name="extra_description_{$language.id_lang}" value="{$product->extra_description[$language.id_lang]|htmlentitiesUTF8|default:''}"/><sup> *</sup> </span> </div> {/foreach} </td> </tr>
Explanation: A couple of things to notice: first:
<td class="col-left"> {include file="controllers/products/multishop/checkbox.tpl" field="name" type="default" multilang="true"} <label>{l s='Extra Description:'}</label> </td>
See the checkbox.tpl inclusion? This is useful for multishop environments, and allows you to have a different text value for each of your shops.
Next:
<td style="padding-bottom:5px;" class="translatable"> {foreach from=$languages item=language} <div class="lang_{$language.id_lang}" style="{if !$language.is_default}display: none;{/if} float: left;"> <input size="43" type="text" {if !$product->id}disabled="disabled"{/if} id="extra_description_{$language.id_lang}" name="extra_description_{$language.id_lang}" value="{$product->extra_description[$language.id_lang]|htmlentitiesUTF8|default:''}"/><sup> *</sup> </span> </div> {/foreach} </td>
In this table cell, as we need to display one field for each language, we need to go through all of the languages set, and display the input accordingly. Notice that the name and id of the input field MUST be in this format: ‘fieldname_{$language.id_lang}’ so that it is unique for each language. Type something & save.
And what about displaying it? Well, it’s the same as the previous property, simply add this in product.tpl:
<p>{$product->extra_description}</p>
Adding extra fields to other Object types
As I mentioned at the beginning of the tutorial, products use a different input system for the back office, compared to all the other object types. We will now use categories as an example of inputting extra properties for these objects.
First of all, create the new database column for the category table as we did before. Use any name you like, I will call it extravar and it will be, again, a text field.
Now add the category override following the same steps used for the product, or download and add the Category.php override file you find in the project archive. Then, we are ready to start.
The Admin*ObjectName*Controller
These other objects’ forms in the back office are rendered using controllers, therefore we would need another override. BUT because of Prestashop’s fault, you can’t directly extend the method used to render the form. We could add an extra field by using a dedicated variable, but for some weird reason it only allows us to add one single field. Hopefully, it will get fixed soon. Therefore, despite being bad practice, we need to grab the entire renderForm method from the original controller and paste it into our new override with some slight modifications. Go to controllers/admin/ and open the file named AdminCategoriesController.php
Copy the whole renderForm() method. Now go to override/controllers/admin and create a new file named AdminCategoriesController.php. Inside it, add the class declaration:
<?php Class AdminCategoriesController extends AdminCategoriesControllerCore { } ?>
Then, paste the renderForm method:
<?php Class AdminCategoriesController extends AdminCategoriesControllerCore { public function renderForm() { $this->initToolbar(); $obj = $this->loadObject(true); $id_shop = Context::getContext()->shop->id; $selected_cat = array((isset($obj->id_parent) && $obj->isParentCategoryAvailable($id_shop))? (int)$obj->id_parent : (int)Tools::getValue('id_parent', Category::getRootCategory()->id)); $unidentified = new Group(Configuration::get('PS_UNIDENTIFIED_GROUP')); $guest = new Group(Configuration::get('PS_GUEST_GROUP')); $default = new Group(Configuration::get('PS_CUSTOMER_GROUP')); $unidentified_group_information = sprintf($this->l('%s - All people without a valid customer account.'), '<b>'.$unidentified->name[$this->context->language->id].'</b>'); $guest_group_information = sprintf($this->l('%s - Customer who placed an order with the guest checkout.'), '<b>'.$guest->name[$this->context->language->id].'</b>'); $default_group_information = sprintf($this->l('%s - All people who have created an account on this site.'), '<b>'.$default->name[$this->context->language->id].'</b>'); $root_category = Category::getRootCategory(); $root_category = array('id_category' => $root_category->id, 'name' => $root_category->name); $this->fields_form = array( 'tinymce' => true, 'legend' => array( 'title' => $this->l('Category'), 'image' => '../img/admin/tab-categories.gif' ), 'input' => array( array( 'type' => 'text', 'label' => $this->l('Name:'), 'name' => 'name', 'lang' => true, 'size' => 48, 'required' => true, 'class' => 'copy2friendlyUrl', 'hint' => $this->l('Invalid characters:').' <>;=#{}', ), array( 'type' => 'radio', 'label' => $this->l('Displayed:'), 'name' => 'active', 'required' => false, 'class' => 't', 'is_bool' => true, 'values' => array( array( 'id' => 'active_on', 'value' => 1, 'label' => $this->l('Enabled') ), array( 'id' => 'active_off', 'value' => 0, 'label' => $this->l('Disabled') ) ) ), array( 'type' => 'categories', 'label' => $this->l('Parent category:'), 'name' => 'id_parent', 'values' => array( 'trads' => array( 'Root' => $root_category, 'selected' => $this->l('Selected'), 'Collapse All' => $this->l('Collapse All'), 'Expand All' => $this->l('Expand All') ), 'selected_cat' => $selected_cat, 'input_name' => 'id_parent', 'use_radio' => true, 'use_search' => false, 'disabled_categories' => array(4), 'top_category' => Category::getTopCategory(), 'use_context' => true, ) ), array( 'type' => 'radio', 'label' => $this->l('Root Category:'), 'name' => 'is_root_category', 'required' => false, 'is_bool' => true, 'class' => 't', 'values' => array( array( 'id' => 'is_root_on', 'value' => 1, 'label' => $this->l('Yes') ), array( 'id' => 'is_root_off', 'value' => 0, 'label' => $this->l('No') ) ) ), array( 'type' => 'textarea', 'label' => $this->l('Description:'), 'name' => 'description', 'lang' => true, 'rows' => 10, 'cols' => 100, 'hint' => $this->l('Invalid characters:').' <>;=#{}' ), array( 'type' => 'file', 'label' => $this->l('Image:'), 'name' => 'image', 'display_image' => true, 'desc' => $this->l('Upload a category logo from your computer.') ), array( 'type' => 'text', 'label' => $this->l('Meta title:'), 'name' => 'meta_title', 'lang' => true, 'hint' => $this->l('Forbidden characters:').' <>;=#{}' ), array( 'type' => 'text', 'label' => $this->l('Meta description:'), 'name' => 'meta_description', 'lang' => true, 'hint' => $this->l('Forbidden characters:').' <>;=#{}' ), array( 'type' => 'tags', 'label' => $this->l('Meta keywords:'), 'name' => 'meta_keywords', 'lang' => true, 'hint' => $this->l('Forbidden characters:').' <>;=#{}', 'desc' => $this->l('To add "tags," click in the field, write something, and then press "Enter."') ), array( 'type' => 'text', 'label' => $this->l('Friendly URL:'), 'name' => 'link_rewrite', 'lang' => true, 'required' => true, 'hint' => $this->l('Only letters and the minus (-) character are allowed.') ), array( 'type' => 'group', 'label' => $this->l('Group access:'), 'name' => 'groupBox', 'values' => Group::getGroups(Context::getContext()->language->id), 'info_introduction' => $this->l('You now have three default customer groups.'), 'unidentified' => $unidentified_group_information, 'guest' => $guest_group_information, 'customer' => $default_group_information, 'desc' => $this->l('Mark all of the customer groups you;d like to have access to this category.') ) ), 'submit' => array( 'title' => $this->l('Save'), 'class' => 'button' ) ); $this->tpl_form_vars['shared_category'] = Validate::isLoadedObject($obj) && $obj->hasMultishopEntries(); $this->tpl_form_vars['PS_ALLOW_ACCENTED_CHARS_URL'] = (int)Configuration::get('PS_ALLOW_ACCENTED_CHARS_URL'); if (Shop::isFeatureActive()) $this->fields_form['input'][] = array( 'type' => 'shop', 'label' => $this->l('Shop association:'), 'name' => 'checkBoxShopAsso', ); // remove category tree and radio button "is_root_category" if this category has the root category as parent category to avoid any conflict if ($this->_category->id_parent == Category::getTopCategory()->id && Tools::isSubmit('updatecategory')) foreach ($this->fields_form['input'] as $k => $input) if (in_array($input['name'], array('id_parent', 'is_root_category'))) unset($this->fields_form['input'][$k]); if (Tools::isSubmit('add'.$this->table.'root')) unset($this->fields_form['input'][2],$this->fields_form['input'][3]); if (!($obj = $this->loadObject(true))) return; $image = ImageManager::thumbnail(_PS_CAT_IMG_DIR_.'/'.$obj->id.'.jpg', $this->table.'_'.(int)$obj->id.'.'.$this->imageType, 350, $this->imageType, true); $this->fields_value = array( 'image' => $image ? $image : false, 'size' => $image ? filesize(_PS_CAT_IMG_DIR_.'/'.$obj->id.'.jpg') / 1000 : false ); // Added values of object Group $category_groups_ids = $obj->getGroups(); $groups = Group::getGroups($this->context->language->id); // if empty $carrier_groups_ids : object creation : we set the default groups if (empty($category_groups_ids)) { $preselected = array(Configuration::get('PS_UNIDENTIFIED_GROUP'), Configuration::get('PS_GUEST_GROUP'), Configuration::get('PS_CUSTOMER_GROUP')); $category_groups_ids = array_merge($category_groups_ids, $preselected); } foreach ($groups as $group) $this->fields_value['groupBox_'.$group['id_group']] = Tools::getValue('groupBox_'.$group['id_group'], (in_array($group['id_group'], $category_groups_ids))); return AdminController::renderForm(); } } ?>
Massive, but no fear we only need to amend it slightly. First of all, change the last line to
return AdminController::renderForm();
This way, we are bypassing the original categories controller call, which would otherwise cause the form to appear two times. Next, let’s amend the fields_form array. If you look at the whole method you’ll understand why we can’t extend it: if we add something to the array from the extension class, then call the parent, the new parameter will never see the light as the array is being recreated from scratch. If we place the constructor before, the content will be outputted before we add the parameter. That’s why we need to use this kind of trick. It’s not optimal as we bypass the direct parent, but at the moment it’s the only way to go.
In any case, add this new array to the fields_form fields list, right after the category name.
array( 'type' => 'text', 'label' => $this->l('Our new field:'), 'name' => 'extravar', 'size' => 128 ),
And that’s basically it. If you add a languaged field like we did for the product, you’d only need to specify lang => true in the array of the new field, and the form helper will take cart of displaying all the proper HTML. At this point, you can display it in category.tpl as we did with the product extension.
Conclusion
As you saw, extending a Prestashop class object is not terribly difficult. There are still some flaws with most of the controller-driven forms, but hopefully the Prestateam will fix them, so that we can use a simpler method in the next releases