Métodos para agregar imágenes a los productos vía código en Magento

Uno de los desarrollos en los que más participo dentro de los proyectos Magento, suelen ser las integraciones entre cualquier sistema externo y la tienda, ya sea que se envíe o se reciba información.

Si bien ya van 7 años con con la plataforma y las integraciones, por más que algunos procesos ya están bastante logrados y no necesitan reprogramación, sólo configuración; siempre es necesario hacer algún ajuste o cambiar alguna lógica, ya que cada tienda es única.

Una de las entidades/procesos que rara vez se salva de ser reprogramada en las integraciones, es el catálogo. Incluso, dentro del propio mundo que puede resultar el catálogo de una tienda, la gestión de imágenes de los productos que se sincronizan también suele ser diferente entre un proyecto y otro.

Al momento de asociar imágenes a un producto, vía código, podemos pensar en 3 métodos diferentes (no es para tomar como algo definitivo, pero serían 3 las formas de atacar la tarea).

Para los distintos ejemplos, vamos a considerar que tenemos un único producto (en mi caso, el entity_id 1), con N cantidad de imágenes que se encuentran dentro de del directorio media/import.

Editar las imágenes de un producto en Magento

El primer método, quizás el más difundido y conocido, es haciendo uso del propio model catalog/product y el método addImageToMediaGallery.

$_product = Mage::getModel('catalog/product')->load(1);
$_images_path = Mage::getBaseDir() . DS . 'media' . DS . 'import';
$_directory = scandir($_images_path);
if ($_directory) {
    foreach ($_directory as $_file) {
        $_image = $_images_path . DS . $_file;
        if (is_file($_image)) {
            $_product->addImageToMediaGallery($_image, array('image', 'small_image', 'thumbnail'), false, false);
        }
    }
}
$_product->save();

Como puede verse, las imágenes se agregaron correctamente pero (siempre hay un pero), no pudimos especificar realmente cuál sería la imagen principal, ya que al repetirse la iteración, la última imagen quedo marcada como la principal para cada tipo de imagen que nos mostrará la tienda.

Imágenes agregadas con el model catalog/product de Magento

Para cambiar esto, deberíamos hacer alguna validación en la iteración para asignarle, al primer archivo, los atributos que indicarán que será la imagen principal.

Con éste método, encontramos otros problemas:

  • No podemos asignar a discreción el valor del atributo position de la imagen.
  • No podemos asignar el label para la imagen.

El método (si podemos olvidarnos por un momento del label de la imagen) nos va a ser útil cuando trabajemos con productos de una sola imagen.

Supongamos entonces que necesitamos poder manejar, ya sea por alguna lógica (muchas veces, en el nombre del archivo se adjunta posición) qué imagen irá en cada posición. Si este fuera nuestro escenario, vamos a olvidarnos del método addImageToMediaGallery y vamos a recurrir a otro modelo: catalog/product_attribute_media_api (si, a la API, pero ahora usándola desde adentro).

$_product = Mage::getModel('catalog/product')->load(1);
$_images_path = Mage::getBaseDir() . DS . 'media' . DS . 'import';
$_directory = scandir($_images_path);
if ($_directory) {
    $_media_api = Mage::getModel('catalog/product_attribute_media_api');
    $_position = 1;
    foreach ($_directory as $_file) {
        $_image = $_images_path . DS . $_file;
        if (is_file($_image)) {
            $_path_info = pathinfo($_image);
            $_image_type = ($_position == 1) ? array('image', 'small_image', 'thumbnail') : array();
            switch($_path_info['extension']){
                case 'png':
                    $_image_mime_type = 'image/png';
                    break;
                case 'jpg':
                    $_image_mime_type = 'image/jpeg';
                    break;
                case 'gif':
                    $_image_mime_type = 'image/gif';
                    break;
            }
            if ($_image_mime_type) {
                $_new_image = array(
                            'file' => array(
                                'content' => base64_encode($_image),
                                'mime' => $_image_mime_type,
                                'name' => basename($_image),
                                ),
                            'label' => $_product->getName() . ' - posición ' . $_position, 
                            'position' => $_position,
                            'types' => $_image_type,
                            'exclude' => 0,
                        );
                $_media_api->create($_product->getEntityId(), $_new_image);
                $_position++;
                unset($_image_mime_type);
            }
        }
    }
}

Con éste modelo, tenemos mayor posibilidad de manipulación de los datos, permitiéndonos atacar tanto el position como el label de la imagen.

Si miramos en la siguiente captura de pantalla, tal como se muestra en el código, la imágen de posición 1 fue marcada como principal y el label lleva el nombre del producto y la posición.

Imágenes agregadas con el model de la API de Magento

Normalmente, con éste método no deberíamos tener mayores problemas (en la teoría, claro).

Cuando participé del desarrollo de Mamuky, encontramos que debido a la cantidad y la frecuencia de las actualizaciones, en algunas oportunidades, al modificar un producto (la modificación de la galería dispara una actualización del producto), se daban algunos problemas con los índices del catálogo.

Para resolver problemas de este tipo, en particular con catálogos muy grandes que reciban actualizaciones cuantiosas de forma continua, la solución, no muy magento-style, es atacar la base de datos y manejar los archivos por nuestra cuenta.

