Compartir

Exprimiendo los extension attributes en Magento 2 #Codehacks

13 marzo, 2018

En este post explicaré como hacer un uso correcto de los extension attributes. Lo primero de todo es saber para qué sirve esta nueva funcionalidad de Magento 2.
Los extension attributes son usados para añadir nuevos atributos a modelos sin tener que modificar ni la clase ni la interfaz de ellos. Esto es aplicable tanto a modelos propios de Magento como de módulos de terceros. El único requisito es que la interfaz del modelo que vayamos a extender herede de  Magento \ Framework \ Api \ ExtensibleDataInterface y su clase de Magento \ Framework \ Model \ AbstractExtensibleModel.

Para explicar esta nueva funcionalidad, vamos a basarnos en una entidad propia, la cual explicamos en el post Creación de una nueva entidad en Magento 2.

Cabe destacar que todo el módulo sigue las recomendaciones estándar de PHP.

Creación de modelos, recursos y repositorios

Para el uso de los extension attributes tenemos 2 opciones, guardar estos nuevos atributos en la misma tabla de la base de datos o en otra distinta. El ejemplo que vamos a explicar es añadiendo la nueva información en una tabla.

Para ello, crearemos exactamente una nueva entidad con los mismos pasos que en el post Creación de una nueva entidad en Magento 2.. En nuesto caso, en el InstallSchema.php crearemos un campo que almacenará el id del modelo principal el cual será una clave foránea del campo del modelo principal.

app / code / Interactiv4 / CustomPost / Setup / InstallSchema.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
<?php
/**
* @author Interactiv4 Team
* @copyright  Copyright © Interactiv4 (https://www.interactiv4.com)
*/


namespace Interactiv4\CustomPost\Setup;

use Interactiv4\CustomPost\Api\Data\PostInterface;
use Interactiv4\Post\Api\Data\EntityInterface;
use Magento\Framework\DB\Adapter\AdapterInterface;
use Magento\Framework\DB\Ddl\Table;
use Magento\Framework\Setup\InstallSchemaInterface;
use Magento\Framework\Setup\ModuleContextInterface;
use Magento\Framework\Setup\SchemaSetupInterface;

/**
* Class InstallSchema
*/

class InstallSchema implements InstallSchemaInterface
{
   /**
    * {@inheritdoc}
    */

   public function install(SchemaSetupInterface $setup, ModuleContextInterface $context)
   {
       $installer = $setup;

       $installer->startSetup();

       //Custom post
       $installer->getConnection()->dropTable($installer->getTable(PostInterface::SCHEMA_TABLE));
       $this->installTableCustomPost($installer);

       $installer->endSetup();
   }


   /**
    * Create table relations between custom entity and custom post
    *
    * @param SchemaSetupInterface $installer
    * @throws \Zend_Db_Exception
    */

   private function installTableCustomPost(SchemaSetupInterface $installer)
   {
       $table = $installer->getConnection()->newTable(
           $installer->getTable(PostInterface::SCHEMA_TABLE)
       )->addColumn(
           PostInterface::FIELD_ID,
           Table::TYPE_INTEGER,
           null,
           ['identity' => true, 'unsigned' => true, 'nullable' => false, 'primary' => true],
           'Custom post Id'
       )->addColumn(
           PostInterface::FIELD_POST_ID,
           Table::TYPE_INTEGER,
           null,
           ['unsigned' => true, 'nullable' => false],
           'Post Id'
       )->addColumn(
           PostInterface::FIELD_SHORT_DESCRIPTION,
           Table::TYPE_TEXT,
           null,
           ['nullable' => false, 'default' => ''],
           'Short description'
       )->addIndex(
           $installer->getIdxName(
               $installer->getTable(PostInterface::SCHEMA_TABLE),
               [PostInterface::FIELD_POST_ID],
               AdapterInterface::INDEX_TYPE_UNIQUE
           ),
           [PostInterface::FIELD_POST_ID],
           ['type' => AdapterInterface::INDEX_TYPE_UNIQUE]
       )->addForeignKey(
           $installer->getFkName(
               PostInterface::SCHEMA_TABLE,
               PostInterface::FIELD_POST_ID,
               EntityInterface::TABLE,
               EntityInterface::ID
           ),
           PostInterface::FIELD_POST_ID,
           $installer->getTable(EntityInterface::TABLE),
           EntityInterface::ID,
           Table::ACTION_CASCADE
       )->setComment(
           'Custom Post'
       );

       $installer->getConnection()->createTable($table);
   }
}

