This article is dedicated to the Model-view-controller in Prestashop CMS. Due to the amount of information we decided to divide it into 3 parts. In the following part we are going to consider basic principles of creation and operation with a model. Moreover, you can find some information regarding this subject under the following link.
Let’s consider Category(/classes/Category.php) object as a model example. Category class is inherited from ObjectModel:
1 2 3 4 5 6 7 8 9 10 11 12 |
class CategoryCore extends ObjectModel { public $id; /** @var int category ID */ public $id_category; /** @var string Name */ public $name; … } |
Which in turn implements ERM (entity-relationship model) in Prestashop, in other words every row in database table subsumes to entity and object, that very simplifies data management.
Basing on Category class example we are going to create new tables in a database to build up our model:
1 2 3 4 5 6 7 8 9 |
CREATE TABLE IF NOT EXISTS `ps_category` ( `id_category` int(10) unsigned NOT NULL AUTO_INCREMENT, `id_parent` int(10) unsigned NOT NULL, `id_shop_default` int(10) unsigned NOT NULL DEFAULT '1', ... `is_root_category` tinyint(1) NOT NULL DEFAULT '0', PRIMARY KEY (`id_category`), … ) ENGINE=InnoDB DEFAULT CHARSET=utf8; |
1 2 3 4 5 6 7 8 |
CREATE TABLE IF NOT EXISTS `ps_category_lang` ( `id_category` int(10) unsigned NOT NULL, `id_shop` int(11) unsigned NOT NULL DEFAULT '1', `id_lang` int(10) unsigned NOT NULL, `name` varchar(128) NOT NULL, `description` text, ... ) ENGINE=InnoDB DEFAULT CHARSET=utf8; |
1 2 3 4 5 6 |
CREATE TABLE IF NOT EXISTS `ps_category_shop` ( `id_category` int(11) NOT NULL, `id_shop` int(11) NOT NULL, `position` int(10) unsigned NOT NULL DEFAULT '0', PRIMARY KEY (`id_category`,`id_shop`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; |
1 2 3 4 5 6 7 |
CREATE TABLE IF NOT EXISTS `ps_category_group` ( `id_category` int(10) unsigned NOT NULL, `id_group` int(10) unsigned NOT NULL, PRIMARY KEY (`id_category`,`id_group`), KEY `id_category` (`id_category`), KEY `id_group` (`id_group`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; |
In order of creating new tables it is necessary to follow some rules:
1. A name of unique table key is built in accordance with the rule:
1 |
'id_' . $name_table(without prefix, CREATE TABLE IF NOT EXISTS `ps_category` … - `id_category` int(11) NOT NULL, ...) |
2. If there are any data in different languages in the entity, then you must create a table name with postfix:
1 |
$name_table . '_lang' (CREATE TABLE IF NOT EXISTS `ps_category_lang` …) |
- A table should contain at least 3 columns: ‘id_name_table’, ‘id_lang’, ‘id_shop’
3. In case entity has to work with multistore, it is necessary to create a table name with postfix:
1 |
$name_table . '_shop' ( CREATE TABLE IF NOT EXISTS `ps_category_shop` …) |
- A table should contain at least 2 columns: ‘id_name_table’, ‘id_shop’
4. In case you need to limit an access to the entity for different user groups, then you should create a table name with postfix: $name_table . ‘_group’ (CREATE TABLE IF NOT EXISTS ps_category_group
…). Access restriction implementation is not inherited from ObjectModel class, and it will require to create additional methods in your entity. Let’s consider it below.
- A table should contain at least 2 columns: ‘id_name_table’, ‘id_shop’
5. ‘id_name_table’ without AUTO_INCREMENT in all tables except of the first, because data could be duplicated and it may lead to exception.
Model Class:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
class CategoryCore extends ObjectModel { public $id; /** @var int category ID */ public $id_category; /** @var string Name */ public $name; /** @var bool Status for display */ public $active = 1; /** @var int category position */ public $position; ... |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
/** * @see ObjectModel::$definition */ public static $definition = array( 'table' => 'category', 'primary' => 'id_category', 'multilang' => true, 'multilang_shop' => true, 'fields' => array( 'nleft' => array('type' => self::TYPE_INT, 'validate' => 'isUnsignedInt'), 'nright' => array('type' => self::TYPE_INT, 'validate' => 'isUnsignedInt'), 'level_depth' => array('type' => self::TYPE_INT, 'validate' => 'isUnsignedInt'), 'active' => array('type' => self::TYPE_BOOL, 'validate' => 'isBool', 'required' => true), 'id_parent' => array('type' => self::TYPE_INT, 'validate' => 'isUnsignedInt'), 'id_shop_default' => array('type' => self::TYPE_INT, 'validate' => 'isUnsignedId'), ... /* Lang fields */ 'name' => array('type' => self::TYPE_STRING, 'lang' => true, 'validate' => 'isCatalogName', 'required' => true, 'size' => 128), 'link_rewrite' => array('type' => self::TYPE_STRING, 'lang' => true, 'validate' => 'isLinkRewrite', 'required' => true, 'size' => 128), ... ), ); |
As you can see from the example, each column of the table corresponds to a class property:
name
varchar(128) NOT NULL, – public $name;
id_category
int(10) unsigned NOT NULL, – public $id_category;
To create a connection between model and tables it is necessary to specify the name of the table in $definition property, as well as the name of the unique key and determine whether our model will work with several languages:
1 2 3 4 5 6 |
public static $definition = array( 'table' => 'category', 'primary' => 'id_category', 'multilang' => true, 'multilang_shop' => true, … |
Those fields that hold data in different languages should have ‘lang’ index with ‘true’ value:
1 |
('name' => array('type' => self::TYPE_STRING, 'lang' => true, 'validate' => 'isCatalogName', 'required' => true, 'size' => 128) |
And in case of using a multistore: ‘shop’ => true
1 |
('id_category_default' => array('type' => self::TYPE_INT, 'shop' => true, 'validate' => 'isUnsignedId'), property from Product class) |
It is necessary to add Shop::addTableAssociation() method in the model constructor, to configure the operation of model with multistore:
1 2 3 4 5 |
public function __construct($id_name_table = null, $id_lang = null, $id_shop = null) { Shop::addTableAssociation('name_table', array('type' => 'shop')); parent::__construct($id_name_table, $id_lang, $id_shop); } |
Let’s consider an implementation of permissions restriction by using ‘ps_category_group’ table:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 |
// /classes/Category.php /** * Update customer groups associated to the object * * @param array $list groups */ public function updateGroup($list) { $this->cleanGroups(); if (empty($list)) { $list = array(Configuration::get('PS_UNIDENTIFIED_GROUP'), Configuration::get('PS_GUEST_GROUP'), Configuration::get('PS_CUSTOMER_GROUP')); } $this->addGroups($list); } public function cleanGroups() { return Db::getInstance()->delete('category_group', 'id_category = '.(int)$this->id); } public function addGroups($groups) { foreach ($groups as $group) { if ($group !== false) { Db::getInstance()->insert('category_group', array('id_category' => (int)$this->id, 'id_group' => (int)$group)); } } } public function getGroups() { $cache_id = 'Category::getGroups_'.(int)$this->id; if (!Cache::isStored($cache_id)) { $result = Db::getInstance()->executeS(' SELECT cg.`id_group` FROM '._DB_PREFIX_.'category_group cg WHERE cg.`id_category` = '.(int)$this->id); $groups = array(); foreach ($result as $group) { $groups[] = $group['id_group']; } Cache::store($cache_id, $groups); return $groups; } return Cache::retrieve($cache_id); } // /controllers/front/CategoryController.php /** * checkAccess return true if id_customer is in a group allowed to see this category. * * @param mixed $id_customer * @access public * @return bool true if access allowed for customer $id_customer */ public function checkAccess($id_customer) { $cache_id = 'Category::checkAccess_'.(int)$this->id.'-'.$id_customer.(!$id_customer ? '-'. (int)Group::getCurrent()->id : ''); if (!Cache::isStored($cache_id)) { if (!$id_customer) { $result = (bool)Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue(' SELECT ctg.`id_group` FROM '._DB_PREFIX_.'category_group ctg WHERE ctg.`id_category` = '.(int)$this->id.' AND ctg.`id_group` = '. (int)Group::getCurrent()->id); } else { $result = (bool)Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue(' SELECT ctg.`id_group` FROM '._DB_PREFIX_.'category_group ctg INNER JOIN '._DB_PREFIX_.'customer_group cg on (cg.`id_group` = ctg.`id_group` AND cg.`id_customer` = '.(int) $id_customer.') WHERE ctg.`id_category` = '.(int)$this->id); } Cache::store($cache_id, $result); return $result; } return Cache::retrieve($cache_id); } |
Unfortunately, ObjectModel and controller implementation does not support its realization and you will have to add them by yourself.
As the result we are getting model class, in which the following methods are already determined:
- add($autodate = true, $nullValues = false) – Save current object to database (add or update).
- Delete() – Delete current object from database.
- save($nullValues = false, $autodate = true) – Save current object to database (add or update).
- update($nullValues = false) – Update current object to database.
A simple example of the model:
1 2 3 4 5 6 |
$context = Context::getContext(); $id_category = 12; $category = new Category((int)$id_category, $context->language->id); $category->active = $category->active == 1 ? 0 : 1; $category->name = ‘New name’; $category->save(); |
After the execution of a code in ‘ps_category’ table, in the string with ‘id_category’ = 12, ‘active’ field will change its value to opposite or will stay the same. ‘Name’ property, which is determined in ‘ps_category_lang’ table will be saved at the same place in the current language. But if you want to save it in several languages, do as follows:
1 2 3 4 5 6 7 8 |
$id_category = 12; $category = new Category((int)$id_category); $category->active = $category->active == 1 ? 0 : 1; foreach (Language::getLanguages(true) as $lang){ $tab->name[$lang['id_lang']] = ‘New name’;//for all languages } $category->save(); |
Properties that are related to ‘ps_category_shop’ table will be saved automatically for the current store, but getting the value list from database will require to use Shop::addSqlAssociation() method. This method adds JOIN into sql request to connect entity fields selection from the current store.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
/** * Return available categories * * @param int $id_lang Language ID * @param bool $active return only active categories * @return array Categories */ public static function getCategories($id_lang = false, $active = true, $order = true, $sql_filter = '', $sql_sort = '', $sql_limit = '') { if (!Validate::isBool($active)) { die(Tools::displayError()); } $result = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS('SELECT * FROM `'._DB_PREFIX_.'category` c '.Shop::addSqlAssociation('category', 'c').' LEFT JOIN `'._DB_PREFIX_.'category_lang` cl ON c.`id_category` = cl.`id_category`'.Shop::addSqlRestrictionOnLang('cl').' WHERE 1 '.$sql_filter.' '.($id_lang ? 'AND `id_lang` = '.(int)$id_lang : '').' '.($active ? 'AND `active` = 1' : '').' '.(!$id_lang ? 'GROUP BY c.id_category' : '').' '.($sql_sort != '' ? $sql_sort : 'ORDER BY c.`level_depth` ASC, category_shop.`position` ASC').' '.($sql_limit != '' ? $sql_limit : '') ); ... |
And now let’s take a look how model operates with AdminController. AdminController is intended for data management in backend. Usually it displays lists of categories, products, editing forms. Look how easy it is to connect a module to controller (based on /controllers/admin/AdminCategoriesController.php):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
public function __construct() { $this->bootstrap = true; $this->table = 'category'; $this->className = 'Category'; $this->lang = true; $this->deleted = false; $this->explicitSelect = true; $this->_defaultOrderBy = 'position'; $this->allow_export = true; $this->context = Context::getContext(); $this->fieldImageSettings = array( 'name' => 'image', 'dir' => 'c' ); $this->fields_list = array( 'id_category' => array( 'title' => $this->l('ID'), 'align' => 'center', 'class' => 'fixed-width-xs' ), 'name' => array( 'title' => $this->l('Name') ), 'description' => array( 'title' => $this->l('Description'), 'callback' => 'getDescriptionClean', 'orderby' => false ), ... |
Here we got 2 most important properties in controller to operate with a model:
1 2 |
$this->table = 'category'; //name table of entity $this->className = 'Category'; //name class model entity |
And if you need to output entity list (rows of tables in your database) in your controller, then just specify the model properties names in $this->fields_list property:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
$this->fields_list = array( 'id_category' => array( 'title' => $this->l('ID'), 'align' => 'center', 'class' => 'fixed-width-xs' ), 'name' => array( 'title' => $this->l('Name') ), 'description' => array( 'title' => $this->l('Description'), 'callback' => 'getDescriptionClean', 'orderby' => false ), class CategoryCore extends ObjectModel { public $id; /** @var int category ID */ public $id_category; /** @var string Name */ public $name; /** @var bool Status for display */ public $active = 1; /** @var int category position */ public $position; |
It’s much easier to edit the data. Just create a form and make sure that fields names are identical to model properties names.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
public function renderForm() { … $this->fields_form = array( 'tinymce' => true, 'legend' => array( 'title' => $this->l('Category'), 'icon' => 'icon-tags' ), 'input' => array( array( 'type' => 'text', 'label' => $this->l('Name'), 'name' => 'name', /** @var string Name */ //public $name; 'lang' => true, 'required' => true, 'class' => 'copy2friendlyUrl', 'hint' => $this->l('Invalid characters:').' <>;=#{}', ), array( 'type' => 'switch', 'label' => $this->l('Displayed'), 'name' => 'active', /** @var bool Status for display */ //public $active = 1; 'required' => false, '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') ) ) ), |
Creation, updating and filling out the form and list fields are performed by AdminController, and it does not require any development of additional logic of saving, deleting or updating.
Pre-save validation is performed by using the settings specified in ‘fields’ array of $definition model property ( ‘active’ => array(‘type’ => self::TYPE_BOOL, ‘validate’ => ‘isBool’, ‘required’ => true),). It’s very important to name a unique key in the first table in accordance with the rule ‘id_’ . $name_table – ‘id_category’. This will help to avoid problems with data updating and deleting.
As we described above, the creation and using of a model in Prestashop is pretty easy process. Managing the model in admin area is rather simple and it doesn’t require monotypic implementation of CRUD.