$_product = Mage::getModel('catalog/product')->load(1);
$_images_path = Mage::getBaseDir() . DS . 'media' . DS . 'import';
$_directory = scandir($_images_path);
if ($_directory) {
    $_position = 1;
    foreach ($_directory as $_file) {
        $_image = $_images_path . DS . $_file;
        if (is_file($_image)) {
            $_path_info = pathinfo($_image);
            $_image_type = ($_position == 1) ? array('image', 'small_image', 'thumbnail') : array();
            switch($_path_info['extension']){
                case 'png':
                    $_image_mime_type = 'image/png';
                    break;
                case 'jpg':
                    $_image_mime_type = 'image/jpeg';
                    break;
                case 'gif':
                    $_image_mime_type = 'image/gif';
                    break;
            }
            if ($_image_mime_type) {
                $this->_addMediaGallery($_product->getEntityId(), $_image, $_position, $_product->getName());
                $_position++;
                unset($_image_mime_type);
            }
        }
    }
}

El proceso se ve muy parecido al caso anterior, pero si prestamos atención, en lugar de usar el modelo de la API, estoy invocando un nuevo método, llamado _addMediaGallery. La idea de separar la grabación y manejo de la imagen en un método aparte, es simplemente para tener un poco más de orden en el código.

Dentro de ese método, tenemos lo siguiente:

private function _addMediaGallery($product_id, $image, $position, $label) {
    $_connection = Mage::getSingleton('core/resource')->getConnection('read');
    //Obtenemos los id's de los atributos de imagen
    $_table = Mage::getSingleton('core/resource')->getTableName('eav/attribute');
    $_attributes_ids = $_connection->fetchAll("SELECT attribute_id FROM {$_table} WHERE entity_type_id = 4 AND attribute_code IN ('thumbnail','small_image','image');");
    //Copiamos el archivo físico, aplicando la lógica de dispersión de Magento
    $_file_io = new Varien_Io_File();
    $_file_io->setAllowCreateFolders(true);
    $_pathinfo = pathinfo($image);
    $_image_name_new = DS . substr($_pathinfo['filename'],0,1) . DS . substr($_pathinfo['filename'],1,1) . DS . $_pathinfo['filename'] . '.' . $_pathinfo['extension'];
    $_file_io->createDestinationDir(Mage::getBaseDir() . DS . 'media' . DS . 'catalog' . DS . 'product' . DS . substr($_pathinfo['filename'],0,1) . DS . substr($_pathinfo['filename'],1,1) . DS);
    unset($_file_io);
    @copy($image, Mage::getBaseDir() . DS . 'media' . DS . 'catalog' . DS . 'product' . $_image_name_new);
    if ($position === 1) {
        $_table = Mage::getSingleton('core/resource')->getTableName('catalog_product_entity_varchar');
        $_attributes_ids_string = '';
        foreach ($_attributes_ids as $_attributes_id) {
            if ($_attributes_ids_string) {
                $_attributes_ids_string .= ',';
            }
            $_attributes_ids_string .= $_attributes_id['attribute_id'];
        }
        //Borramos los valores para los atributos de imagen que pudieran existir
        $_connection->query("DELETE FROM {$_table} WHERE entity_id = {$product_id} AND attribute_id IN ({$_attributes_ids_string});");
        //Asignamos los valores para los atributos de imagen
        foreach ($_attributes_ids as $_attributes_id) {
            $_connection->query("INSERT INTO {$_table} (entity_type_id, attribute_id, store_id, entity_id, value) VALUES (
                                    4,
                                    {$_attributes_id['attribute_id']},
                                    0,
                                    {$product_id},
                                    '{$_image_name_new}'
                                    );");
        }
    }
    //Insertamos las imágenes en la galería
    $_table = Mage::getSingleton('core/resource')->getTableName('eav/attribute');
    $_media_gallery_attribute = $_connection->fetchAll("SELECT attribute_id FROM {$_table} WHERE entity_type_id = 4 AND attribute_code IN ('media_gallery');");
    $_table = Mage::getSingleton('core/resource')->getTableName('catalog/product_attribute_media_gallery');
    $_connection->query("INSERT INTO {$_table} (attribute_id, entity_id, value) VALUES (
                                {$_media_gallery_attribute[0]['attribute_id']},
                                {$product_id},
                                '{$_image_name_new}'
                                );");
    $_table_values = Mage::getSingleton('core/resource')->getTableName('catalog/product_attribute_media_gallery_value');
    $_connection->query("INSERT INTO {$_table_values} (value_id, store_id, label, position, disabled) VALUES (
                                (SELECT value_id FROM {$_table} WHERE entity_id = {$product_id} AND value = '{$_image_name_new}'),
                                0,
                                '{$label}',
                                {$position},
                                0
                                );");
 
}

Al ejecutar el proceso, vamos a ver que las imágenes se cargaron, nuevamente, de forma correcta.

Imágenes agregadas vía SQL en Magento

Y dado que atacamos directo a la base de datos, no nos vamos a encontrar con problemas de reindexado por culpa de cualquier otro evento que pudiera ser disparado luego de actualizar un producto.