Creación de extension attributes

Para añadir los extension attributes a un modelo ya creado, debemos primero crear un archivo extension_attributes.xml, en el cual especificaremos la interfaz del modelo principal y los campos que vamos a añadir.

app / code / Interactiv4 / CustomPost / etc / extension_attributes.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?xml version="1.0"?>
<!--
~ @author Interactiv4 Team
~ @copyright  Copyright © Interactiv4 (https://www.interactiv4.com)
-->

<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Api/etc/extension_attributes.xsd">
   <extension_attributes for="Interactiv4\Post\Api\Data\EntityInterface">
       <attribute code="short_description" type="string">
           <resources>
               <resource ref="Interactiv4_Post::entity"/>
           </resources>
           <join reference_table="interactiv4_custompost_post" reference_field="post_id" join_on_field="entity_id">
               <field>short_description</field>
           </join>
       </attribute>
   </extension_attributes>
</config>

En el ejemplo de arriba podemos ver, como le indicamos la interfaz del modelo principal, el nombre del nuevo atributo y su tipología (int, string, un objeto, array…).

El parámetro resources, es opcional y restringe el acceso al usuario con un permiso específico.

Cabe destacar la especificación del join, el cual es opcional. En este punto, especificamos nuestra nueva tabla y los campos que están referenciados entre sí entre el modelo principal y el nuestro. Esto sólo funcionará si en el método getList del repositorio principal está incluido el joinProcessor de los extension attributes (Magento \ Framework \ Api \ ExtensionAttribute \ JoinProcessorInterface).

app / code / Interactiv4 / Post / Model / EntityRepository.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
<?php
/**
* @author Interactiv4 Team
* @copyright Copyright (c) 2017 Interactiv4 (https://www.interactiv4.com)
* @package Interactiv4_Post
*/


namespace Interactiv4\Post\Model;

...
use Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface;

class EntityRepository implements EntityRepositoryInterface
{

...

   /**
    * EntityRepository constructor.
    *
    * @param EntityFactory $entityFactory
    * @param CollectionFactory $entityCollectionFactory
    * @param EntitySearchResultsInterfaceFactory $entitySearchResultsInterfaceFactory
    * @param CollectionProcessorInterface $collectionProcessor
    * @param JoinProcessorInterface $extensionAttributesJoinProcessor
    */

   public function __construct(
       EntityFactory $entityFactory,
       CollectionFactory $entityCollectionFactory,
       EntitySearchResultsInterfaceFactory $entitySearchResultsInterfaceFactory,
       CollectionProcessorInterface $collectionProcessor,
       JoinProcessorInterface $extensionAttributesJoinProcessor
   ) {
       $this->entityFactory = $entityFactory;
       $this->entityCollectionFactory = $entityCollectionFactory;
       $this->entitySearchResultsInterfaceFactory = $entitySearchResultsInterfaceFactory;
       $this->collectionProcessor = $collectionProcessor;
       $this->extensionAttributesJoinProcessor = $extensionAttributesJoinProcessor;
   }

   ...

   /**
    * @inheritdoc
    */

   public function getList(SearchCriteriaInterface $searchCriteria)
   {
       /** @var Collection $collection */
       $collection = $this->entityCollectionFactory->create();

       $this->extensionAttributesJoinProcessor->process($collection, EntityInterface::class);
       $this->collectionProcessor->process($searchCriteria, $collection);

       /** @var EntitySearchResultsInterface $searchResults */
       $searchResults = $this->entitySearchResultsInterfaceFactory->create();
       $searchResults->setSearchCriteria($searchCriteria);
       $searchResults->setItems($collection->getItems());
       $searchResults->setTotalCount($collection->getSize());

       return $searchResults;
   }
}

Gracias a este joinProcessor podemos definir en nuestro PostRepository.php una función getListPostByShortDescription() la cual obtiene un listado de todas las entidades del modelo principal filtrando por el valor de un extension attributes.

app / code / Interactiv4 / CustomPost / Model / PostRepository.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
<?php
/**
* @author Interactiv4 Team
* @copyright  Copyright © Interactiv4 (https://www.interactiv4.com)
*/


namespace Interactiv4\CustomPost\Model;

use Exception;
use Interactiv4\CustomPost\Api\Data\PostInterface;
use Interactiv4\CustomPost\Api\Data\PostSearchResultsInterface;
use Interactiv4\CustomPost\Api\Data\PostSearchResultsInterfaceFactory;
use Interactiv4\CustomPost\Api\PostRepositoryInterface;
use Interactiv4\CustomPost\Model\PostFactory;
use Interactiv4\CustomPost\Model\ResourceModel\Post as ResourceCustomPost;
use Interactiv4\CustomPost\Model\ResourceModel\Post\Collection;
use Interactiv4\CustomPost\Model\ResourceModel\Post\CollectionFactory;
use Interactiv4\Post\Api\Data\EntityInterface;
use Interactiv4\Post\Api\EntityRepositoryInterface;
use Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface;
use Magento\Framework\Api\FilterBuilder;
use Magento\Framework\Api\Search\SearchCriteria;
use Magento\Framework\Api\Search\SearchCriteriaBuilder;
use Magento\Framework\Api\SearchCriteria\CollectionProcessorInterface;
use Magento\Framework\Api\SearchCriteriaInterface;
use Magento\Framework\Api\SortOrder;
use Magento\Framework\Exception\CouldNotSaveException;
use Magento\Framework\Exception\NoSuchEntityException;
use Magento\Framework\Exception\StateException;
use Magento\Framework\Exception\ValidatorException;

/**
* Class PostRepository
*/

class PostRepository implements PostRepositoryInterface
{
   /**
    * @var ResourceCustomPost $resourceCustomPost
    */

   private $resourceCustomPost;

   /**
    * @var PostFactory
    */

   private $customPostFactory;

   /**
    * @var CollectionFactory
    */

   private $customPostCollectionFactory;

   /**
    * @var PostSearchResultsInterfaceFactory
    */

   private $customPostSearchResultInterfaceFactory;

   /**
    * @var CollectionProcessorInterface
    */

   private $collectionProcessor;

   /**
    * @var JoinProcessorInterface
    */

   private $extensionAttributesJoinProcessor;

   /**
    * @var EntityRepositoryInterface
    */

   private $entityRepository;

   /**
    * @var FilterBuilder
    */

   private $filterBuilder;

   /**
    * @var SearchCriteriaBuilder
    */

   private $searchCriteriaBuilder;

   /**
    * @var SortOrder
    */

   private $sortOrder;

   /**
    * PostRepository constructor.
    *
    * @param ResourceCustomPost $resourceCustomPost
    * @param PostFactory $customPostFactory
    * @param CollectionFactory $customPostCollectionFactory
    * @param PostSearchResultsInterfaceFactory $customPostSearchResultInterfaceFactory
    * @param CollectionProcessorInterface $collectionProcessor
    * @param JoinProcessorInterface $extensionAttributesJoinProcessor
    * @param EntityRepositoryInterface $entityRepository
    * @param FilterBuilder $filterBuilder
    * @param SearchCriteriaBuilder $searchCriteriaBuilder
    * @param SortOrder $sortOrder
    */

   public function __construct(
       ResourceCustomPost $resourceCustomPost,
       PostFactory $customPostFactory,
       CollectionFactory $customPostCollectionFactory,
       PostSearchResultsInterfaceFactory $customPostSearchResultInterfaceFactory,
       CollectionProcessorInterface $collectionProcessor,
       JoinProcessorInterface $extensionAttributesJoinProcessor,
       EntityRepositoryInterface $entityRepository,
       FilterBuilder $filterBuilder,
       SearchCriteriaBuilder $searchCriteriaBuilder,
       SortOrder $sortOrder
   ) {
       $this->resourceCustomPost = $resourceCustomPost;
       $this->customPostFactory = $customPostFactory;
       $this->customPostCollectionFactory = $customPostCollectionFactory;
       $this->customPostSearchResultInterfaceFactory = $customPostSearchResultInterfaceFactory;
       $this->collectionProcessor = $collectionProcessor;
       $this->extensionAttributesJoinProcessor = $extensionAttributesJoinProcessor;
       $this->entityRepository = $entityRepository;
       $this->filterBuilder = $filterBuilder;
       $this->searchCriteriaBuilder = $searchCriteriaBuilder;
       $this->sortOrder = $sortOrder;
   }

   /**
    * @inheritdoc
    */

   public function save(PostInterface $customPost)
   {
       $this->resourceCustomPost->save($customPost);

       return $customPost;
   }

   /**
    * @inheritdoc
    */

   public function getById($customPostId)
   {
       return $this->get($customPostId);
   }

   /**
    * @inheritdoc
    */

   public function get($value, $attributeCode = null)
   {
       /** @var Post $customPost */
       $customPost = $this->customPostFactory->create()->load($value, $attributeCode);

       if (!$customPost->getId()) {
           throw new NoSuchEntityException(__('Unable to find custom post'));
       }

       return $customPost;
   }

   /**
    * @inheritdoc
    */

   public function delete(PostInterface $customPost)
   {
       $customPostId = $customPost->getId();
       try {
           $this->resourceCustomPost->delete($customPost);
       } catch (ValidatorException $e) {
           throw new CouldNotSaveException(__($e->getMessage()));
       } catch (Exception $e) {
           throw new StateException(
               __('Unable to remove custom post %1', $customPostId)
           );
       }

       return true;
   }

   /**
    * @inheritdoc
    */

   public function deleteById($customPostId)
   {
       $customPost = $this->getById($customPostId);

       return $this->delete($customPost);
   }

   /**
    * @inheritdoc
    */

   public function getList(SearchCriteriaInterface $searchCriteria)
   {
       /** @var Collection $collection */
       $collection = $this->customPostCollectionFactory->create();

       $this->extensionAttributesJoinProcessor->process($collection, PostInterface::class);
       $this->collectionProcessor->process($searchCriteria, $collection);

       /** @var PostSearchResultsInterface $searchResults */
       $searchResults = $this->customPostSearchResultInterfaceFactory->create();
       $searchResults->setSearchCriteria($searchCriteria);
       $searchResults->setItems($collection->getItems());
       $searchResults->setTotalCount($collection->getSize());

       return $searchResults;
   }

   /**
    * @inheritdoc
    */

   public function getListPostByShortDescription($shortDescription)
   {

       $filter = $this->filterBuilder
           ->setField(PostInterface::FIELD_SHORT_DESCRIPTION)
           ->setValue($shortDescription)
           ->setConditionType('LIKE')
           ->create();

       /** @var SearchCriteria $searchCriteria */
       $searchCriteria = $this->searchCriteriaBuilder->addFilter($filter);

       $sortOrder = $this->sortOrder->setField(EntityInterface::NAME)->setDirection('asc');
       $searchCriteria->setSortOrders([$sortOrder]);
       $entitySearchResults = $this->entityRepository->getList($searchCriteria);

       return $entitySearchResults;
   }
}

Gracias a la definición del archivo extension_attributes.xml, Magento generará en la carpeta var / generation (< Magento 2.2) o generated (> Magento 2.2) una clase EntityExtensionInterface.php con los getters y setters de los atributos que hayamos definido en dicho archivo xml.

var / generation / Interactiv4 / Post / Api / Data / EntityExtensionInterface.php ó
generated / code / Interactiv4 / Post / Api / Data / EntityExtensionInterface.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php
/**
* @author Interactiv4 Team
* @copyright  Copyright © Interactiv4 (https://www.interactiv4.com)
*/


namespace Interactiv4\Post\Api\Data;

/**
* ExtensionInterface class for @see \Interactiv4\Post\Api\Data\EntityInterface
*/

interface EntityExtensionInterface extends \Magento\Framework\Api\ExtensionAttributesInterface
{
   /**
    * @return string|null
    */

   public function getShortDescription();

   /**
    * @param string $shortDescription
    * @return $this
    */

   public function setShortDescription($shortDescription);
}

Obtener y almacenar información de extension attributes

Plugins

Magento no almacena ni obtiene la información de los extension attributes de manera automática. Tenemos que obtenerla y guardarla de manera manual haciendo uso de los plugins en las funciones get(), getList() y save() del repositorio principal.

app / etc / di.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ @author Interactiv4 Team
~ @copyright  Copyright © Interactiv4 (https://www.interactiv4.com)
-->

<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
   <preference for="Interactiv4\CustomPost\Api\Data\PostInterface" type="Interactiv4\CustomPost\Model\Post" />
   <preference for="Interactiv4\CustomPost\Api\Data\PostSearchResultsInterface" type="Interactiv4\CustomPost\Model\PostSearchResults" />
   <preference for="Interactiv4\CustomPost\Api\PostRepositoryInterface" type="Interactiv4\CustomPost\Model\PostRepository" />
   <type name="Interactiv4\Post\Api\EntityRepositoryInterface">
       <plugin name="interactiv4_custompost_plugin_entity_repository" type="Interactiv4\CustomPost\Plugin\EntityRepositoryPlugin"/>
   </type>
</config>

app / code / Plugin / EntityRepositoryPlugin.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
<?php
/**
* @author Interactiv4 Team
* @copyright  Copyright © Interactiv4 (https://www.interactiv4.com)
*/


namespace Interactiv4\CustomPost\Plugin;

use Interactiv4\CustomPost\Api\Data\PostInterface;
use Interactiv4\CustomPost\Api\Data\PostInterfaceFactory;
use Interactiv4\CustomPost\Api\PostRepositoryInterface;
use Interactiv4\Post\Api\Data\EntityExtensionFactory;
use Interactiv4\Post\Api\Data\EntityExtensionInterface;
use Interactiv4\Post\Api\Data\EntityInterface;
use Interactiv4\Post\Api\Data\EntitySearchResultsInterface;
use Interactiv4\Post\Api\EntityRepositoryInterface;
use Magento\Framework\Exception\CouldNotSaveException;
use Magento\Framework\Exception\NoSuchEntityException;

/**
* Class EntityRepositoryPlugin
*/

class EntityRepositoryPlugin
{
   /**
    * @var EntityExtensionFactory
    */

   private $entityExtensionFactory;

   /**
    * @var PostInterfaceFactory
    */

   private $customPostFactory;

   /**
    * @var PostRepositoryInterface
    */

   private $customPostRepository;

   /**
    * PostGet constructor.
    *
    * @param EntityExtensionFactory $entityExtensionFactory
    * @param PostInterfaceFactory $customPostFactory
    * @param PostRepositoryInterface $customPostRepository
    */

   public function __construct(
       EntityExtensionFactory $entityExtensionFactory,
       PostInterfaceFactory $customPostFactory,
       PostRepositoryInterface $customPostRepository
   ) {
       $this->entityExtensionFactory = $entityExtensionFactory;
       $this->customPostFactory = $customPostFactory;
       $this->customPostRepository = $customPostRepository;
   }

   /**
    * @param EntityRepositoryInterface $subject
    * @param EntityInterface $result
    * @return EntityInterface
    */

   public function afterGet(
       EntityRepositoryInterface $subject,
       EntityInterface $result
   ) {
       /** @var EntityExtensionInterface $extensionAttributes */
       $extensionAttributes = $result->getExtensionAttributes() ?: $this->entityExtensionFactory->create();

       try {
           /** @var PostInterface $customPost */
           $customPost = $this->customPostRepository->get($result->getId(), PostInterface::FIELD_POST_ID);
       } catch (NoSuchEntityException $e) {
           $result->setExtensionAttributes($extensionAttributes);

           return $result;
       }
       $extensionAttributes->setShortDescription($customPost->getShortDescription());

       $result->setExtensionAttributes($extensionAttributes);

       return $result;
   }

   /**
    * @param EntityRepositoryInterface $subject
    * @param EntitySearchResultsInterface $entities
    * @return EntitySearchResultsInterface
    */

   public function afterGetList(
       EntityRepositoryInterface $subject,
       EntitySearchResultsInterface $entities
   ) {
       foreach ($entities->getItems() as $entity) {
           $this->afterGet($subject, $entity);
       }

       return $entities;
   }

   /**
    * @param EntityRepositoryInterface $subject
    * @param EntityInterface $result
    * @return EntityInterface
    * @throws CouldNotSaveException
    */

   public function afterSave(
       EntityRepositoryInterface $subject,
       EntityInterface $result
   ) {
       $extensionAttributes = $result->getExtensionAttributes() ?: $this->entityExtensionFactory->create();
       if ($extensionAttributes !== null &&
           $extensionAttributes->getShortDescription() !== null
       ) {
           /** @var PostInterface $customPost */
           try {
               $customPost = $this->customPostRepository->get($result->getId(), PostInterface::FIELD_POST_ID);
           } catch (NoSuchEntityException $e) {
               $customPost = $this->customPostFactory->create();
           }
           $customPost->setPostId($result->getId());
           $customPost->setShortDescription($extensionAttributes->getShortDescription());
           $this->customPostRepository->save($customPost);
       }

       return $result;
   }
}

De esta manera conseguimos que cada vez que se cargue un modelo o un listado, contenga la información adicional de los extension attributes. A su vez, cada vez que guardemos la información de un producto, también guardaremos la información adicional.

Espero que os haya sido de ayuda y cualquier duda podéis dejar un comentario.

En este link podéis descargar el código de esta explicación y en este otrolink la entidad principal de la que vamos a extender.

Categoría
Etiquetas
Autor

Compartir

Suscríbete a nuestra newsletter

También te puede interesar

Usamos cookies de terceros para mejorar nuestros servicios y obtener datos estadísticos de tus hábitos de navegación. Si continúas navegando consideramos que aceptas su uso. Puedes obtener más información en Política de privacidad y cookies