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">&nbsp;</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

Additional Resources

You like the tuts and want to say "thank you"? Well, you can always feel free to donate:

  • Firoz

    Hi i am create a code on controller/front/Catagorycontroller .. Code like function int(){
    ….

    $client = new SoapClient(‘info.wso.xml’);
    $result = $client->TopGoalScorers(array(‘iTopN’ => $searchDate));
    $array = $result->TopGoalScorersResult->tTopGoalScorer;
    if($searchDate) {
    print ”

    Rank
    Name
    Goals
    Flag

    “;
    // print_r($array);
    foreach($array as $k=>$v){
    print ”

    ” . ($k+1) . ”
    ” . $v->sName . ”
    ” . $v->iGoals . ”
    sFlag.”>
    “;
    }
    print “”;
    }
    ….
    ….
    }

    Bt when its view on category tpl page i can’t handle it
    now i want to write this html code on Category.tpl page … how.. plz help

  • Max

    Hi, i have the same issue :(

    Need help with combinations override

  • Kubus

    Hi,

    thanks for great tutorial! Im a little noob :) the new category field works for me perfect, but I cannot find the *.tpl file in admin where I can ad this new var so I can edit from BO? Can you please advice?

    Thanks!

  • Stef

    Hi Nemo

    First thanks for this very good tutorial. It run perfectly with a new field in informations tpl.
    I try to do the same thing with the combinations but with no success.
    I override classe/combination and admin/templates/products/combinations.tpl
    In BO i see my new field but when i enter a value and i save it nothing happened in the data base.
    So i enter the value directly in the Data Base and when i look my new field in the bo, i don’t see the value.

    Do you already try your method with combination ?

    Hope, you’ll understand my english !!!

  • taseaford

    Hey Nemo!

    My shop is primarily selling books so I need to add a product field for “Author.” I have successfully added a column in the database and it is displaying the information in the FO. I have also added a field for the value to be edited via the BO. But I seem to be having trouble getting the BO to connect to the database to change the value.

    I have refreshed the cache.
    I can retrieve data from other tables using the same code.
    I have verified that my column name matches what I have in my code.
    The same code that retrieves the information in the FO will not work in the BO.

    Any suggestions?

  • http://www.tecnolongia.org Jaume

    Great tutorial, Nemo! Thank you very much!

  • http://www.marmolys.fr Marmolys

    Hi,

    Thanks for the tutorial.
    I’m trying to figure it out if Prestashop is the right solution for our needs because we have to add custom fields to “stores” and, after reading this post, I’m less worried about this matter.

  • Nils

    Thank you for a nice demo. I wonder, if you add a custom field will it automatically be added to the web service as well? Or do you need to modify the web service to?

    Nils

  • http://pelechano.es Enrique

    However, I find the automated process to cause more troubles than benefits, therefore I’ll go for a manual override
    I suppose you say that because if two or more modules overrides something in the same class, there will be problems (I didn’t tried it yet)

    Then how would you do that within a module? I’ve seen some modules that do I/O to writte in the file and modify the source code.

    Thanks

  • sare

    Any idea how to display the ‘product.condition’ field in the shopping cart? (for each product)

    I’m using {$product.condition} but nothing outputs in the cart. I can output all the other fields in the product table, but not ‘condition’ which is a field I’ve customized and use lots. I have this field displayed on my product pages, no trouble.

    In shopping-cart-product-line.tpl i have

    {$product.condition} | {$product.name|escape:’htmlall':’UTF-8′}

    Do I need to add something somewhere else?
    Go on, make my day, Pleeeeese help

    Many Thanks

  • Jamie

    Hi Nemo,

    Thanks for the tutorial, I am attempting to add a boolean value to categories in Prestashop.

    However I am having trouble getting the categoriescontroller to override, my extended class never appears to get called?

    My file is: ‘/override/controllers/admin/AdminCategoriesController.php’

    ‘override/classes/Category.php’ works fine, if I echo back the $definition in the core file it is pulling through the new field correctly.

    Could I have missed something obvious?

    • Jamie

      Yes I could have, forgot about the cache regenerating!

      Thanks again for the tutorial, very helpful!

Westhost Website Builder 20% off 300x250

Store Top Sales

You like the tuts and want to say "thank you"? Well, you can always feel free to donate: