Crea un complemento personalizado en OpenCart 2.1.x.x: Primera parte
() translation by (you can also view the original English article)
Como desarrollador, siempre es emocionante crear cosas personalizadas en cualquier framework, y lo mismo ocurre con los complementos de OpenCart.
En esta serie de dos partes, explicaré el desarrollo de plugins personalizados en OpenCart. Desde la perspectiva de un desarrollador novato, repasaremos los detalles del desarrollo de extensiones en OpenCart. También crearemos un pequeño plugin personalizado para demostrar todos y cada uno de los aspectos de la estructura del plugin OpenCart.
En esta primera parte, crearemos un plugin personalizado que muestre los productos recientes en el front-end de la tienda, y podrás configurar la cantidad de productos desde el propio back-end. Ese es el objetivo de este artículo: desarrollar un plugin de back-end con un formulario de configuración.
Supongo que configuraste la última versión de OpenCart, que es 2.1.0.2 al momento de escribir esto. Antes de continuar y desarrollar un plugin real, te guiaré a través de la arquitectura básica del plugin de OpenCart en la siguiente sección.
MVCL en pocas palabras
OpenCart está desarrollado con uno de los patrones de desarrollo web más populares, el patrón MVC, con una variación menor, o más bien diría que es una adición. La adición tiene la forma de un componente de lenguaje que lo convierte en MVCL en el mundo de OpenCart. Puede ser que hayas oído hablar de este patrón, pero por el bien de los principiantes, resumiré rápidamente de qué se trata el patrón.
El M en MVC significa modelo, y ahí es donde reside la mayor parte de la lógica de negocios. En el contexto de OpenCart, es el modelo que interactúa con la capa de abstracción de la base de datos para hacer todo el trabajo pesado necesario para ejecutar la tienda. Es un área donde te encontrarás la mayor parte del tiempo como desarrollador.
Luego, la V significa Vista y representa la capa de presentación de la aplicación. Como su nombre lo indica, solo se ocupa de la lógica de presentación de cualquier página, y recibe la entrada de otras capas y genera la salida XHTML la mayor parte del tiempo. La lógica de negocios de una aplicación debe mantenerse alejada de esta capa; sólo debe preocuparse por qué hacer en lugar de cómo hacerlo.
La C, es el controlador, en MVC el que se encuentra al frente de todo, manejando cada solicitud y tratándola como corresponde. Es un área que incluye la mayor parte de la lógica de la aplicación, desde el manejo y la validación de la entrada del usuario hasta la carga del modelo adecuado y los componentes de visualización para preparar la salida de la página.
Finalmente, hay un componente adicional, la L, que significa lenguaje. Hace que configurar sitios multilingües sea muy sencillo.
Así que esa es una vista previa de la arquitectura OpenCart, y tendrá más sentido a medida que avancemos a la explicación detallada de cada componente.
El Skeleton de cualquier plugin OpenCart
Revisemos rápidamente la lista de archivos que debemos implementar para el plugin del back-end personalizado.
-
admin/language/english/module/recent_products.php
: Es un archivo que contiene etiquetas estáticas que se utilizan en todo el área de la aplicación de administración. -
admin/controller/module/recent_products.php
: Es un archivo de controlador que contiene la lógica de la aplicación de nuestro módulo. -
admin/view/template/module/recent_products.tpl
: Es un archivo de plantilla de vista y contiene código XHTML.
En la siguiente sección, crearemos cada archivo mencionado anteriormente, con una explicación detallada.
Según las convenciones, necesitamos colocar los archivos de plugin personalizados en el directorio del módulo. En este caso, como estamos desarrollando un plugin de back-end, serán los directorios bajo administración los que contengan nuestros archivos. Por supuesto, los archivos se distribuyen en diferentes directorios, o más bien componentes, según la arquitectura OpenCart mostrada anteriormente.
Crear archivos para el plugin de back-end
En esta sección, comenzaremos a crear los archivos del módulo. Primero, crearemos un archivo de idioma admin/language/english/module/recent_products.php
con el siguiente contenido. Es un archivo importante desde la perspectiva de OpenCart, ya que es imprescindible que tu plugin sea detectado por OpenCart.
1 |
<?php
|
2 |
// admin/language/english/module/recent_products.php
|
3 |
// Heading
|
4 |
$_['heading_title'] = 'Recent Products'; |
5 |
|
6 |
// Text
|
7 |
$_['text_module'] = 'Modules'; |
8 |
$_['text_success'] = 'Success: You have modified Recent Products module!'; |
9 |
$_['text_edit'] = 'Edit Recent Products Module'; |
10 |
|
11 |
// Entry
|
12 |
$_['entry_name'] = 'Module Name'; |
13 |
$_['entry_limit'] = 'Limit'; |
14 |
$_['entry_status'] = 'Status'; |
15 |
|
16 |
// Error
|
17 |
$_['error_permission'] = 'Warning: You do not have permission to modify Recent Products module!'; |
18 |
$_['error_name'] = 'Module Name must be between 3 and 64 characters!'; |
Como puedes ver, estamos asignando etiquetas estáticas a una matriz PHP. Más adelante, tendrás acceso a estas variables en el archivo de plantilla de vista a medida que la matriz se convierte en variables PHP.
Es posible que también hayas notado que el archivo se crea en el directorio inglés, ya que es el idioma predeterminado de la tienda. Por supuesto, en el caso de un sitio multilingüe, deberás asegurarte de crearlo también en otros idiomas. Por ejemplo, la versión en francés del mismo archivo debe crearse en admin/language/french/module/recent_products.php
.
Luego, crearemos uno de los archivos de plugin más importantes: el archivo de controlador. Continuemos y creemos admin/controller/module/recent_products.php
con el siguiente contenido.
1 |
<?php
|
2 |
// admin/controller/module/recent_products.php
|
3 |
class ControllerModuleRecentProducts extends Controller { |
4 |
private $error = array(); |
5 |
|
6 |
public function index() { |
7 |
$this->load->language('module/recent_products'); |
8 |
|
9 |
$this->document->setTitle($this->language->get('heading_title')); |
10 |
|
11 |
$this->load->model('extension/module'); |
12 |
|
13 |
if (($this->request->server['REQUEST_METHOD'] == 'POST') && $this->validate()) { |
14 |
if (!isset($this->request->get['module_id'])) { |
15 |
$this->model_extension_module->addModule('recent_products', $this->request->post); |
16 |
} else { |
17 |
$this->model_extension_module->editModule($this->request->get['module_id'], $this->request->post); |
18 |
}
|
19 |
|
20 |
$this->session->data['success'] = $this->language->get('text_success'); |
21 |
|
22 |
$this->response->redirect($this->url->link('extension/module', 'token=' . $this->session->data['token'], 'SSL')); |
23 |
}
|
24 |
|
25 |
$data['heading_title'] = $this->language->get('heading_title'); |
26 |
|
27 |
$data['text_edit'] = $this->language->get('text_edit'); |
28 |
$data['text_enabled'] = $this->language->get('text_enabled'); |
29 |
$data['text_disabled'] = $this->language->get('text_disabled'); |
30 |
|
31 |
$data['entry_name'] = $this->language->get('entry_name'); |
32 |
$data['entry_limit'] = $this->language->get('entry_limit'); |
33 |
$data['entry_status'] = $this->language->get('entry_status'); |
34 |
|
35 |
$data['button_save'] = $this->language->get('button_save'); |
36 |
$data['button_cancel'] = $this->language->get('button_cancel'); |
37 |
|
38 |
if (isset($this->error['warning'])) { |
39 |
$data['error_warning'] = $this->error['warning']; |
40 |
} else { |
41 |
$data['error_warning'] = ''; |
42 |
}
|
43 |
|
44 |
if (isset($this->error['name'])) { |
45 |
$data['error_name'] = $this->error['name']; |
46 |
} else { |
47 |
$data['error_name'] = ''; |
48 |
}
|
49 |
|
50 |
$data['breadcrumbs'] = array(); |
51 |
|
52 |
$data['breadcrumbs'][] = array( |
53 |
'text' => $this->language->get('text_home'), |
54 |
'href' => $this->url->link('common/dashboard', 'token=' . $this->session->data['token'], 'SSL') |
55 |
);
|
56 |
|
57 |
$data['breadcrumbs'][] = array( |
58 |
'text' => $this->language->get('text_module'), |
59 |
'href' => $this->url->link('extension/module', 'token=' . $this->session->data['token'], 'SSL') |
60 |
);
|
61 |
|
62 |
if (!isset($this->request->get['module_id'])) { |
63 |
$data['breadcrumbs'][] = array( |
64 |
'text' => $this->language->get('heading_title'), |
65 |
'href' => $this->url->link('module/recent_products', 'token=' . $this->session->data['token'], 'SSL') |
66 |
);
|
67 |
} else { |
68 |
$data['breadcrumbs'][] = array( |
69 |
'text' => $this->language->get('heading_title'), |
70 |
'href' => $this->url->link('module/recent_products', 'token=' . $this->session->data['token'] . '&module_id=' . $this->request->get['module_id'], 'SSL') |
71 |
);
|
72 |
}
|
73 |
|
74 |
if (!isset($this->request->get['module_id'])) { |
75 |
$data['action'] = $this->url->link('module/recent_products', 'token=' . $this->session->data['token'], 'SSL'); |
76 |
} else { |
77 |
$data['action'] = $this->url->link('module/recent_products', 'token=' . $this->session->data['token'] . '&module_id=' . $this->request->get['module_id'], 'SSL'); |
78 |
}
|
79 |
|
80 |
$data['cancel'] = $this->url->link('extension/module', 'token=' . $this->session->data['token'], 'SSL'); |
81 |
|
82 |
if (isset($this->request->get['module_id']) && ($this->request->server['REQUEST_METHOD'] != 'POST')) { |
83 |
$module_info = $this->model_extension_module->getModule($this->request->get['module_id']); |
84 |
}
|
85 |
|
86 |
if (isset($this->request->post['name'])) { |
87 |
$data['name'] = $this->request->post['name']; |
88 |
} elseif (!empty($module_info)) { |
89 |
$data['name'] = $module_info['name']; |
90 |
} else { |
91 |
$data['name'] = ''; |
92 |
}
|
93 |
|
94 |
if (isset($this->request->post['limit'])) { |
95 |
$data['limit'] = $this->request->post['limit']; |
96 |
} elseif (!empty($module_info)) { |
97 |
$data['limit'] = $module_info['limit']; |
98 |
} else { |
99 |
$data['limit'] = 5; |
100 |
}
|
101 |
|
102 |
if (isset($this->request->post['status'])) { |
103 |
$data['status'] = $this->request->post['status']; |
104 |
} elseif (!empty($module_info)) { |
105 |
$data['status'] = $module_info['status']; |
106 |
} else { |
107 |
$data['status'] = ''; |
108 |
}
|
109 |
|
110 |
$data['header'] = $this->load->controller('common/header'); |
111 |
$data['column_left'] = $this->load->controller('common/column_left'); |
112 |
$data['footer'] = $this->load->controller('common/footer'); |
113 |
|
114 |
$this->response->setOutput($this->load->view('module/recent_products.tpl', $data)); |
115 |
}
|
116 |
|
117 |
protected function validate() { |
118 |
if (!$this->user->hasPermission('modify', 'module/recent_products')) { |
119 |
$this->error['warning'] = $this->language->get('error_permission'); |
120 |
}
|
121 |
|
122 |
if ((utf8_strlen($this->request->post['name']) < 3) || (utf8_strlen($this->request->post['name']) > 64)) { |
123 |
$this->error['name'] = $this->language->get('error_name'); |
124 |
}
|
125 |
|
126 |
return !$this->error; |
127 |
}
|
128 |
}
|
Define la nueva clase para nuestro plugin personalizado que extiende la clase base Controller
. Según las convenciones, el nombre de la clase debe imitar la estructura de directorios en la que se coloca el archivo. Por lo tanto, ¡la ruta de acceso controller/module/recent_products.php
se convierte en ControllerModuleRecentProducts
reemplazando los caracteres de barra diagonal y subrayado de acuerdo con la convención camel-case!
Después, hay un método index
de facto que se llama cuando se carga el plugin en el front-end. Por lo tanto, es un método index que define la mayor parte de la lógica de aplicación del plugin.
En el contexto de la aplicación actual, la abreviatura $this->load->language
carga el archivo del idioma correspondiente. En nuestro caso, carga el archivo del idioma definido en la sección anterior. La sintaxis es bastante simple, solo necesitas pasar el nombre del plugin prefijado por module/
. Se puede acceder a las variables del idioma mediante el método $this->language->get
.
Después, configura el título de la página mediante el método setTitle
del objeto del documento.
En el futuro, la abreviatura $this->load->model
se usa para cargar el modelo del módulo. Es la clase de modelo que proporciona métodos de utilidad para guardar los parámetros del módulo y similares.
Después, hay un fragmento importante, como se muestra a continuación, que verifica si se trata del envío de datos POST y, en ese caso, guarda la configuración del módulo.
1 |
if (($this->request->server['REQUEST_METHOD'] == 'POST') && $this->validate()) { |
2 |
if (!isset($this->request->get['module_id'])) { |
3 |
$this->model_extension_module->addModule('recent_products', $this->request->post); |
4 |
} else { |
5 |
$this->model_extension_module->editModule($this->request->get['module_id'], $this->request->post); |
6 |
}
|
7 |
|
8 |
$this->session->data['success'] = $this->language->get('text_success'); |
9 |
$this->response->redirect($this->url->link('extension/module', 'token=' . $this->session->data['token'], 'SSL')); |
10 |
}
|
Además, estamos asignando etiquetas de idioma como heading_title
y text_edit
a la matriz $data
para que podamos usarlas en el archivo de plantilla de vista.
Luego, hay un fragmento de código que crea los vínculos de ruta de navegación correctos para la página de configuración.
1 |
$data['breadcrumbs'] = array(); |
2 |
|
3 |
$data['breadcrumbs'][] = array( |
4 |
'text' => $this->language->get('text_home'), |
5 |
'href' => $this->url->link('common/dashboard', 'token=' . $this->session->data['token'], 'SSL') |
6 |
);
|
7 |
|
8 |
$data['breadcrumbs'][] = array( |
9 |
'text' => $this->language->get('text_module'), |
10 |
'href' => $this->url->link('extension/module', 'token=' . $this->session->data['token'], 'SSL') |
11 |
);
|
12 |
|
13 |
if (!isset($this->request->get['module_id'])) { |
14 |
$data['breadcrumbs'][] = array( |
15 |
'text' => $this->language->get('heading_title'), |
16 |
'href' => $this->url->link('module/recent_products', 'token=' . $this->session->data['token'], 'SSL') |
17 |
);
|
18 |
} else { |
19 |
$data['breadcrumbs'][] = array( |
20 |
'text' => $this->language->get('heading_title'), |
21 |
'href' => $this->url->link('module/recent_products', 'token=' . $this->session->data['token'] . '&module_id=' . $this->request->get['module_id'], 'SSL') |
22 |
);
|
23 |
}
|
Si el módulo ya se había configurado anteriormente y en modo de edición, el siguiente fragmento completa la configuración predeterminada del módulo.
1 |
if (isset($this->request->get['module_id']) && ($this->request->server['REQUEST_METHOD'] != 'POST')) { |
2 |
$module_info = $this->model_extension_module->getModule($this->request->get['module_id']); |
3 |
}
|
Por último, estamos cargando los elementos comunes de la página como el encabezado, el pie de página y la barra lateral izquierda. Además, es la abreviatura $this->load->view
la que carga el archivo de vista real recent_products.tpl
y muestra el formulario de configuración.
Hay un par de notas importantes para recordar en el archivo del controlador. Verás muchas llamadas como $this->load->ELEMENT
, donde ELEMENT
podría ser la vista, el modelo o el idioma. Carga los componentes de la vista, el modelo e idioma correspondientes.
El siguiente y último archivo para el artículo de hoy es un archivo de plantilla de vista admin/view/template/module/recent_products.tpl
. ¡Anímate y créalo!
1 |
<!-- admin/view/template/module/recent_products.tpl -->
|
2 |
<?php echo $header; ?><?php echo $column_left; ?> |
3 |
<div id="content"> |
4 |
<div class="page-header"> |
5 |
<div class="container-fluid"> |
6 |
<div class="pull-right"> |
7 |
<button type="submit" form="form-recent-products" data-toggle="tooltip" title="<?php echo $button_save; ?>" class="btn btn-primary"><i class="fa fa-save"></i></button> |
8 |
<a href="<?php echo $cancel; ?>" data-toggle="tooltip" title="<?php echo $button_cancel; ?>" class="btn btn-default"><i class="fa fa-reply"></i></a></div> |
9 |
<h1><?php echo $heading_title; ?></h1> |
10 |
<ul class="breadcrumb"> |
11 |
<?php foreach ($breadcrumbs as $breadcrumb) { ?> |
12 |
<li><a href="<?php echo $breadcrumb['href']; ?>"><?php echo $breadcrumb['text']; ?></a></li> |
13 |
<?php } ?> |
14 |
</ul>
|
15 |
</div>
|
16 |
</div>
|
17 |
<div class="container-fluid"> |
18 |
<?php if ($error_warning) { ?> |
19 |
<div class="alert alert-danger"><i class="fa fa-exclamation-circle"></i> <?php echo $error_warning; ?> |
20 |
<button type="button" class="close" data-dismiss="alert">×</button> |
21 |
</div>
|
22 |
<?php } ?> |
23 |
<div class="panel panel-default"> |
24 |
<div class="panel-heading"> |
25 |
<h3 class="panel-title"><i class="fa fa-pencil"></i> <?php echo $text_edit; ?></h3> |
26 |
</div>
|
27 |
<div class="panel-body"> |
28 |
<form action="<?php echo $action; ?>" method="post" enctype="multipart/form-data" id="form-recent-products" class="form-horizontal"> |
29 |
<div class="form-group"> |
30 |
<label class="col-sm-2 control-label" for="input-name"><?php echo $entry_name; ?></label> |
31 |
<div class="col-sm-10"> |
32 |
<input type="text" name="name" value="<?php echo $name; ?>" placeholder="<?php echo $entry_name; ?>" id="input-name" class="form-control" /> |
33 |
<?php if ($error_name) { ?> |
34 |
<div class="text-danger"><?php echo $error_name; ?></div> |
35 |
<?php } ?> |
36 |
</div>
|
37 |
</div>
|
38 |
<div class="form-group"> |
39 |
<label class="col-sm-2 control-label" for="input-limit"><?php echo $entry_limit; ?></label> |
40 |
<div class="col-sm-10"> |
41 |
<input type="text" name="limit" value="<?php echo $limit; ?>" placeholder="<?php echo $entry_limit; ?>" id="input-limit" class="form-control" /> |
42 |
</div>
|
43 |
</div>
|
44 |
<div class="form-group"> |
45 |
<label class="col-sm-2 control-label" for="input-status"><?php echo $entry_status; ?></label> |
46 |
<div class="col-sm-10"> |
47 |
<select name="status" id="input-status" class="form-control"> |
48 |
<?php if ($status) { ?> |
49 |
<option value="1" selected="selected"><?php echo $text_enabled; ?></option> |
50 |
<option value="0"><?php echo $text_disabled; ?></option> |
51 |
<?php } else { ?> |
52 |
<option value="1"><?php echo $text_enabled; ?></option> |
53 |
<option value="0" selected="selected"><?php echo $text_disabled; ?></option> |
54 |
<?php } ?> |
55 |
</select>
|
56 |
</div>
|
57 |
</div>
|
58 |
</form>
|
59 |
</div>
|
60 |
</div>
|
61 |
</div>
|
62 |
</div>
|
63 |
<?php echo $footer; ?> |
Los usuarios atentos ya habrán notado que solo muestra las variables que se pasaron desde el archivo del controlador. Aparte de eso, es un código XHTML simple para mostrar el formulario de configuración, y la cereza del pastel es que responde de inmediato.
Entonces, eso es todo en lo que respecta a la configuración del archivo para nuestro plugin personalizado de back-end.
Habilita el plugin
Dirígete al back-end de OpenCart y ve a Extensiones > Módulos. Deberías ver Productos recientes en la lista. Haz clic en el signo + para instalar el módulo como se muestra en la siguiente captura de pantalla.



