Opencart tutorial crud

In this Opencart 3 tutorial, we will show you how to code and create a custom page in the admin section to add or create testimonials, this tutorial is part of CRUD functionalities for OpenCart 3. We will create database tables for testimonials, then create form and save data to the database.

We already showed you in opencart database defined video, how to create the database tables and how to take consideration of multi-language, multi-store and multi-layout of Opencart 3 while creating the database.

custom table schema for Testimonial

MySQL queries for creating similar database tables as the above diagram is like below:

CREATE TABLE `oc_testimonial` (
  `testimonial_id` int(11) NOT NULL AUTO_INCREMENT,
  `image` varchar(255) DEFAULT NULL,
  `status` tinyint(1) NOT NULL,
  `sort_order` int(11) NOT NULL,
  `date_added` datetime NOT NULL,
  `date_modified` datetime NOT NULL,
  PRIMARY KEY (`testimonial_id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci;

CREATE TABLE `oc_testimonial_description` (
  `testimonial_id` int(11) NOT NULL,
  `language_id` int(11) NOT NULL,
  `name` varchar(255) NOT NULL,
  `description` text NOT NULL,
  PRIMARY KEY (`testimonial_id`,`language_id`),
  KEY `name` (`name`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci;

CREATE TABLE `oc_testimonial_to_layout` (
  `testimonial_id` int(11) NOT NULL,
  `store_id` int(11) NOT NULL,
  `layout_id` int(11) NOT NULL,
  PRIMARY KEY (`testimonial_id`,`store_id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci;

CREATE TABLE `oc_testimonial_to_store` (
  `testimonial_id` int(11) NOT NULL,
  `store_id` int(11) NOT NULL,
  PRIMARY KEY (`testimonial_id`,`store_id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci;

For now, we will directly run the SQL queries in the database but when we start making multi-instance testimonials module we will create from the install method of the module controller.

Let’s see the files and folders structure for our testimonials pages and module. Opencart allows us to create files and folders inside the following so we need to create the files and folder inside them.

// A list of allowed directories to be written to
$allowed = array(
   'admin/controller/extension/',
   'admin/language/',
   'admin/model/extension/',
   'admin/view/image/',
   'admin/view/javascript/',
   'admin/view/stylesheet/',
   'admin/view/template/extension/',
   'catalog/controller/extension/',
   'catalog/language/',
   'catalog/model/extension/',
   'catalog/view/javascript/',
   'catalog/view/theme/',
   'system/config/',
   'system/library/',
   'image/catalog/'
);

For our Testimonials pages and module following are the files that we need to create:

Files and folders of Testimonial module Opencart

In this tutorial, we will create a custom page in admin and create form and save data to the database, so for that following files are needed, we will create other files in upcoming posts.

  • admin/controller/extension/module/testimonialcrud.php
  • admin/language/en-gb/extension/module/testimonailcrud.php
  • admin/model/extension/module/testimonail.php
  • admin/view/template/extension/module/testimonail_form.twig
  • admin/view/template/extension/module/testimonail_list.twig

With the above file and folder structure, the URL will be like http://opencart.loc/admin/index.php?route=extension/module/testimonialcrud&user_token=5XdZM31DgqUkg4uEmrInmL3pp7uiaYUr, here the http://opencart.loc is your URL and 5XdZM31DgqUkg4uEmrInmL3pp7uiaYUr is your token id created automatically when logged in the Opencart admin section. The output of the listing pages will be like in the image below:

Testimonial listing page

The form output will be like in the image below:

Testimonial form

In admin/controller/extension/module/ create a file called testimonialcrud.php, then paste the following code. As per MVCL opencart pattern, the code of controller in testimonial CRUD will look like below: (Codes are explained in comments between /*** and ***/). In the controller, there are the following methods:

  • index() – Default method which is called when class ControllerExtensionModuleTestimonialcrud is called. This will list out the testimonials as it calls getList().
  • add() – This method is called when someone clicks the add button on the listing page and the save button on the form. If the add button is clicked then it shows the forms with blank fields. If the save button is clicked on the form then it validates the data and saves data in the database and redirects to the listing page.
  • edit() – Edit method is called when someone clicks the edit button in the listing page of the testimonial which will show the form with the data, and similarly it is called when someone clicks the save button on the form while editing, when saved it will validate the form and update the data in the database and redirects to the listing page.
  • delete() – Delete method is called when someone clicks the delete button by selecting the testimonial to delete. Once testimonial/s is/are deleted then it is redirected to the listing page.
  • getList() – This method creates logic to create a listing and pass variables to template twig files where they are manipulated and shown in the table.
  • getForm() – This method creates logic to create a form. When someone clicks the add button then it shows form with blank fields, if someone clicks the edit button then it shows form with data of that testimonial.
  • validateForm() – This method is to check whether the user has permission to edit or add the data from the form. In this method, we can validate any form field if needed.
  • validateDelete() – This method is to confirm if it is ok to delete the selected testimonial to delete.

https://github.com/rupaknepali/Opencart-free-modules/blob/master/testimonial-opencart-3/upload/admin/controller/extension/module/testimonialcrud.php

<?php
/*** We create a file named ‘testimonialcrud.php’ in the admin/controller/extension/module/ folder. Since we named the file testimonialcrud.php and put it at admin/controller/extension/module/ folder, the controller class name will be ControllerExtensionModuleTestimonialcrud which inherits the Controller. ***/
class ControllerExtensionModuleTestimonialcrud extends Controller
{
	/*** private error property for this class only which will hold the error message if occurs any. ***/
	private $error = array();
	/*** The index method is default method, it is called whenever the main controller ControllerExtensionModuleTestimonialcrud is called through route URL, like http://opencart.loc/admin/index.php?route=extension/module/testimonialcrud&user_token=5XdZM31DgqUkg4uEmrInmL3pp7uiaYUr.
	 * Here the language file is loaded
	 * Then Document title is set
	 * Then model file is loaded
	 * Then protected method getList is called which list out all the testimonials. Thus default page is the listing page because we called getList in index() method. ***/
	public function index()
	{
		$this->load->language('extension/module/testimonialcrud');
		$this->document->setTitle($this->language->get('heading_title'));
		$this->load->model('extension/module/testimonial');
		$this->getList();
	}
	/*** add() - This method is called when someone clicks the add button in the listing page and the save button on the form. If the add button is clicked then it shows the forms with blank fields. If the save button is clicked on the form then it validates the data and saves data in the database and redirects to the listing page. ***/
	public function add()
	{
		$this->load->language('extension/module/testimonialcrud');
		$this->document->setTitle($this->language->get('heading_title'));
		$this->load->model('extension/module/testimonial');
		/*** This is the section when someone clicks save button while adding the testimonial. It checks if the request method is post and if form is validated. Then it will call the addTestimonial method of model class which save the new testimonial to the database ***/
		if (($this->request->server['REQUEST_METHOD'] == 'POST') && $this->validateForm()) {
			$this->model_extension_module_testimonial->addTestimonial($this->request->post);
			$this->session->data['success'] = $this->language->get('text_success');
			$url = '';
			if (isset($this->request->get['sort'])) {
				$url .= '&sort=' . $this->request->get['sort'];
			}
			if (isset($this->request->get['order'])) {
				$url .= '&order=' . $this->request->get['order'];
			}
			if (isset($this->request->get['page'])) {
				$url .= '&page=' . $this->request->get['page'];
			}
			/*** This line of code is to redirect to the listing page ***/
			$this->response->redirect($this->url->link('extension/module/testimonialcrud', 'user_token=' . $this->session->data['user_token'] . $url, true));
		}
		/*** This is to show the form ***/
		$this->getForm();
	}
	/*** edit() - Edit method is called when someone clicks the edit button in the listing page of the testimonial which will show the form with the data, and similarly it is called when someone clicks the save button on the form while editing, when saved it will validate the form and update the data in the database and redirects to the listing page. ***/
	public function edit()
	{
		$this->load->language('extension/module/testimonialcrud');
		$this->document->setTitle($this->language->get('heading_title'));
		$this->load->model('extension/module/testimonial');
		/*** This is the section when someone clicks edit button and save the testimonial. It checks if the request method is post and if form is validated. Then it will call the editTestimonial method of model class which save the updated testimonial to the database ***/
		if (($this->request->server['REQUEST_METHOD'] == 'POST') && $this->validateForm()) {
			$this->model_extension_module_testimonial->editTestimonial($this->request->get['testimonial_id'], $this->request->post);
			$this->session->data['success'] = $this->language->get('text_success');
			$url = '';
			if (isset($this->request->get['sort'])) {
				$url .= '&sort=' . $this->request->get['sort'];
			}
			if (isset($this->request->get['order'])) {
				$url .= '&order=' . $this->request->get['order'];
			}
			if (isset($this->request->get['page'])) {
				$url .= '&page=' . $this->request->get['page'];
			}
			/*** This line of code is to redirect to the listing page ***/
			$this->response->redirect($this->url->link('extension/module/testimonialcrud', 'user_token=' . $this->session->data['user_token'] . $url, true));
		}
		/*** This is to show the form ***/
		$this->getForm();
	}
	/*** delete() - Delete method is called when someone clicks delete button by selecting the testimonial to delete. Once testimonial/s is/are deleted then it is redirected to the listing page.***/
	public function delete()
	{
		$this->load->language('extension/module/testimonialcrud');
		$this->document->setTitle($this->language->get('heading_title'));
		$this->load->model('extension/module/testimonial');
		/*** This is the section which find which testimonial are selected that need to be deleted. The deleteTestimonial method of the model class is called which remove the testimonial from the database ***/
		if (isset($this->request->post['selected']) && $this->validateDelete()) {
			foreach ($this->request->post['selected'] as $testimonial_id) {
				$this->model_extension_module_testimonial->deleteTestimonial($testimonial_id);
			}
			$this->session->data['success'] = $this->language->get('text_success');
			$url = '';
			if (isset($this->request->get['sort'])) {
				$url .= '&sort=' . $this->request->get['sort'];
			}
			if (isset($this->request->get['order'])) {
				$url .= '&order=' . $this->request->get['order'];
			}
			if (isset($this->request->get['page'])) {
				$url .= '&page=' . $this->request->get['page'];
			}
			$this->response->redirect($this->url->link('extension/module/testimonialcrud', 'user_token=' . $this->session->data['user_token'] . $url, true));
		}
		$this->getList();
	}
	/*** getList() - This method creates logic to create a listing and pass variables to template twig files where they are manipulated and shown in the table.
	the listing page will look like in the image url https://webocreation.com/blog/wp-content/uploads/2019/09/testimonial-listings.jpg  ***/
	protected function getList()
	{
		if (isset($this->request->get['sort'])) {
			$sort = $this->request->get['sort'];
		} else {
			$sort = 'name';
		}
		if (isset($this->request->get['order'])) {
			$order = $this->request->get['order'];
		} else {
			$order = 'ASC';
		}
		if (isset($this->request->get['page'])) {
			$page = $this->request->get['page'];
		} else {
			$page = 1;
		}
		$url = '';
		if (isset($this->request->get['sort'])) {
			$url .= '&sort=' . $this->request->get['sort'];
		}
		if (isset($this->request->get['order'])) {
			$url .= '&order=' . $this->request->get['order'];
		}
		if (isset($this->request->get['page'])) {
			$url .= '&page=' . $this->request->get['page'];
		}
		/*** Breadcrumbs variables set ***/
		$data['breadcrumbs'] = array();
		$data['breadcrumbs'][] = array(
			'text' => $this->language->get('text_home'),
			'href' => $this->url->link('common/dashboard', 'user_token=' . $this->session->data['user_token'], true)
		);
		$data['breadcrumbs'][] = array(
			'text' => $this->language->get('heading_title'),
			'href' => $this->url->link('extension/module/testimonialcrud', 'user_token=' . $this->session->data['user_token'] . $url, true)
		);
		/*** Add and delete button URL setup for the form ***/
		$data['add'] = $this->url->link('extension/module/testimonialcrud/add', 'user_token=' . $this->session->data['user_token'] . $url, true);
		$data['delete'] = $this->url->link('extension/module/testimonialcrud/delete', 'user_token=' . $this->session->data['user_token'] . $url, true);
		/*** testimonials variables is set to empty array, latter we will set the testimonials in it ***/
		$data['testimonials'] = array();
		/*** We set filter_data like below, $sort, $order, $page are assigned in above code, we can get from the URL paramaters or the config values. We pass this array and in model the SQL will be create as per this filter data   ***/
		$filter_data = array(
			'sort'  => $sort,
			'order' => $order,
			'start' => ($page - 1) * $this->config->get('config_limit_admin'),
			'limit' => $this->config->get('config_limit_admin')
		);
		/*** This is to get the total of number of testimonials as this is needed for the pagination ***/
		$testimonialcrud_total = $this->model_extension_module_testimonial->getTotalTestimonials();
		/*** This is to get filtered testimonials ***/
		$results = $this->model_extension_module_testimonial->getTestimonials($filter_data);
		/*** This is how we set data to the testimonials array, we can get many variables in the $results variables so we separate what is needed in template twig file and pass them to it ***/
		foreach ($results as $result) {
			$data['testimonials'][] = array(
				'testimonial_id' => $result['testimonial_id'],
				'name'        => $result['name'],
				'sort_order'  => $result['sort_order'],
				'edit'        => $this->url->link('extension/module/testimonialcrud/edit', 'user_token=' . $this->session->data['user_token'] . '&testimonial_id=' . $result['testimonial_id'] . $url, true),
				'delete'      => $this->url->link('extension/module/testimonialcrud/delete', 'user_token=' . $this->session->data['user_token'] . '&testimonial_id=' . $result['testimonial_id'] . $url, true)
			);
		}
		if (isset($this->error['warning'])) {
			$data['error_warning'] = $this->error['warning'];
		} else {
			$data['error_warning'] = '';
		}
		if (isset($this->session->data['success'])) {
			$data['success'] = $this->session->data['success'];
			unset($this->session->data['success']);
		} else {
			$data['success'] = '';
		}
		if (isset($this->request->post['selected'])) {
			$data['selected'] = (array) $this->request->post['selected'];
		} else {
			$data['selected'] = array();
		}
		$url = '';
		if ($order == 'ASC') {
			$url .= '&order=DESC';
		} else {
			$url .= '&order=ASC';
		}
		if (isset($this->request->get['page'])) {
			$url .= '&page=' . $this->request->get['page'];
		}
		$data['sort_name'] = $this->url->link('extension/module/testimonialcrud', 'user_token=' . $this->session->data['user_token'] . '&sort=name' . $url, true);
		$data['sort_sort_order'] = $this->url->link('extension/module/testimonialcrud', 'user_token=' . $this->session->data['user_token'] . '&sort=sort_order' . $url, true);
		$url = '';
		if (isset($this->request->get['sort'])) {
			$url .= '&sort=' . $this->request->get['sort'];
		}
		if (isset($this->request->get['order'])) {
			$url .= '&order=' . $this->request->get['order'];
		}
		/*** Pagination in Opencart they are self explainatory ***/
		$pagination = new Pagination();
		$pagination->total = $testimonialcrud_total;
		$pagination->page = $page;
		$pagination->limit = $this->config->get('config_limit_admin');
		$pagination->url = $this->url->link('extension/module/testimonialcrud', 'user_token=' . $this->session->data['user_token'] . $url . '&page={page}', true);
		$data['pagination'] = $pagination->render();
		$data['results'] = sprintf($this->language->get('text_pagination'), ($testimonialcrud_total) ? (($page - 1) * $this->config->get('config_limit_admin')) + 1 : 0, ((($page - 1) * $this->config->get('config_limit_admin')) > ($testimonialcrud_total - $this->config->get('config_limit_admin'))) ? $testimonialcrud_total : ((($page - 1) * $this->config->get('config_limit_admin')) + $this->config->get('config_limit_admin')), $testimonialcrud_total, ceil($testimonialcrud_total / $this->config->get('config_limit_admin')));
		$data['sort'] = $sort;
		$data['order'] = $order;
		/*** Pass the header, column_left and footer to the testimonialcrud_list.twig template ***/
		$data['header'] = $this->load->controller('common/header');
		$data['column_left'] = $this->load->controller('common/column_left');
		$data['footer'] = $this->load->controller('common/footer');
		/*** Set the response output ***/
		$this->response->setOutput($this->load->view('extension/module/testimonialcrud_list', $data));
	}
	/*** getForm() - This method creates logic to create a form. When someone clicks the add button then it shows form with blank fields, if someone clicks the edit button then it shows form with data of that testimonial.
	 ***/
	protected function getForm()
	{
		$data['text_form'] = !isset($this->request->get['testimonial_id']) ? $this->language->get('text_add') : $this->language->get('text_edit');
		if (isset($this->error['warning'])) {
			$data['error_warning'] = $this->error['warning'];
		} else {
			$data['error_warning'] = '';
		}
		if (isset($this->error['name'])) {
			$data['error_name'] = $this->error['name'];
		} else {
			$data['error_name'] = array();
		}
		$url = '';
		if (isset($this->request->get['sort'])) {
			$url .= '&sort=' . $this->request->get['sort'];
		}
		if (isset($this->request->get['order'])) {
			$url .= '&order=' . $this->request->get['order'];
		}
		if (isset($this->request->get['page'])) {
			$url .= '&page=' . $this->request->get['page'];
		}
		$data['breadcrumbs'] = array();
		$data['breadcrumbs'][] = array(
			'text' => $this->language->get('text_home'),
			'href' => $this->url->link('common/dashboard', 'user_token=' . $this->session->data['user_token'], true)
		);
		$data['breadcrumbs'][] = array(
			'text' => $this->language->get('heading_title'),
			'href' => $this->url->link('extension/module/testimonialcrud', 'user_token=' . $this->session->data['user_token'] . $url, true)
		);
		/*** This is the code which separate the action of edit or add action, if the URL parameter contains testimonial_id then it is edit else it is add  ***/
		if (!isset($this->request->get['testimonial_id'])) {
			$data['action'] = $this->url->link('extension/module/testimonialcrud/add', 'user_token=' . $this->session->data['user_token'] . $url, true);
		} else {
			$data['action'] = $this->url->link('extension/module/testimonialcrud/edit', 'user_token=' . $this->session->data['user_token'] . '&testimonial_id=' . $this->request->get['testimonial_id'] . $url, true);
		}
		$data['cancel'] = $this->url->link('extension/module/testimonialcrud', 'user_token=' . $this->session->data['user_token'] . $url, true);
		/*** This is the code which pulls the testimonial that we have to edit  ***/
		if (isset($this->request->get['testimonial_id']) && ($this->request->server['REQUEST_METHOD'] != 'POST')) {
			$testimonialcrud_info = $this->model_extension_module_testimonial->getTestimonial($this->request->get['testimonial_id']);
		}
		$data['user_token'] = $this->session->data['user_token'];
		/*** These two line of codes are to pull all active language ***/
		$this->load->model('localisation/language');
		$data['languages'] = $this->model_localisation_language->getLanguages();
		/*** This is the code to check testimonial_description field values, if it empty, or just clicked save button and request method is Post or set the available data in it while editing. We do this for all the fields. ***/
		if (isset($this->request->post['testimonial_description'])) {
			$data['testimonial_description'] = $this->request->post['testimonial_description'];
		} elseif (isset($this->request->get['testimonial_id'])) {
			$data['testimonial_description'] = $this->model_extension_module_testimonial->getTestimonialDescriptions($this->request->get['testimonial_id']);
		} else {
			$data['testimonial_description'] = array();
		}
		/*** This is code is to pull all active stores and set the selected stores. ***/
		$this->load->model('setting/store');
		$data['stores'] = array();
		$data['stores'][] = array(
			'store_id' => 0,
			'name'     => $this->language->get('text_default')
		);
		$stores = $this->model_setting_store->getStores();
		foreach ($stores as $store) {
			$data['stores'][] = array(
				'store_id' => $store['store_id'],
				'name'     => $store['name']
			);
		}
		if (isset($this->request->post['testimonial_store'])) {
			$data['testimonial_store'] = $this->request->post['testimonial_store'];
		} elseif (isset($this->request->get['testimonial_id'])) {
			$data['testimonial_store'] = $this->model_extension_module_testimonial->getTestimonialStores($this->request->get['testimonial_id']);
		} else {
			$data['testimonial_store'] = array(0);
		}
		/*** This is for image field ***/
		if (isset($this->request->post['image'])) {
			$data['image'] = $this->request->post['image'];
		} elseif (!empty($testimonialcrud_info)) {
			$data['image'] = $testimonialcrud_info['image'];
		} else {
			$data['image'] = '';
		}
		$this->load->model('tool/image');
		/*** This is for resizing of image to show thumbnails ***/
		if (isset($this->request->post['image']) && is_file(DIR_IMAGE . $this->request->post['image'])) {
			$data['thumb'] = $this->model_tool_image->resize($this->request->post['image'], 100, 100);
		} elseif (!empty($testimonialcrud_info) && is_file(DIR_IMAGE . $testimonialcrud_info['image'])) {
			$data['thumb'] = $this->model_tool_image->resize($testimonialcrud_info['image'], 100, 100);
		} else {
			$data['thumb'] = $this->model_tool_image->resize('no_image.png', 100, 100);
		}
		$data['placeholder'] = $this->model_tool_image->resize('no_image.png', 100, 100);
		/*** This is for sort order field ***/
		if (isset($this->request->post['sort_order'])) {
			$data['sort_order'] = $this->request->post['sort_order'];
		} elseif (!empty($testimonialcrud_info)) {
			$data['sort_order'] = $testimonialcrud_info['sort_order'];
		} else {
			$data['sort_order'] = 0;
		}
		/*** This is for status field ***/
		if (isset($this->request->post['status'])) {
			$data['status'] = $this->request->post['status'];
		} elseif (!empty($testimonialcrud_info)) {
			$data['status'] = $testimonialcrud_info['status'];
		} else {
			$data['status'] = true;
		}
		/*** This is for layout field ***/
		if (isset($this->request->post['testimonial_layout'])) {
			$data['testimonial_layout'] = $this->request->post['testimonial_layout'];
		} elseif (isset($this->request->get['testimonial_id'])) {
			$data['testimonial_layout'] = $this->model_extension_module_testimonial->getTestimonialLayouts($this->request->get['testimonial_id']);
		} else {
			$data['testimonial_layout'] = array();
		}
		/*** This is to get all layouts ***/
		$this->load->model('design/layout');
		$data['layouts'] = $this->model_design_layout->getLayouts();
		$data['header'] = $this->load->controller('common/header');
		$data['column_left'] = $this->load->controller('common/column_left');
		$data['footer'] = $this->load->controller('common/footer');
		$this->response->setOutput($this->load->view('extension/module/testimonialcrud_form', $data));
	}
	/***
	 * validateForm() - This method is to check whether the user has permission to edit or add the data from the form. In this method, we can validate any form field if needed.
	 ***/
	protected function validateForm()
	{
		/*** This is how we check if the user has permission to modify or not. ***/
		if (!$this->user->hasPermission('modify', 'extension/module/testimonialcrud')) {
			$this->error['warning'] = $this->language->get('error_permission');
		}
		/*** This is to check if the testimonial_description name contiains more than 1 character and less than 255 characters ***/
		foreach ($this->request->post['testimonial_description'] as $language_id => $value) {
			if ((utf8_strlen($value['name']) < 1) || (utf8_strlen($value['name']) > 255)) {
				$this->error['name'][$language_id] = $this->language->get('error_name');
			}
		}
		if ($this->error && !isset($this->error['warning'])) {
			$this->error['warning'] = $this->language->get('error_warning');
		}
		return !$this->error;
	}
	/*** validateDelete() - This method is to check if the user has permission to delete or not ***/
	protected function validateDelete()
	{
		if (!$this->user->hasPermission('modify', 'extension/module/testimonialcrud')) {
			$this->error['warning'] = $this->language->get('error_permission');
		}
		return !$this->error;
	}
}

For language we create testimonailcrud.php file at admin/language/en-gb/extension/module/ and paste the following code. We defined required language variables and assign them the text of that language.

https://github.com/rupaknepali/Opencart-free-modules/blob/master/testimonial-opencart-3/upload/admin/language/en-gb/extension/module/testimonialcrud.php

<?php
// Heading
$_['heading_title']          = 'Testimonial CRUD';
// Text
$_['text_success']           = 'Success: You have modified Testimonial CRUD!';
$_['text_list']              = 'Testimonial List';
$_['text_add']               = 'Add Testimonial';
$_['text_edit']              = 'Edit Testimonial';
$_['text_default']           = 'Default';
$_['text_keyword']           = 'Do not use spaces, instead replace spaces with - and make sure the SEO URL is globally unique.';
// Column
$_['column_name']            = 'Testimonial Name';
$_['column_sort_order']      = 'Sort Order';
$_['column_action']          = 'Action';
// Entry
$_['entry_name']             = 'Testimonial Name';
$_['entry_description']      = 'Description';
$_['entry_meta_title']          = 'Meta Tag Title';
$_['entry_meta_keyword']     = 'Meta Tag Keywords';
$_['entry_meta_description'] = 'Meta Tag Description';
$_['entry_store']            = 'Stores';
$_['entry_keyword']          = 'Keyword';
$_['entry_parent']           = 'Parent';
$_['entry_filter']           = 'Filters';
$_['entry_image']            = 'Image';
$_['entry_top']              = 'Top';
$_['entry_column']           = 'Columns';
$_['entry_sort_order']       = 'Sort Order';
$_['entry_status']           = 'Status';
$_['entry_layout']           = 'Layout Override';
// Help
$_['help_filter']            = '(Autocomplete)';
$_['help_top']               = 'Display in the top menu bar. Only works for the top parent Testimonial CRUD.';
$_['help_column']            = 'Number of columns to use for the bottom 3 Testimonial CRUD. Only works for the top parent Testimonial CRUD.';
// Error
$_['error_warning']          = 'Warning: Please check the form carefully for errors!';
$_['error_permission']       = 'Warning: You do not have permission to modify Testimonial CRUD!';
$_['error_name']             = 'Testimonial Name must be between 1 and 255 characters!';
$_['error_meta_title']       = 'Meta Title must be greater than 1 and less than 255 characters!';
$_['error_keyword']          = 'SEO URL already in use!';
$_['error_unique']           = 'SEO URL must be unique!';
$_['error_parent']           = 'The parent Testimonial you have chosen is a child of the current one!';

For model we create testimonail.php file at admin/model/extension/module/ and add the following lines of code, code is described in comments:

https://github.com/rupaknepali/Opencart-free-modules/blob/master/testimonial-opencart-3/upload/admin/model/extension/module/testimonial.php

<?php
class ModelExtensionModuleTestimonial extends Model
{
	/** addTestimonial method is to add testimonial which is called from controller like $this->model_extension_module_testimonial->addTestimonial($this->request->post);. Data is inserted in the oc_testimonial table, oc_testimonail_description, oc_testimonial_to_store and oc_testimonial_to_layout and cache is cleared for the testimonial variable ***/
	public function addTestimonial($data)
	{
		$this->db->query("INSERT INTO " . DB_PREFIX . "testimonial SET sort_order = '" . (int) $data['sort_order'] . "', status = '" . (int) $data['status'] . "', date_modified = NOW(), date_added = NOW()");
		$testimonial_id = $this->db->getLastId();
		if (isset($data['image'])) {
			$this->db->query("UPDATE " . DB_PREFIX . "testimonial SET image = '" . $this->db->escape($data['image']) . "' WHERE testimonial_id = '" . (int) $testimonial_id . "'");
		}
		//Testimonial Descritpion added
		foreach ($data['testimonial_description'] as $language_id => $value) {
			$this->db->query("INSERT INTO " . DB_PREFIX . "testimonial_description SET testimonial_id = '" . (int) $testimonial_id . "', language_id = '" . (int) $language_id . "', name = '" . $this->db->escape($value['name']) . "', description = '" . $this->db->escape($value['description']) . "'");
		}
		//Set which store to use with this testimonial
		if (isset($data['testimonial_store'])) {
			foreach ($data['testimonial_store'] as $store_id) {
				$this->db->query("INSERT INTO " . DB_PREFIX . "testimonial_to_store SET testimonial_id = '" . (int) $testimonial_id . "', store_id = '" . (int) $store_id . "'");
			}
		}
		// Set which layout to use with this testimonial
		if (isset($data['testimonial_layout'])) {
			foreach ($data['testimonial_layout'] as $store_id => $layout_id) {
				$this->db->query("INSERT INTO " . DB_PREFIX . "testimonial_to_layout SET testimonial_id = '" . (int) $testimonial_id . "', store_id = '" . (int) $store_id . "', layout_id = '" . (int) $layout_id . "'");
			}
		}
		$this->cache->delete('testimonial');
		return $testimonial_id;
	}
	/** editTestimonial method is to update the testimonial which is called from controller like $this->model_extension_module_testimonial->editTestimonial($this->request->post);. Data is updated in the oc_testimonial table, oc_testimonail_description, oc_testimonial_to_store and oc_testimonial_to_layout and cache is cleared for the testimonial variable ***/
	public function editTestimonial($testimonial_id, $data)
	{
		$this->db->query("UPDATE " . DB_PREFIX . "testimonial SET sort_order = '" . (int) $data['sort_order'] . "', status = '" . (int) $data['status'] . "', date_modified = NOW() WHERE testimonial_id = '" . (int) $testimonial_id . "'");
		if (isset($data['image'])) {
			$this->db->query("UPDATE " . DB_PREFIX . "testimonial SET image = '" . $this->db->escape($data['image']) . "' WHERE testimonial_id = '" . (int) $testimonial_id . "'");
		}
		$this->db->query("DELETE FROM " . DB_PREFIX . "testimonial_description WHERE testimonial_id = '" . (int) $testimonial_id . "'");
		foreach ($data['testimonial_description'] as $language_id => $value) {
			$this->db->query("INSERT INTO " . DB_PREFIX . "testimonial_description SET testimonial_id = '" . (int) $testimonial_id . "', language_id = '" . (int) $language_id . "', name = '" . $this->db->escape($value['name']) . "', description = '" . $this->db->escape($value['description']) . "'");
		}
		$this->db->query("DELETE FROM " . DB_PREFIX . "testimonial_to_store WHERE testimonial_id = '" . (int) $testimonial_id . "'");
		if (isset($data['testimonial_store'])) {
			foreach ($data['testimonial_store'] as $store_id) {
				$this->db->query("INSERT INTO " . DB_PREFIX . "testimonial_to_store SET testimonial_id = '" . (int) $testimonial_id . "', store_id = '" . (int) $store_id . "'");
			}
		}
		$this->db->query("DELETE FROM " . DB_PREFIX . "testimonial_to_layout WHERE testimonial_id = '" . (int) $testimonial_id . "'");
		if (isset($data['testimonial_layout'])) {
			foreach ($data['testimonial_layout'] as $store_id => $layout_id) {
				$this->db->query("INSERT INTO " . DB_PREFIX . "testimonial_to_layout SET testimonial_id = '" . (int) $testimonial_id . "', store_id = '" . (int) $store_id . "', layout_id = '" . (int) $layout_id . "'");
			}
		}
		$this->cache->delete('testimonial');
	}
	/** deleteTestimonial method is to delete the testimonial which is called from controller like $this->model_extension_module_testimonial->deleteTestimonial($testimonial_id);. Data is removed from the oc_testimonial table, oc_testimonail_description, oc_testimonial_to_store and oc_testimonial_to_layout and cache is cleared for the testimonial variable ***/
	public function deleteTestimonial($testimonial_id)
	{

		$this->db->query("DELETE FROM " . DB_PREFIX . "testimonial WHERE testimonial_id = '" . (int) $testimonial_id . "'");
		$this->db->query("DELETE FROM " . DB_PREFIX . "testimonial_description WHERE testimonial_id = '" . (int) $testimonial_id . "'");
		$this->db->query("DELETE FROM " . DB_PREFIX . "testimonial_to_store WHERE testimonial_id = '" . (int) $testimonial_id . "'");
		$this->db->query("DELETE FROM " . DB_PREFIX . "testimonial_to_layout WHERE testimonial_id = '" . (int) $testimonial_id . "'");

		$this->cache->delete('testimonial');
	}
	/** getTestimonial method is to retrieve the testimonial which is called from controller like $testimonialcrud_info = $this->model_extension_module_testimonial->getTestimonial($this->request->get['testimonial_id']);. Only one testimonial with that testimonial_id is returned  ***/
	public function getTestimonial($testimonial_id)
	{
		$query = $this->db->query("SELECT DISTINCT * FROM " . DB_PREFIX . "testimonial c LEFT JOIN " . DB_PREFIX . "testimonial_description cd2 ON (c.testimonial_id = cd2.testimonial_id) WHERE c.testimonial_id = '" . (int) $testimonial_id . "' AND cd2.language_id = '" . (int) $this->config->get('config_language_id') . "'");
		return $query->row;
	}
	/** getTestimonials method is to retrieve the testimonials which is called from controller like $results = $this->model_extension_module_testimonial->getTestimonials($filter_data);. $data is the filtering parameter. Multiple testimonials are returned  ***/
	public function getTestimonials($data = array())
	{
		$sql = "SELECT * FROM " . DB_PREFIX . "testimonial c1 LEFT JOIN " . DB_PREFIX . "testimonial_description cd2 ON (c1.testimonial_id = cd2.testimonial_id) WHERE cd2.language_id ='" . (int) $this->config->get('config_language_id') . "'";
		$sort_data = array(
			'name',
			'sort_order'
		);
		if (isset($data['sort']) && in_array($data['sort'], $sort_data)) {
			$sql .= " ORDER BY " . $data['sort'];
		} else {
			$sql .= " ORDER BY sort_order";
		}
		if (isset($data['order']) && ($data['order'] == 'DESC')) {
			$sql .= " DESC";
		} else {
			$sql .= " ASC";
		}
		if (isset($data['start']) || isset($data['limit'])) {
			if ($data['start'] < 0) {
				$data['start'] = 0;
			}
			if ($data['limit'] < 1) {
				$data['limit'] = 20;
			}
			$sql .= " LIMIT " . (int) $data['start'] . "," . (int) $data['limit'];
		}
		$query = $this->db->query($sql);
		return $query->rows;
	}
	/** getTestimonialDescriptions method is to retrieve the testimonials' description as per the language ***/
	public function getTestimonialDescriptions($testimonial_id)
	{
		$testimonial_description_data = array();
		$query = $this->db->query("SELECT * FROM " . DB_PREFIX . "testimonial_description WHERE testimonial_id = '" . (int) $testimonial_id . "'");
		foreach ($query->rows as $result) {
			$testimonial_description_data[$result['language_id']] = array(
				'name'             => $result['name'],
				'description'      => $result['description']
			);
		}
		return $testimonial_description_data;
	}
	/** getTestimonialStores method is to retrieve the testimonials' store of the testimonial***/
	public function getTestimonialStores($testimonial_id)
	{
		$testimonial_store_data = array();
		$query = $this->db->query("SELECT * FROM " . DB_PREFIX . "testimonial_to_store WHERE testimonial_id = '" . (int) $testimonial_id . "'");
		foreach ($query->rows as $result) {
			$testimonial_store_data[] = $result['store_id'];
		}
		return $testimonial_store_data;
	}
	/** getTestimonialLayouts method is to retrieve the testimonials' layout of the testimonial***/
	public function getTestimonialLayouts($testimonial_id)
	{
		$testimonial_layout_data = array();
		$query = $this->db->query("SELECT * FROM " . DB_PREFIX . "testimonial_to_layout WHERE testimonial_id = '" . (int) $testimonial_id . "'");
		foreach ($query->rows as $result) {
			$testimonial_layout_data[$result['store_id']] = $result['layout_id'];
		}
		return $testimonial_layout_data;
	}
	/** getTotalTestimonials method is to count the total number of testimonials ***/
	public function getTotalTestimonials()
	{
		$query = $this->db->query("SELECT COUNT(*) AS total FROM " . DB_PREFIX . "testimonial");
		return $query->row['total'];
	}
	/** getTotalTestimonialsByLayoutId method is to count the total number of testimonials as per layout id ***/
	public function getTotalTestimonialsByLayoutId($layout_id)
	{
		$query = $this->db->query("SELECT COUNT(*) AS total FROM " . DB_PREFIX . "testimonial_to_layout WHERE layout_id = '" . (int) $layout_id . "'");
		return $query->row['total'];
	}
}

For the listing page, we create testimonialcrud_list.twig at admin/view/template/extension/module/ and add the following lines of code, they are self-explanatory, let us know in the comment is you didn’t understand anything.
https://github.com/rupaknepali/Opencart-free-modules/blob/master/testimonial-opencart-3/upload/admin/view/template/extension/module/testimonialcrud_list.twig

{{ header }}{{ column_left }}
<div id="content">
    <div class="page-header">
        <div class="container-fluid">
            <div class="pull-right">
                <a href="{{ add }}" data-toggle="tooltip" title="{{ button_add }}" class="btn btn-primary">
                    <i class="fa fa-plus"></i>
                </a>
                <button type="button" data-toggle="tooltip" title="{{ button_delete }}" class="btn btn-danger" onclick="confirm('{{ text_confirm }}') ? $('#form-testimonial').submit() : false;">
                    <i class="fa fa-trash-o"></i>
                </button>
            </div>
            <h1>{{ heading_title }}</h1>
            <ul class="breadcrumb">
                {% for breadcrumb in breadcrumbs %}
                    <li>
                        <a href="{{ breadcrumb.href }}">{{ breadcrumb.text }}</a>
                    </li>
                {% endfor %}
            </ul>
        </div>
    </div>
    <div class="container-fluid">
        {% if error_warning %}
            <div class="alert alert-danger alert-dismissible">
                <i class="fa fa-exclamation-circle"></i>
                {{ error_warning }}
                <button type="button" class="close" data-dismiss="alert">×</button>
            </div>
        {% endif %}
        {% if success %}
            <div class="alert alert-success alert-dismissible">
                <i class="fa fa-check-circle"></i>
                {{ success }}
                <button type="button" class="close" data-dismiss="alert">×</button>
            </div>
        {% endif %}
        <div class="panel panel-default">
            <div class="panel-heading">
                <h3 class="panel-title">
                    <i class="fa fa-list"></i>
                    {{ text_list }}</h3>
            </div>
            <div class="panel-body">
                <form action="{{ delete }}" method="post" enctype="multipart/form-data" id="form-testimonial">
                    <div class="table-responsive">
                        <table class="table table-bordered table-hover">
                            <thead>
                                <tr>
                                    <td style="width: 1px;" class="text-center"><input type="checkbox" onclick="$('input[name*=\'selected\']').prop('checked', this.checked);"/></td>
                                    <td class="text-left">
                                        {% if sort %}
                                            <a href="{{ sort_name }}" class="{{ order|lower }}">{{ column_name }}</a>
                                        {% else %}
                                            <a href="{{ sort_name }}">{{ column_name }}</a>
                                        {% endif %}
                                    </td>
                                    <td class="text-right">
                                        {% if sort == 'sort_order' %}
                                            <a href="{{ sort_sort_order }}" class="{{ order|lower }}">{{ column_sort_order }}</a>
                                        {% else %}
                                            <a href="{{ sort_sort_order }}">{{ column_sort_order }}</a>
                                        {% endif %}
                                    </td>
                                    <td class="text-right">{{ column_action }}</td>
                                </tr>
                            </thead>
                            <tbody>
                                {% if testimonials %}
                                    {% for testimonial in testimonials %}
                                        <tr>
                                            <td class="text-center">
                                                {% if testimonial.testimonial_id in selected %}
                                                    <input type="checkbox" name="selected[]" value="{{ testimonial.testimonial_id }}" checked="checked"/>
                                                {% else %}
                                                    <input type="checkbox" name="selected[]" value="{{ testimonial.testimonial_id }}"/>
                                                {% endif %}
                                            </td>
                                            <td class="text-left">{{ testimonial.name }}</td>
                                            <td class="text-right">{{ testimonial.sort_order }}</td>
                                            <td class="text-right">
                                                <a href="{{ testimonial.edit }}" data-toggle="tooltip" title="{{ button_edit }}" class="btn btn-primary">
                                                    <i class="fa fa-pencil"></i>
                                                </a>
                                            </td>
                                        </tr>
                                    {% endfor %}
                                {% else %}
                                    <tr>
                                        <td class="text-center" colspan="4">{{ text_no_results }}</td>
                                    </tr>
                                {% endif %}
                            </tbody>
                        </table>
                    </div>
                </form>
                <div class="row">
                    <div class="col-sm-6 text-left">{{ pagination }}</div>
                    <div class="col-sm-6 text-right">{{ results }}</div>
                </div>
            </div>
        </div>
    </div>
</div>
{{ footer }}

For form we create testimonialcrud_form.twig file at admin/view/template/extension/module/ and add following code. Most of the code is self-explanatory, one thing summernote.js is added for the text area WYSIWYG editor.
https://github.com/rupaknepali/Opencart-free-modules/blob/master/testimonial-opencart-3/upload/admin/view/template/extension/module/testimonialcrud_form.twig

{{ header }}{{ column_left }}
<div id="content">
    <div class="page-header">
        <div class="container-fluid">
            <div class="pull-right">
                <button type="submit" form="form-category" data-toggle="tooltip" title="{{ button_save }}" class="btn btn-primary">
                    <i class="fa fa-save"></i>
                </button>
                <a href="{{ cancel }}" data-toggle="tooltip" title="{{ button_cancel }}" class="btn btn-default">
                    <i class="fa fa-reply"></i>
                </a>
            </div>
            <h1>{{ heading_title }}</h1>
            <ul class="breadcrumb">
                {% for breadcrumb in breadcrumbs %}
                    <li>
                        <a href="{{ breadcrumb.href }}">{{ breadcrumb.text }}</a>
                    </li>
                {% endfor %}
            </ul>
        </div>
    </div>
    <div class="container-fluid">
        {% if error_warning %}
            <div class="alert alert-danger alert-dismissible">
                <i class="fa fa-exclamation-circle"></i>
                {{ error_warning }}
                <button type="button" class="close" data-dismiss="alert">×</button>
            </div>
        {% endif %}
        <div class="panel panel-default">
            <div class="panel-heading">
                <h3 class="panel-title">
                    <i class="fa fa-pencil"></i>
                    {{ text_form }}</h3>
            </div>
            <div class="panel-body">
                <form action="{{ action }}" method="post" enctype="multipart/form-data" id="form-category" class="form-horizontal">
                    <ul class="nav nav-tabs">
                        <li class="active">
                            <a href="#tab-general" data-toggle="tab">{{ tab_general }}</a>
                        </li>
                        <li>
                            <a href="#tab-design" data-toggle="tab">{{ tab_design }}</a>
                        </li>
                    </ul>
                    <div class="tab-content">
                        <div class="tab-pane active" id="tab-general">
                            <ul class="nav nav-tabs" id="language">
                                {% for language in languages %}
                                    <li>
                                        <a href="#language{{ language.language_id }}" data-toggle="tab"><img src="language/{{ language.code }}/{{ language.code }}.png" title="{{ language.name }}"/>
                                            {{ language.name }}</a>
                                    </li>
                                {% endfor %}
                            </ul>
                            <div class="tab-content">
                                {% for language in languages %}
                                    <div class="tab-pane" id="language{{ language.language_id }}">
                                        <div class="form-group required">
                                            <label class="col-sm-2 control-label" for="input-name{{ language.language_id }}">{{ entry_name }}</label>
                                            <div class="col-sm-10">
                                                <input type="text" name="testimonial_description[{{ language.language_id }}][name]" value="{{ testimonial_description[language.language_id] ? testimonial_description[language.language_id].name }}" placeholder="{{ entry_name }}" id="input-name{{ language.language_id }}" class="form-control"/>
                                                {% if error_name[language.language_id] %}
                                                    <div class="text-danger">{{ error_name[language.language_id] }}</div>
                                                {% endif %}
                                            </div>
                                        </div>
                                        <div class="form-group">
                                            <label class="col-sm-2 control-label" for="input-description{{ language.language_id }}">{{ entry_description }}</label>
                                            <div class="col-sm-10">
                                                <textarea name="testimonial_description[{{ language.language_id }}][description]" placeholder="{{ entry_description }}" id="input-description{{ language.language_id }}" data-toggle="summernote" data-lang="{{ summernote }}" class="form-control">{{ testimonial_description[language.language_id] ? testimonial_description[language.language_id].description }}</textarea>
                                            </div>
                                        </div>
                                    </div>
                                {% endfor %}
                            </div>
                            <div class="form-group">
                                <label class="col-sm-2 control-label">{{ entry_store }}</label>
                                <div class="col-sm-10">
                                    <div class="well well-sm" style="height: 150px; overflow: auto;">
                                        {% for store in stores %}
                                            <div class="checkbox">
                                                <label>
                                                    {% if store.store_id in testimonial_store %}
                                                        <input type="checkbox" name="testimonial_store[]" value="{{ store.store_id }}" checked="checked"/>
                                                        {{ store.name }}
                                                    {% else %}
                                                        <input type="checkbox" name="testimonial_store[]" value="{{ store.store_id }}"/>
                                                        {{ store.name }}
                                                    {% endif %}
                                                </label>
                                            </div>
                                        {% endfor %}
                                    </div>
                                </div>
                            </div>
                            <div class="form-group">
                                <label class="col-sm-2 control-label">{{ entry_image }}</label>
                                <div class="col-sm-10">
                                    <a href="" id="thumb-image" data-toggle="image" class="img-thumbnail"><img src="{{ thumb }}" alt="" title="" data-placeholder="{{ placeholder }}"/></a>
                                    <input type="hidden" name="image" value="{{ image }}" id="input-image"/>
                                </div>
                            </div>
                            <div class="form-group">
                                <label class="col-sm-2 control-label" for="input-sort-order">{{ entry_sort_order }}</label>
                                <div class="col-sm-10">
                                    <input type="text" name="sort_order" value="{{ sort_order }}" placeholder="{{ entry_sort_order }}" id="input-sort-order" class="form-control"/>
                                </div>
                            </div>
                            <div class="form-group">
                                <label class="col-sm-2 control-label" for="input-status">{{ entry_status }}</label>
                                <div class="col-sm-10">
                                    <select name="status" id="input-status" class="form-control">
                                        {% if status %}
                                            <option value="1" selected="selected">{{ text_enabled }}</option>
                                            <option value="0">{{ text_disabled }}</option>
                                        {% else %}
                                            <option value="1">{{ text_enabled }}</option>
                                            <option value="0" selected="selected">{{ text_disabled }}</option>
                                        {% endif %}
                                    </select>
                                </div>
                            </div>
                        </div>
                        <div class="tab-pane" id="tab-design">
                            <div class="table-responsive">
                                <table class="table table-bordered table-hover">
                                    <thead>
                                        <tr>
                                            <td class="text-left">{{ entry_store }}</td>
                                            <td class="text-left">{{ entry_layout }}</td>
                                        </tr>
                                    </thead>
                                    <tbody>

                                        {% for store in stores %}
                                            <tr>
                                                <td class="text-left">{{ store.name }}</td>
                                                <td class="text-left">
                                                    <select name="testimonial_layout[{{ store.store_id }}]" class="form-control">
                                                        <option value=""></option>
                                                        {% for layout in layouts %}
                                                            {% if testimonial_layout[store.store_id] and testimonial_layout[store.store_id] == layout.layout_id %}
                                                                <option value="{{ layout.layout_id }}" selected="selected">{{ layout.name }}</option>
                                                            {% else %}
                                                                <option value="{{ layout.layout_id }}">{{ layout.name }}</option>
                                                            {% endif %}
                                                        {% endfor %}
                                                    </select>
                                                </td>
                                            </tr>
                                        {% endfor %}
                                    </tbody>
                                </table>
                            </div>
                        </div>
                    </div>
                </form>
            </div>
        </div>
    </div>
    <script type="text/javascript" src="view/javascript/summernote/summernote.js"></script>
    <link href="view/javascript/summernote/summernote.css" rel="stylesheet"/>
    <script type="text/javascript" src="view/javascript/summernote/summernote-image-attributes.js"></script>
    <script type="text/javascript" src="view/javascript/summernote/opencart.js"></script>
    <script
        type="text/javascript">
        <!--
        $( '#language a:first' ).tab( 'show' );
        //
        -->
    </script>
</div>
{{ footer }}

Like this way we create a form in Opencart admin(Create), validate the form, submit the form data to the database, list out the data in a table (Read), edit the data and update it (Update), and perform the delete (Delete), like this we perform Opencart CRUD functionalities in Opencart. Hope you liked this post, please subscribe to our YouTube Channel for Opencart video tutorials. You can also find us on Twitter and Facebook.

LEAVE A REPLY

Please enter your comment!
Please enter your name here