📋 Admin Tab + CRUD completo — ecom_faqs
Actualizado: 2025-01-15
Patron clasico de admin
Este es el patron mas usado en modulos PS 1.6-1.7: ObjectModel + AdminController + Tab. Funciona en PS 8.x tambien. Para PS 9.x se recomienda migrar a Symfony controllers, pero este patron legacy sigue soportado.
#Que construimos
Un sistema de FAQs administrable desde el Back Office. El admin puede crear, editar, ordenar y eliminar preguntas frecuentes. Las FAQs se muestran en el front via hook. Incluye: tabla custom, ObjectModel multilang, AdminController con list+form, y hook de display.
#ObjectModel — FaqItem
classes/FaqItem.php — ObjectModel con multilang
php
<?php
class FaqItem extends ObjectModel
{
/** @var int */
public $id_faq_item;
/** @var string Pregunta (multilang) */
public $question;
/** @var string Respuesta (multilang) */
public $answer;
/** @var int Categoria de FAQ */
public $id_faq_category;
/** @var int Posicion para ordenar */
public $position;
/** @var bool Activo/inactivo */
public $active;
/** @var string */
public $date_add;
/** @var string */
public $date_upd;
/**
* Definicion del ObjectModel
* - 'lang' => true para campos multilang
* - 'validate' => tipo de validacion automatica
* - 'required' => true para campos obligatorios
*/
public static $definition = [
'table' => 'ecom_faq_item',
'primary' => 'id_faq_item',
'multilang' => true,
'fields' => [
'id_faq_category' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId'],
'position' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedInt'],
'active' => ['type' => self::TYPE_BOOL, 'validate' => 'isBool'],
'date_add' => ['type' => self::TYPE_DATE, 'validate' => 'isDate'],
'date_upd' => ['type' => self::TYPE_DATE, 'validate' => 'isDate'],
// Campos multilang
'question' => ['type' => self::TYPE_STRING, 'validate' => 'isCleanHtml', 'lang' => true, 'required' => true, 'size' => 500],
'answer' => ['type' => self::TYPE_HTML, 'validate' => 'isCleanHtml', 'lang' => true, 'required' => true, 'size' => 65535],
],
];
/**
* Obtener FAQs activas para el front
*/
public static function getActiveFaqs(int $idLang, int $idCategory = 0, int $limit = 50): array
{
$sql = (new DbQuery())
->select('f.*, fl.question, fl.answer')
->from('ecom_faq_item', 'f')
->innerJoin('ecom_faq_item_lang', 'fl',
'f.id_faq_item = fl.id_faq_item AND fl.id_lang = ' . (int) $idLang)
->where('f.active = 1')
->orderBy('f.position ASC')
->limit($limit);
if ($idCategory > 0) {
$sql->where('f.id_faq_category = ' . (int) $idCategory);
}
return Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS($sql->build()) ?: [];
}
}
#Registrar el Tab en install()
install() y uninstall() con Tab
php
<?php
public function install()
{
return parent::install()
&& $this->executeSqlFile('install')
&& $this->installTab()
&& $this->registerHook('displayFooter');
}
public function uninstall()
{
return $this->uninstallTab()
&& $this->executeSqlFile('uninstall')
&& parent::uninstall();
}
/**
* Crear tab en el menu del BO
* class_name DEBE coincidir con el nombre del AdminController
*/
protected function installTab(): bool
{
$tab = new Tab();
$tab->active = 1;
$tab->class_name = 'AdminEcomFaqs'; // → controllers/admin/AdminEcomFaqsController.php
$tab->module = $this->name;
$tab->id_parent = (int) Tab::getIdFromClassName('AdminCatalog'); // Bajo "Catalogo"
$tab->icon = 'help_outline'; // Material icon (PS 1.7+)
// Nombre del tab en cada idioma
foreach (Language::getLanguages(true) as $lang) {
$tab->name[$lang['id_lang']] = 'FAQs';
}
return $tab->add();
}
protected function uninstallTab(): bool
{
$idTab = (int) Tab::getIdFromClassName('AdminEcomFaqs');
if ($idTab) {
$tab = new Tab($idTab);
return $tab->delete();
}
return true;
}
protected function executeSqlFile(string $name): bool
{
$file = dirname(__FILE__) . '/sql/' . $name . '.sql';
if (!file_exists($file)) {
return false;
}
$sql = str_replace(
['PREFIX_', 'ENGINE_TYPE'],
[_DB_PREFIX_, _MYSQL_ENGINE_],
file_get_contents($file)
);
// Ejecutar multiples queries separadas por ;
foreach (array_filter(array_map('trim', explode(';', $sql))) as $query) {
if (!Db::getInstance()->execute($query)) {
return false;
}
}
return true;
}
sql/install.sql
sql
CREATE TABLE IF NOT EXISTS `PREFIX_ecom_faq_item` (
`id_faq_item` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
`id_faq_category` INT(10) UNSIGNED NOT NULL DEFAULT 0,
`position` INT(10) UNSIGNED NOT NULL DEFAULT 0,
`active` TINYINT(1) NOT NULL DEFAULT 1,
`date_add` DATETIME NOT NULL,
`date_upd` DATETIME NOT NULL,
PRIMARY KEY (`id_faq_item`),
KEY `active_position` (`active`, `position`)
) ENGINE=ENGINE_TYPE DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS `PREFIX_ecom_faq_item_lang` (
`id_faq_item` INT(10) UNSIGNED NOT NULL,
`id_lang` INT(10) UNSIGNED NOT NULL,
`question` VARCHAR(500) NOT NULL,
`answer` TEXT NOT NULL,
PRIMARY KEY (`id_faq_item`, `id_lang`)
) ENGINE=ENGINE_TYPE DEFAULT CHARSET=utf8mb4;
#AdminController completo
controllers/admin/AdminEcomFaqsController.php
php
<?php
require_once _PS_MODULE_DIR_ . 'ecom_faqs/classes/FaqItem.php';
class AdminEcomFaqsController extends ModuleAdminController
{
public function __construct()
{
$this->table = 'ecom_faq_item'; // Tabla sin prefijo
$this->className = 'FaqItem'; // Nombre del ObjectModel
$this->identifier = 'id_faq_item'; // Primary key
$this->lang = true; // Formulario multilang
$this->bootstrap = true; // Usar Bootstrap en templates
$this->position_identifier = 'id_faq_item'; // Para drag&drop
$this->_defaultOrderBy = 'position'; // Orden por defecto
$this->_defaultOrderWay = 'ASC';
parent::__construct();
$this->meta_title = 'Gestionar FAQs';
$this->toolbar_title = 'Preguntas Frecuentes';
// Definir columnas del listado
$this->fields_list = $this->getFieldsList();
// Bulk actions
$this->bulk_actions = [
'delete' => [
'text' => $this->trans('Delete selected', [], 'Admin.Global'),
'icon' => 'icon-trash',
'confirm' => $this->trans('Delete selected items?', [], 'Admin.Global'),
],
'enableSelection' => [
'text' => $this->trans('Enable selection', [], 'Admin.Global'),
'icon' => 'icon-power-off text-success',
],
'disableSelection' => [
'text' => $this->trans('Disable selection', [], 'Admin.Global'),
'icon' => 'icon-power-off text-danger',
],
];
}
/**
* Columnas del HelperList
*/
protected function getFieldsList(): array
{
return [
'id_faq_item' => [
'title' => 'ID',
'align' => 'center',
'class' => 'fixed-width-xs',
],
'question' => [
'title' => 'Pregunta',
'filter_key' => 'b!question', // b = tabla lang alias
],
'position' => [
'title' => 'Posicion',
'filter_key' => 'a!position',
'position' => 'position',
'align' => 'center',
'class' => 'fixed-width-sm',
],
'active' => [
'title' => 'Activo',
'active' => 'status', // Genera toggle automatico
'type' => 'bool',
'align' => 'center',
'class' => 'fixed-width-sm',
'orderby' => false,
],
'date_add' => [
'title' => 'Creado',
'type' => 'datetime',
'align' => 'center',
],
];
}
/**
* Formulario de creacion/edicion
*/
public function renderForm()
{
$this->fields_form = [
'legend' => [
'title' => 'FAQ',
'icon' => 'icon-question-circle',
],
'input' => [
[
'type' => 'text',
'label' => 'Pregunta',
'name' => 'question',
'lang' => true,
'required' => true,
'hint' => 'Maximo 500 caracteres',
],
[
'type' => 'textarea',
'label' => 'Respuesta',
'name' => 'answer',
'lang' => true,
'required' => true,
'autoload_rte' => true, // Editor TinyMCE
'hint' => 'Puedes usar HTML',
],
[
'type' => 'switch',
'label' => 'Activo',
'name' => 'active',
'is_bool' => true,
'values' => [
['id' => 'active_on', 'value' => 1, 'label' => 'Si'],
['id' => 'active_off', 'value' => 0, 'label' => 'No'],
],
],
],
'submit' => [
'title' => $this->trans('Save', [], 'Admin.Global'),
],
];
return parent::renderForm();
}
}
#Hook displayFooter — mostrar FAQs en FO
Mostrar FAQs en el front
php
<?php
public function hookDisplayFooter($params)
{
$faqs = FaqItem::getActiveFaqs(
(int) $this->context->language->id,
0, // todas las categorias
10 // limite
);
if (empty($faqs)) {
return '';
}
$this->context->smarty->assign([
'faqs' => $faqs,
'module_dir' => $this->_path,
]);
return $this->display(__FILE__, 'views/templates/hook/footer-faqs.tpl');
}
views/templates/hook/footer-faqs.tpl
smarty
{* Schema.org FAQPage para SEO *}
<section class="ecom-faqs" itemscope itemtype="https://schema.org/FAQPage">
<h2 class="ecom-faqs__title">{l s='Preguntas Frecuentes' mod='ecom_faqs'}</h2>
<div class="ecom-faqs__list">
{foreach from=$faqs item=faq}
<details class="ecom-faqs__item" itemscope itemprop="mainEntity" itemtype="https://schema.org/Question">
<summary class="ecom-faqs__question" itemprop="name">
{$faq.question|escape:'html':'UTF-8'}
</summary>
<div class="ecom-faqs__answer" itemscope itemprop="acceptedAnswer" itemtype="https://schema.org/Answer">
<div itemprop="text">{$faq.answer nofilter}</div>
</div>
</details>
{/foreach}
</div>
</section>
Descargar en Markdown
Pensado para pegar en ChatGPT, Claude u otra IA. Incluye solo el contenido de esta pagina.