Una vez instalado, verás un icono de edición. Haz clic en él para abrir el formulario de configuración del módulo.



En el formulario de configuración, puedes configurar el número de productos recientes que quieres mostrar en el bloque del front-end. Además, !no olvides configurar el campo de estado en Habilitado! Guarda el módulo y debería verse así.



Hay una nueva entrada para el módulo titulada Productos recientes > Mi plugin de bloque reciente. ¡La razón es que puedes replicarlo varias veces para diferentes páginas!
¡Ya casi terminamos! Creamos un plugin personalizado del back-end completo en OpenCart. En la siguiente parte, veremos la contraparte del front-end que muestra un bloque de productos atractivo en el front-end.
Conclusión
Hoy, discutimos el desarrollo de plugins personalizados en OpenCart. En la primera parte de esta serie de dos partes, analizamos el desarrollo del plugin de back-end y creamos un plugin personalizado que funciona y proporciona un formulario de configuración.
Si estás buscando herramientas, utilidades, extensiones, y otras cosas adicionales de OpenCart que puedas aprovechar en tus propios proyectos o para tu propia educación, ve lo que tenemos disponible en la plataforma.
En la siguiente parte, completaremos el plugin creando la parte de front-end que muestra las listas de productos en el front-end. Si tienes alguna duda u opinión, usa la sección de comentarios en la parte de abajo.