From 71de4cc80cc369fbaacaf906fd74747854013317 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Nicolas=20B=C3=A9jean?= <nicolas@bejean.eu>
Date: Fri, 8 May 2020 17:11:30 +0200
Subject: [PATCH] =?UTF-8?q?D=C3=A9veloppement=20des=20models?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../CategoryWidgetStoreFilter.php             |  34 +++
 Model/CategoryWidget.php                      | 245 ++++++++++++++++
 Model/CategoryWidget/DataProvider.php         |  94 ++++++
 Model/CategoryWidget/Source/Enabled.php       |  52 ++++
 Model/CategoryWidgetRepository.php            | 206 ++++++++++++++
 Model/Config/Source/CategoryWidget.php        |  50 ++++
 Model/GetCategoryWidgetByIdentifier.php       |  59 ++++
 Model/Resolver/CategoryWidget/Identity.php    |  47 +++
 Model/Resolver/CategoryWidgets.php            |  96 +++++++
 .../Resolver/DataProvider/CategoryWidget.php  |  75 +++++
 Model/ResourceModel/AbstractCollection.php    | 222 +++++++++++++++
 Model/ResourceModel/CategoryWidget.php        | 269 ++++++++++++++++++
 .../CategoryWidget/Collection.php             | 103 +++++++
 .../CategoryWidget/Grid/Collection.php        | 158 ++++++++++
 .../Relation/Store/ReadHandler.php            |  53 ++++
 .../Relation/Store/SaveHandler.php            |  84 ++++++
 Model/Template/Filter.php                     |  36 +++
 Model/Template/FilterProvider.php             |  74 +++++
 18 files changed, 1957 insertions(+)
 create mode 100755 Model/Api/SearchCriteria/CollectionProcessor/FilterProcessor/CategoryWidgetStoreFilter.php
 create mode 100755 Model/CategoryWidget.php
 create mode 100755 Model/CategoryWidget/DataProvider.php
 create mode 100755 Model/CategoryWidget/Source/Enabled.php
 create mode 100755 Model/CategoryWidgetRepository.php
 create mode 100755 Model/Config/Source/CategoryWidget.php
 create mode 100755 Model/GetCategoryWidgetByIdentifier.php
 create mode 100644 Model/Resolver/CategoryWidget/Identity.php
 create mode 100644 Model/Resolver/CategoryWidgets.php
 create mode 100644 Model/Resolver/DataProvider/CategoryWidget.php
 create mode 100755 Model/ResourceModel/AbstractCollection.php
 create mode 100755 Model/ResourceModel/CategoryWidget.php
 create mode 100755 Model/ResourceModel/CategoryWidget/Collection.php
 create mode 100755 Model/ResourceModel/CategoryWidget/Grid/Collection.php
 create mode 100755 Model/ResourceModel/CategoryWidget/Relation/Store/ReadHandler.php
 create mode 100755 Model/ResourceModel/CategoryWidget/Relation/Store/SaveHandler.php
 create mode 100755 Model/Template/Filter.php
 create mode 100755 Model/Template/FilterProvider.php

diff --git a/Model/Api/SearchCriteria/CollectionProcessor/FilterProcessor/CategoryWidgetStoreFilter.php b/Model/Api/SearchCriteria/CollectionProcessor/FilterProcessor/CategoryWidgetStoreFilter.php
new file mode 100755
index 0000000..8b4ce06
--- /dev/null
+++ b/Model/Api/SearchCriteria/CollectionProcessor/FilterProcessor/CategoryWidgetStoreFilter.php
@@ -0,0 +1,34 @@
+<?php
+namespace NicolasBejean\CategoryWidget\Model\Api\SearchCriteria\CollectionProcessor\FilterProcessor;
+
+use \Magento\Framework\Api\Filter;
+use \Magento\Framework\Api\SearchCriteria\CollectionProcessor\FilterProcessor\CustomFilterInterface;
+use \Magento\Framework\Data\Collection\AbstractDb;
+use \NicolasBejean\CategoryWidget\Model\ResourceModel\CategoryWidget\Collection;
+
+/**
+ * Class ImageStoreFilter
+ *
+ * @category PHP
+ * @package  NicolasBejean\CategoryWidget\Model\Api\SearchCriteria\CollectionProcessor\FilterProcessor
+ * @author   Nicolas Béjean <nicolas@bejean.eu>
+ * @license  https://github.com/nicolasbejean/category-widget/blob/master/licence.txt BSD Licence
+ * @link     https://www.bejean.eu
+ */
+class CategoryWidgetStoreFilter implements CustomFilterInterface
+{
+    /**
+     * Apply custom store filter to collection
+     *
+     * @param Filter $filter
+     * @param AbstractDb $collection
+     * @return bool
+     */
+    public function apply(Filter $filter, AbstractDb $collection)
+    {
+        /** @var Collection $collection */
+        $collection->addStoreFilter($filter->getValue(), false);
+
+        return true;
+    }
+}
diff --git a/Model/CategoryWidget.php b/Model/CategoryWidget.php
new file mode 100755
index 0000000..4a65b23
--- /dev/null
+++ b/Model/CategoryWidget.php
@@ -0,0 +1,245 @@
+<?php
+namespace NicolasBejean\CategoryWidget\Model;
+
+use \Magento\Framework\Exception\LocalizedException;
+use \NicolasBejean\CategoryWidget\Api\Data\CategoryWidgetInterface;
+use \Magento\Framework\DataObject\IdentityInterface;
+use \Magento\Framework\Model\AbstractModel;
+
+/**
+ * Class CategoryWidget
+ *
+ * @category PHP
+ * @package  NicolasBejean\CategoryWidget\Model
+ * @author   Nicolas Béjean <nicolas@bejean.eu>
+ * @license  https://github.com/nicolasbejean/category-widget/blob/master/licence.txt BSD Licence
+ * @link     https://www.bejean.eu
+ */
+class CategoryWidget extends AbstractModel implements CategoryWidgetInterface, IdentityInterface
+{
+    /**
+     * Category Widget Cache tag
+     *
+     * @var string
+     */
+    const CACHE_TAG = 'categorywidget';
+
+    /**
+     * Category Widget statuses
+     */
+    const STATUS_ENABLED = 1;
+    const STATUS_DISABLED = 0;
+
+    /**
+     * Prefix of model events names
+     *
+     * @var string
+     */
+    protected $eventPrefix = 'categorywidget';
+
+    /**
+     * @return void
+     */
+    protected function _construct()
+    {
+        $this->_init(ResourceModel\CategoryWidget::class);
+    }
+
+    /**
+     * Prevent category widgetrecursion
+     *
+     * @return AbstractModel
+     * @throws LocalizedException
+     */
+    public function beforeSave()
+    {
+        if ($this->hasDataChanges()) {
+            $this->setUpdateTime(null);
+        }
+
+        $needle = 'entity_id="' . $this->getId() . '"';
+        if (false == strstr($this->getIdentifier(), $needle)) {
+            return parent::beforeSave();
+        }
+        throw new LocalizedException(
+            __('Make sure that category widget path does not reference the category widget itself.')
+        );
+    }
+
+    /**
+     * Get identities
+     *
+     * @return array
+     */
+    public function getIdentities()
+    {
+        return [self::CACHE_TAG . '_' . $this->getId(), self::CACHE_TAG . '_' . $this->getIdentifier()];
+    }
+
+    /**
+     * Retrieve category widgetid
+     *
+     * @return int
+     */
+    public function getId()
+    {
+        return $this->getData(self::ENTITY_ID);
+    }
+
+    /**
+     * Retrieve category widget identifier
+     *
+     * @return string
+     */
+    public function getIdentifier()
+    {
+        return (string)$this->getData(self::IDENTIFIER);
+    }
+
+    /**
+     * Retrieve category widget name
+     *
+     * @return string
+     */
+    public function getName()
+    {
+        return $this->getData(self::NAME);
+    }
+
+    /**
+     * Retrieve category widget content
+     *
+     * @return string
+     */
+    public function getContent()
+    {
+        return $this->getData(self::CONTENT);
+    }
+
+    /**
+     * Retrieve category widget creation time
+     *
+     * @return string
+     */
+    public function getCreationTime()
+    {
+        return $this->getData(self::CREATION_TIME);
+    }
+
+    /**
+     * Retrieve category widget update time
+     *
+     * @return string
+     */
+    public function getUpdateTime()
+    {
+        return $this->getData(self::UPDATE_TIME);
+    }
+
+    /**
+     * Is active
+     *
+     * @return bool
+     */
+    public function isActive()
+    {
+        return (bool)$this->getData(self::IS_ACTIVE);
+    }
+
+    /**
+     * Set ID
+     *
+     * @param int $id
+     * @return CategoryWidgetInterface
+     */
+    public function setId($id)
+    {
+        return $this->setData(self::ENTITY_ID, $id);
+    }
+
+    /**
+     * Set path
+     *
+     * @param string $identifier
+     * @return CategoryWidgetInterface
+     */
+    public function setIdentifier($identifier)
+    {
+        return $this->setData(self::IDENTIFIER, $identifier);
+    }
+
+    /**
+     * Set Name
+     *
+     * @param string $name
+     * @return CategoryWidgetInterface
+     */
+    public function setName($name)
+    {
+        return $this->setData(self::NAME, $name);
+    }
+
+    /**
+     * Set Content
+     *
+     * @param string $content
+     * @return CategoryWidgetInterface
+     */
+    public function setContent($content)
+    {
+        return $this->setData(self::CONTENT, $content);
+    }
+
+    /**
+     * Set creation time
+     *
+     * @param string $creationTime
+     * @return CategoryWidgetInterface
+     */
+    public function setCreationTime($creationTime)
+    {
+        return $this->setData(self::CREATION_TIME, $creationTime);
+    }
+
+    /**
+     * Set update time
+     *
+     * @param string $updateTime
+     * @return CategoryWidgetInterface
+     */
+    public function setUpdateTime($updateTime)
+    {
+        return $this->setData(self::UPDATE_TIME, $updateTime);
+    }
+
+    /**
+     * Set is active
+     *
+     * @param bool|int $isActive
+     * @return CategoryWidgetInterface
+     */
+    public function setIsActive($isActive)
+    {
+        return $this->setData(self::IS_ACTIVE, $isActive);
+    }
+
+    /**
+     * Receive slider store ids
+     *
+     * @return int[]
+     */
+    public function getStores()
+    {
+        return $this->hasData('stores') ? $this->getData('stores') : $this->getData('store_id');
+    }
+
+    /**
+     * Prepare category widget statuses.
+     *
+     * @return array
+     */
+    public function getAvailableStatuses()
+    {
+        return [self::STATUS_ENABLED => __('Enabled'), self::STATUS_DISABLED => __('Disabled')];
+    }
+}
diff --git a/Model/CategoryWidget/DataProvider.php b/Model/CategoryWidget/DataProvider.php
new file mode 100755
index 0000000..e9720c3
--- /dev/null
+++ b/Model/CategoryWidget/DataProvider.php
@@ -0,0 +1,94 @@
+<?php
+namespace NicolasBejean\CategoryWidget\Model\CategoryWidget;
+
+use \NicolasBejean\CategoryWidget\Model\CategoryWidget;
+use \NicolasBejean\CategoryWidget\Model\ResourceModel\CategoryWidget\Collection;
+use \NicolasBejean\CategoryWidget\Model\ResourceModel\CategoryWidget\CollectionFactory;
+use \Magento\Framework\App\Request\DataPersistorInterface;
+use \Magento\Ui\DataProvider\AbstractDataProvider;
+
+/**
+ * Class DataProvider
+ *
+ * @category PHP
+ * @package  NicolasBejean\CategoryWidget\Model\CategoryWidget
+ * @author   Nicolas Béjean <nicolas@bejean.eu>
+ * @license  https://github.com/nicolasbejean/category-widget/blob/master/licence.txt BSD Licence
+ * @link     https://www.bejean.eu
+ */
+class DataProvider extends AbstractDataProvider
+{
+    /**
+     * @var Collection
+     */
+    protected $collection;
+
+    /**
+     * @var DataPersistorInterface
+     */
+    protected $dataPersistor;
+
+    /**
+     * @var array
+     */
+    protected $loadedData;
+
+    /**
+     * Constructor
+     *
+     * @param string                    $name
+     * @param string                    $primaryFieldName
+     * @param string                    $requestFieldName
+     * @param CollectionFactory         $categoryWidgetCollectionFactory
+     * @param DataPersistorInterface    $dataPersistor
+     * @param array                     $meta
+     * @param array                     $data
+     */
+    public function __construct(
+        string                          $name,
+        string                          $primaryFieldName,
+        string                          $requestFieldName,
+        CollectionFactory               $categoryWidgetCollectionFactory,
+        DataPersistorInterface          $dataPersistor,
+        array $meta                     = [],
+        array $data                     = []
+    ) {
+        $this->collection               = $categoryWidgetCollectionFactory->create();
+        $this->dataPersistor            = $dataPersistor;
+
+        parent::__construct($name, $primaryFieldName, $requestFieldName, $meta, $data);
+    }
+
+    /**
+     * Get data
+     *
+     * @return array
+     */
+    public function getData()
+    {
+        if (isset($this->loadedData)) {
+            return $this->loadedData;
+        }
+        $items = $this->collection->getItems();
+        /** @var CategoryWidget $categoryWidget */
+        foreach ($items as $categoryWidget) {
+            $this->loadedData[$categoryWidget->getId()] = $categoryWidget->getData();
+            if ($categoryWidget->getIdentifier()) {
+                $m['identifier'] = $categoryWidget->getIdentifier();
+                $fullData = $this->loadedData;
+                $this->loadedData[$categoryWidget->getId()] = array_merge($fullData[$categoryWidget->getId()], $m);
+            }
+        }
+
+        $data = $this->dataPersistor->get('nicolasbejean_categorywidget');
+
+        if (!empty($data)) {
+            $categoryWidget = $this->collection->getNewEmptyItem();
+            $categoryWidget->setData($data);
+            $this->loadedData[$categoryWidget->getId()] = $categoryWidget->getData();
+            $this->dataPersistor->clear('nicolasbejean_categorywidget');
+        }
+
+        return $this->loadedData;
+    }
+}
diff --git a/Model/CategoryWidget/Source/Enabled.php b/Model/CategoryWidget/Source/Enabled.php
new file mode 100755
index 0000000..39e56fd
--- /dev/null
+++ b/Model/CategoryWidget/Source/Enabled.php
@@ -0,0 +1,52 @@
+<?php
+namespace NicolasBejean\CategoryWidget\Model\CategoryWidget\Source;
+
+use \Magento\Framework\Data\OptionSourceInterface;
+use \NicolasBejean\CategoryWidget\Model\CategoryWidget;
+
+/**
+ * Class Enabled
+ *
+ * @category PHP
+ * @package  NicolasBejean\CategoryWidget\Model\CategoryWidget\Source
+ * @author   Nicolas Béjean <nicolas@bejean.eu>
+ * @license  https://github.com/nicolasbejean/category-widget/blob/master/licence.txt BSD Licence
+ * @link     https://www.bejean.eu
+ */
+class Enabled implements OptionSourceInterface
+{
+    /**
+     * @var CategoryWidget
+     */
+    protected $categoryWidget;
+
+    /**
+     * Constructor
+     *
+     * @param CategoryWidget   $categoryWidget
+     */
+    public function __construct(
+        CategoryWidget         $categoryWidget
+    )
+    {
+        $this->categoryWidget  = $categoryWidget;
+    }
+
+    /**
+     * Get options
+     *
+     * @return array
+     */
+    public function toOptionArray()
+    {
+        $availableOptions = $this->categoryWidget->getAvailableStatuses();
+        $options = [];
+        foreach ($availableOptions as $key => $value) {
+            $options[] = [
+                'label' => $value,
+                'value' => $key,
+            ];
+        }
+        return $options;
+    }
+}
diff --git a/Model/CategoryWidgetRepository.php b/Model/CategoryWidgetRepository.php
new file mode 100755
index 0000000..bd9c657
--- /dev/null
+++ b/Model/CategoryWidgetRepository.php
@@ -0,0 +1,206 @@
+<?php
+namespace NicolasBejean\CategoryWidget\Model;
+
+use \Magento\Framework\Api\SearchCriteriaInterface;
+use \Magento\Framework\Exception\LocalizedException;
+use \NicolasBejean\CategoryWidget\Api\Data\CategoryWidgetInterface;
+use \NicolasBejean\CategoryWidget\Api\Data\CategoryWidgetInterfaceFactory;
+use \NicolasBejean\CategoryWidget\Api\Data\CategoryWidgetSearchResultsInterface;
+use \NicolasBejean\CategoryWidget\Api\CategoryWidgetRepositoryInterface;
+use \NicolasBejean\CategoryWidget\Api\Data;
+use \NicolasBejean\CategoryWidget\Model\ResourceModel\CategoryWidget as Resource;
+use \NicolasBejean\CategoryWidget\Model\ResourceModel\CategoryWidget\Collection;
+use \NicolasBejean\CategoryWidget\Model\ResourceModel\CategoryWidget\CollectionFactory as CategoryWidgetCollectionFactory;
+use \Magento\Framework\Api\DataObjectHelper;
+use \Magento\Framework\Api\SearchCriteria\CollectionProcessorInterface;
+use \Magento\Framework\Exception\CouldNotDeleteException;
+use \Magento\Framework\Exception\CouldNotSaveException;
+use \Magento\Framework\Exception\NoSuchEntityException;
+use \Magento\Framework\Reflection\DataObjectProcessor;
+use \Magento\Store\Model\StoreManagerInterface;
+use \Exception;
+
+/**
+ * Class CategoryWidgetRepository
+ *
+ * @category PHP
+ * @package  NicolasBejean\CategoryWidget\Model
+ * @author   Nicolas Béjean <nicolas@bejean.eu>
+ * @license  https://github.com/nicolasbejean/category-widget/blob/master/licence.txt BSD Licence
+ * @link     https://www.bejean.eu
+ */
+class CategoryWidgetRepository implements CategoryWidgetRepositoryInterface
+{
+    /**
+     * @var Resource
+     */
+    protected $resource;
+
+    /**
+     * @var CategoryWidgetFactory
+     */
+    protected $categoryWidgetFactory;
+
+    /**
+     * @var CategoryWidgetCollectionFactory
+     */
+    protected $categoryWidgetCollectionFactory;
+
+    /**
+     * @var Data\CategoryWidgetSearchResultsInterfaceFactory
+     */
+    protected $searchResultsFactory;
+
+    /**
+     * @var DataObjectHelper
+     */
+    protected $dataObjectHelper;
+
+    /**
+     * @var DataObjectProcessor
+     */
+    protected $dataObjectProcessor;
+
+    /**
+     * @var CategoryWidgetInterfaceFactory
+     */
+    protected $dataCategoryWidgetFactory;
+
+    /**
+     * @var StoreManagerInterface
+     */
+    private $storeManager;
+
+    /**
+     * @var CollectionProcessorInterface
+     */
+    private $collectionProcessor;
+
+    /**
+     * @param Resource                                          $resource
+     * @param CategoryWidgetFactory                                $categoryWidgetFactory
+     * @param CategoryWidgetInterfaceFactory                       $dataCategoryWidgetFactory
+     * @param CategoryWidgetCollectionFactory                      $categoryWidgetCollectionFactory
+     * @param Data\CategoryWidgetSearchResultsInterfaceFactory     $searchResultsFactory
+     * @param DataObjectHelper                                  $dataObjectHelper
+     * @param DataObjectProcessor                               $dataObjectProcessor
+     * @param StoreManagerInterface                             $storeManager
+     * @param CollectionProcessorInterface                      $collectionProcessor
+     */
+    public function __construct(
+        Resource                                                $resource,
+        CategoryWidgetFactory                                      $categoryWidgetFactory,
+        CategoryWidgetInterfaceFactory                             $dataCategoryWidgetFactory,
+        CategoryWidgetCollectionFactory                            $categoryWidgetCollectionFactory,
+        Data\CategoryWidgetSearchResultsInterfaceFactory           $searchResultsFactory,
+        DataObjectHelper                                        $dataObjectHelper,
+        DataObjectProcessor                                     $dataObjectProcessor,
+        StoreManagerInterface                                   $storeManager,
+        CollectionProcessorInterface                            $collectionProcessor = null
+    ) {
+        $this->resource                                         = $resource;
+        $this->categoryWidgetFactory                               = $categoryWidgetFactory;
+        $this->categoryWidgetCollectionFactory                     = $categoryWidgetCollectionFactory;
+        $this->searchResultsFactory                             = $searchResultsFactory;
+        $this->dataObjectHelper                                 = $dataObjectHelper;
+        $this->dataCategoryWidgetFactory                           = $dataCategoryWidgetFactory;
+        $this->dataObjectProcessor                              = $dataObjectProcessor;
+        $this->storeManager                                     = $storeManager;
+        $this->collectionProcessor                              = $collectionProcessor;
+    }
+
+    /**
+     * Save Category Widget data
+     *
+     * @param CategoryWidgetInterface $categoryWidget
+     * @return CategoryWidgetInterface
+     * @throws CouldNotSaveException
+     * @throws NoSuchEntityException
+     */
+    public function save(CategoryWidgetInterface $categoryWidget)
+    {
+        if (empty($categoryWidget->getStoreId())) {
+            $categoryWidget->setStoreId($this->storeManager->getStore()->getId());
+        }
+
+        try {
+            $this->resource->save($categoryWidget);
+        } catch (Exception $exception) {
+            throw new CouldNotSaveException(__($exception->getMessage()));
+        }
+        return $categoryWidget;
+    }
+
+    /**
+     * Load Category Widget data by given Image Identity
+     *
+     * @param string $id
+     * @return CategoryWidget
+     * @throws NoSuchEntityException
+     * @throws LocalizedException
+     */
+    public function getById($id)
+    {
+        /** @var CategoryWidget $categoryWidget */
+        $categoryWidget = $this->categoryWidgetFactory->create();
+        $this->resource->load($categoryWidget, $id);
+        if (!$categoryWidget->getId()) {
+            throw new NoSuchEntityException(__('The category widget with the \'%1\' ID doesn\'t exist.', $id));
+        }
+        return $categoryWidget;
+    }
+
+    /**
+     * Load Category Widget data collection by given search criteria
+     *
+     * @SuppressWarnings(PHPMD.CyclomaticComplexity)
+     * @SuppressWarnings(PHPMD.NPathComplexity)
+     * @param SearchCriteriaInterface $criteria
+     * @return CategoryWidgetSearchResultsInterface
+     */
+    public function getList(SearchCriteriaInterface $criteria)
+    {
+        /** @var Collection $collection */
+        $collection = $this->categoryWidgetCollectionFactory->create();
+
+        $this->collectionProcessor->process($criteria, $collection);
+
+        /** @var CategoryWidgetSearchResultsInterface $searchResults */
+        $searchResults = $this->searchResultsFactory->create();
+        $searchResults->setSearchCriteria($criteria);
+        $searchResults->setItems($collection->getItems());
+        $searchResults->setTotalCount($collection->getSize());
+        return $searchResults;
+    }
+
+    /**
+     * Delete Category Widget
+     *
+     * @param CategoryWidgetInterface $categoryWidget
+     * @return bool
+     * @throws CouldNotDeleteException
+     */
+    public function delete(CategoryWidgetInterface $categoryWidget)
+    {
+        try {
+            $this->resource->delete($categoryWidget);
+        } catch (Exception $exception) {
+            throw new CouldNotDeleteException(__($exception->getMessage()));
+        }
+        return true;
+    }
+
+    /**
+     * Delete Category Widget by given Category Widget Identity
+     *
+     * @param string $id
+     * @return bool
+     * @throws CouldNotDeleteException
+     * @throws NoSuchEntityException
+     * @throws LocalizedException
+     */
+    public function deleteById($id)
+    {
+        return $this->delete($this->getById($id));
+    }
+}
diff --git a/Model/Config/Source/CategoryWidget.php b/Model/Config/Source/CategoryWidget.php
new file mode 100755
index 0000000..eafc924
--- /dev/null
+++ b/Model/Config/Source/CategoryWidget.php
@@ -0,0 +1,50 @@
+<?php
+declare(strict_types=1);
+
+namespace NicolasBejean\CategoryWidget\Model\Config\Source;
+
+use \NicolasBejean\CategoryWidget\Model\ResourceModel\CategoryWidget\CollectionFactory;
+use \Magento\Framework\Data\OptionSourceInterface;
+
+/**
+ * Class CategoryWidget
+ *
+ * @category PHP
+ * @package  NicolasBejean\CategoryWidget\Model\Config\Source
+ * @author   Nicolas Béjean <nicolas@bejean.eu>
+ * @license  https://github.com/nicolasbejean/category-widget/blob/master/licence.txt BSD Licence
+ * @link     https://www.bejean.eu
+ */
+class CategoryWidget implements OptionSourceInterface
+{
+    /**
+     * @var array
+     */
+    private $options;
+
+    /**
+     * @var CollectionFactory
+     */
+    private $collectionFactory;
+
+    /**
+     * @param CollectionFactory     $collectionFactory
+     */
+    public function __construct(
+        CollectionFactory           $collectionFactory
+    ) {
+        $this->collectionFactory    = $collectionFactory;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function toOptionArray()
+    {
+        if (!$this->options) {
+            $this->options = $this->collectionFactory->create()->toOptionIdArray();
+        }
+
+        return $this->options;
+    }
+}
diff --git a/Model/GetCategoryWidgetByIdentifier.php b/Model/GetCategoryWidgetByIdentifier.php
new file mode 100755
index 0000000..aeeaba3
--- /dev/null
+++ b/Model/GetCategoryWidgetByIdentifier.php
@@ -0,0 +1,59 @@
+<?php
+namespace NicolasBejean\CategoryWidget\Model;
+
+use \Magento\Framework\Exception\LocalizedException;
+use \NicolasBejean\CategoryWidget\Api\GetCategoryWidgetByIdentifierInterface;
+use \NicolasBejean\CategoryWidget\Api\Data\CategoryWidgetInterface;
+use \Magento\Framework\Exception\NoSuchEntityException;
+use \NicolasBejean\CategoryWidget\Model\ResourceModel\CategoryWidget as CategoryWidgetResource;
+
+/**
+ * Class GetCategoryWidgetByIdentifier
+ *
+ * @category PHP
+ * @package  NicolasBejean\CategoryWidget\Model
+ * @author   Nicolas Béjean <nicolas@bejean.eu>
+ * @license  https://github.com/nicolasbejean/category-widget/blob/master/licence.txt BSD Licence
+ * @link     https://www.bejean.eu
+ */
+class GetCategoryWidgetByIdentifier implements GetCategoryWidgetByIdentifierInterface
+{
+    /**
+     * @var CategoryWidgetFactory
+     */
+    private $categoryWidgetFactory;
+
+    /**
+     * @var CategoryWidgetResource
+     */
+    private $categoryWidgetResource;
+
+    /**
+     * @param CategoryWidgetFactory        $categoryWidgetFactory
+     * @param CategoryWidgetResource       $categoryWidgetResource
+     */
+    public function __construct(
+        CategoryWidgetFactory              $categoryWidgetFactory,
+        CategoryWidgetResource             $categoryWidgetResource
+    ) {
+        $this->categoryWidgetFactory       = $categoryWidgetFactory;
+        $this->categoryWidgetResource      = $categoryWidgetResource;
+    }
+
+    /**
+     * @inheritdoc
+     * @throws LocalizedException
+     */
+    public function execute(string $identifier, int $storeId) : CategoryWidgetInterface
+    {
+        $categoryWidget = $this->categoryWidgetFactory->create();
+        $categoryWidget->setStoreId($storeId);
+        $this->categoryWidgetResource->load($categoryWidget, $identifier, CategoryWidgetInterface::IDENTIFIER);
+
+        if (!$categoryWidget->getId()) {
+            throw new NoSuchEntityException(__('The category widget with the \'%1\' identifier doesn\'t exist.', $identifier));
+        }
+
+        return $categoryWidget;
+    }
+}
diff --git a/Model/Resolver/CategoryWidget/Identity.php b/Model/Resolver/CategoryWidget/Identity.php
new file mode 100644
index 0000000..8eb5b8d
--- /dev/null
+++ b/Model/Resolver/CategoryWidget/Identity.php
@@ -0,0 +1,47 @@
+<?php
+declare(strict_types=1);
+
+namespace NicolasBejean\CategoryWidget\Model\Resolver\CategoryWidget;
+
+use \NicolasBejean\CategoryWidget\Api\Data\CategoryWidgetInterface;
+
+use \Magento\Framework\GraphQl\Query\Resolver\IdentityInterface;
+
+/**
+ * Class CategoryWidget
+ *
+ * @category PHP
+ * @package  NicolasBejean\CategoryWidget\Model\Resolver\CategoryWidget
+ * @author   Nicolas Béjean <nicolas@bejean.eu>
+ * @license  https://github.com/nicolasbejean/category-widget/blob/master/licence.txt BSD Licence
+ * @link     https://www.bejean.eu
+ */
+class Identity implements IdentityInterface
+{
+    /** @var string */
+    private $cacheTag = \NicolasBejean\CategoryWidget\Model\CategoryWidget::CACHE_TAG;
+
+    /**
+     * Get Category Widget identities from resolved data
+     *
+     * @param array $resolvedData
+     * @return string[]
+     */
+    public function getIdentities(array $resolvedData): array
+    {
+        $ids = [];
+        $items = $resolvedData['items'] ?? [];
+        foreach ($items as $item) {
+            if (is_array($item) && !empty($item[CategoryWidgetInterface::ENTITY_ID])) {
+                $ids[] = sprintf('%s_%s', $this->cacheTag, $item[CategoryWidgetInterface::ENTITY_ID]);
+                $ids[] = sprintf('%s_%s', $this->cacheTag, $item[CategoryWidgetInterface::IDENTIFIER]);
+            }
+        }
+
+        if (!empty($ids)) {
+            array_unshift($ids, $this->cacheTag);
+        }
+
+        return $ids;
+    }
+}
diff --git a/Model/Resolver/CategoryWidgets.php b/Model/Resolver/CategoryWidgets.php
new file mode 100644
index 0000000..4985f60
--- /dev/null
+++ b/Model/Resolver/CategoryWidgets.php
@@ -0,0 +1,96 @@
+<?php
+declare(strict_types=1);
+
+namespace NicolasBejean\CategoryWidget\Model\Resolver;
+
+use \NicolasBejean\CategoryWidget\Model\Resolver\DataProvider\CategoryWidget as CategoryWidgetDataProvider;
+
+use \Magento\Framework\GraphQl\Config\Element\Field;
+use \Magento\Framework\GraphQl\Query\ResolverInterface;
+use \Magento\Framework\GraphQl\Schema\Type\ResolveInfo;
+
+use \Magento\Framework\Exception\NoSuchEntityException;
+use \Magento\Framework\GraphQl\Exception\GraphQlInputException;
+use \Magento\Framework\GraphQl\Exception\GraphQlNoSuchEntityException;
+
+/**
+ * Class CategoryWidgets
+ *
+ * @category PHP
+ * @package  NicolasBejean\CategoryWidget\Model\Resolver
+ * @author   Nicolas Béjean <nicolas@bejean.eu>
+ * @license  https://github.com/nicolasbejean/category-widget/blob/master/licence.txt BSD Licence
+ * @link     https://www.bejean.eu
+ */
+class CategoryWidgets implements ResolverInterface
+{
+    /**
+     * @var CategoryWidgetDataProvider
+     */
+    private $dataProvider;
+
+    /**
+     * @param CategoryWidgetDataProvider $dataProvider
+     */
+    public function __construct(
+        CategoryWidgetDataProvider $dataProvider
+    ) {
+        $this->dataProvider = $dataProvider;
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function resolve(
+        Field $field,
+        $context,
+        ResolveInfo $info,
+        array $value = null,
+        array $args = null
+    ) {
+
+        $categoryWidgetIdentifiers = $this->getCategoryWidgetIdentifiers($args);
+        $categoryWidgetsData = $this->getCategoryWidgetsData($categoryWidgetIdentifiers);
+
+        $resultData = [
+            'items' => $categoryWidgetsData,
+        ];
+        return $resultData;
+    }
+
+    /**
+     * Get Category Widget identifiers
+     *
+     * @param array $args
+     * @return string[]
+     * @throws GraphQlInputException
+     */
+    private function getCategoryWidgetIdentifiers(array $args): array
+    {
+        if (!isset($args['identifiers']) || !is_array($args['identifiers']) || count($args['identifiers']) === 0) {
+            throw new GraphQlInputException(__('"identifiers" of Category Widgets should be specified'));
+        }
+
+        return $args['identifiers'];
+    }
+
+    /**
+     * Get category widgets data
+     *
+     * @param array $categoryWidgetIdentifiers
+     * @return array
+     * @throws GraphQlNoSuchEntityException
+     */
+    private function getCategoryWidgetsData(array $categoryWidgetIdentifiers): array
+    {
+        $categoryWidgetsData = [];
+        foreach ($categoryWidgetIdentifiers as $categoryWidgetIdentifier) {
+            try {
+                $categoryWidgetsData[$categoryWidgetIdentifier] = $this->dataProvider->getData($categoryWidgetIdentifier);
+            } catch (NoSuchEntityException $e) {
+                $categoryWidgetsData[$categoryWidgetIdentifier] = new GraphQlNoSuchEntityException(__($e->getMessage()), $e);
+            }
+        }
+        return $categoryWidgetsData;
+    }
+}
diff --git a/Model/Resolver/DataProvider/CategoryWidget.php b/Model/Resolver/DataProvider/CategoryWidget.php
new file mode 100644
index 0000000..f64fe34
--- /dev/null
+++ b/Model/Resolver/DataProvider/CategoryWidget.php
@@ -0,0 +1,75 @@
+<?php
+declare(strict_types=1);
+
+namespace NicolasBejean\CategoryWidget\Model\Resolver\DataProvider;
+
+use \NicolasBejean\CategoryWidget\Api\CategoryWidgetRepositoryInterface;
+use \NicolasBejean\CategoryWidget\Api\Data\CategoryWidgetInterface;
+use \NicolasBejean\CategoryWidget\Model\CategoryWidget as CategoryWidgetModel;
+
+use \Magento\Widget\Model\Template\FilterEmulate;
+
+use \Magento\Framework\Exception\NoSuchEntityException;
+
+/**
+ * Class CategoryWidget
+ *
+ * @category PHP
+ * @package  NicolasBejean\CategoryWidget\Model\Resolver\DataProvider
+ * @author   Nicolas Béjean <nicolas@bejean.eu>
+ * @license  https://github.com/nicolasbejean/category-widget/blob/master/licence.txt BSD Licence
+ * @link     https://www.bejean.eu
+ */
+class CategoryWidget
+{
+    /**
+     * @var CategoryWidgetRepositoryInterface
+     */
+    private $categoryWidgetRepository;
+
+    /**
+     * @var FilterEmulate
+     */
+    private $widgetFilter;
+
+    /**
+     * @param CategoryWidgetRepositoryInterface $categoryWidgetRepository
+     * @param FilterEmulate $widgetFilter
+     */
+    public function __construct(
+        CategoryWidgetRepositoryInterface $categoryWidgetRepository,
+        FilterEmulate $widgetFilter
+    ) {
+        $this->categoryWidgetRepository = $categoryWidgetRepository;
+        $this->widgetFilter = $widgetFilter;
+    }
+
+    /**
+     * Get Category Widget data
+     *
+     * @param string $categoryWidgetIdentifier
+     * @return array
+     * @throws NoSuchEntityException
+     */
+    public function getData(string $categoryWidgetIdentifier): array
+    {
+        /** @var CategoryWidgetModel $categoryWidget */
+        $categoryWidget = $this->categoryWidgetRepository->getById($categoryWidgetIdentifier);
+
+        if (false === $categoryWidget->isActive()) {
+            throw new NoSuchEntityException(
+                __('The Category Widget with the "%1" ID doesn\'t exist.', $categoryWidgetIdentifier)
+            );
+        }
+
+        /* TODO : Faire un explode du content pour renvoyer chaque category widget*/
+        $content = $categoryWidget->getContent();
+
+        $categoryWidgetData = [
+            CategoryWidgetInterface::IDENTIFIER => $categoryWidget->getIdentifier(),
+            CategoryWidgetInterface::NAME => $categoryWidget->getName(),
+            CategoryWidgetInterface::CONTENT => $content,
+        ];
+        return $categoryWidgetData;
+    }
+}
diff --git a/Model/ResourceModel/AbstractCollection.php b/Model/ResourceModel/AbstractCollection.php
new file mode 100755
index 0000000..eb950c5
--- /dev/null
+++ b/Model/ResourceModel/AbstractCollection.php
@@ -0,0 +1,222 @@
+<?php
+namespace NicolasBejean\CategoryWidget\Model\ResourceModel;
+
+use \Magento\Framework\Data\Collection\Db\FetchStrategyInterface;
+use \Magento\Framework\Data\Collection\EntityFactoryInterface;
+use \Magento\Framework\DB\Adapter\AdapterInterface;
+use \Magento\Framework\DB\Select;
+use \Magento\Framework\EntityManager\MetadataPool;
+use \Magento\Framework\Event\ManagerInterface;
+use \Magento\Framework\Exception\NoSuchEntityException;
+use \Magento\Framework\Model\ResourceModel\Db\AbstractDb;
+use \Magento\Store\Model\Store;
+use \Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection as AbstractCollectionExtends;
+use \Magento\Store\Model\StoreManagerInterface;
+use \Psr\Log\LoggerInterface;
+
+/**
+ * Class AbstractCollection
+ *
+ * @category PHP
+ * @package  NicolasBejean\CategoryWidget\Model\ResourceModel
+ * @author   Nicolas Béjean <nicolas@bejean.eu>
+ * @license  https://github.com/nicolasbejean/category-widget/blob/master/licence.txt BSD Licence
+ * @link     https://www.bejean.eu
+ */
+abstract class AbstractCollection extends AbstractCollectionExtends
+{
+    /**
+     * Store manager
+     *
+     * @var StoreManagerInterface
+     */
+    protected $storeManager;
+
+    /**
+     * @var MetadataPool
+     */
+    protected $metadataPool;
+
+    /**
+     * @param EntityFactoryInterface        $entityFactory
+     * @param LoggerInterface               $logger
+     * @param FetchStrategyInterface        $fetchStrategy
+     * @param ManagerInterface              $eventManager
+     * @param StoreManagerInterface         $storeManager
+     * @param MetadataPool                  $metadataPool
+     * @param AdapterInterface|null         $connection
+     * @param AbstractDb|null               $resource
+     */
+    public function __construct(
+        EntityFactoryInterface              $entityFactory,
+        LoggerInterface                     $logger,
+        FetchStrategyInterface              $fetchStrategy,
+        ManagerInterface                    $eventManager,
+        StoreManagerInterface               $storeManager,
+        MetadataPool                        $metadataPool,
+        AdapterInterface                    $connection = null,
+        AbstractDb                          $resource = null
+    ) {
+        $this->storeManager                 = $storeManager;
+        $this->metadataPool                 = $metadataPool;
+
+        parent::__construct($entityFactory, $logger, $fetchStrategy, $eventManager, $connection, $resource);
+    }
+
+    /**
+     * Perform operations after collection load
+     *
+     * @param string $tableName
+     * @param string|null $linkField
+     * @return void
+     * @throws NoSuchEntityException
+     */
+    protected function performAfterLoad($tableName, $linkField)
+    {
+        $linkedIds = $this->getColumnValues($linkField);
+        if (count($linkedIds)) {
+            $connection = $this->getConnection();
+            $select = $connection->select()->from(['cms_entity_store' => $this->getTable($tableName)])
+                ->where('cms_entity_store.' . $linkField . ' IN (?)', $linkedIds);
+            $result = $connection->fetchAll($select);
+            if ($result) {
+                $storesData = [];
+                foreach ($result as $storeData) {
+                    $storesData[$storeData[$linkField]][] = $storeData['store_id'];
+                }
+
+                foreach ($this as $item) {
+                    $linkedId = $item->getData($linkField);
+                    if (!isset($storesData[$linkedId])) {
+                        continue;
+                    }
+                    $storeIdKey = array_search(Store::DEFAULT_STORE_ID, $storesData[$linkedId], true);
+                    if ($storeIdKey !== false) {
+                        $stores = $this->storeManager->getStores(false, true);
+                        $storeId = current($stores)->getId();
+                        $storeCode = key($stores);
+                    } else {
+                        $storeId = current($storesData[$linkedId]);
+                        $storeCode = $this->storeManager->getStore($storeId)->getCode();
+                    }
+                    $item->setData('_first_store_id', $storeId);
+                    $item->setData('store_code', $storeCode);
+                    $item->setData('store_id', $storesData[$linkedId]);
+                }
+            }
+        }
+    }
+
+    /**
+     * Add field filter to collection
+     *
+     * @param array|string $field
+     * @param string|int|array|null $condition
+     * @return AbstractCollectionExtends
+     */
+    public function addFieldToFilter($field, $condition = null)
+    {
+        if ($field === 'store_id') {
+            return $this->addStoreFilter($condition, false);
+        }
+
+        return parent::addFieldToFilter($field, $condition);
+    }
+
+    /**
+     * Add filter by store
+     *
+     * @param int|array|Store $store
+     * @param bool $withAdmin
+     * @return $this
+     */
+    abstract public function addStoreFilter($store, $withAdmin = true);
+
+    /**
+     * Perform adding filter by store
+     *
+     * @param int|array|Store $store
+     * @param bool $withAdmin
+     * @return void
+     */
+    protected function performAddStoreFilter($store, $withAdmin = true)
+    {
+        if ($store instanceof Store) {
+            $store = [$store->getId()];
+        }
+
+        if (!is_array($store)) {
+            $store = [$store];
+        }
+
+        if ($withAdmin) {
+            $store[] = Store::DEFAULT_STORE_ID;
+        }
+
+        $this->addFilter('store', ['in' => $store], 'public');
+    }
+
+    /**
+     * Join store relation table if there is store filter
+     *
+     * @param string $tableName
+     * @param string|null $linkField
+     * @return void
+     */
+    protected function joinStoreRelationTable($tableName, $linkField)
+    {
+        if ($this->getFilter('store')) {
+            $this->getSelect()->join(
+                ['store_table' => $this->getTable($tableName)],
+                'main_table.' . $linkField . ' = store_table.' . $linkField,
+                []
+            )->group(
+                'main_table.' . $linkField
+            );
+        }
+        parent::_renderFiltersBefore();
+    }
+
+    /**
+     * Get SQL for get record count
+     *
+     * Extra GROUP BY strip added.
+     *
+     * @return Select
+     */
+    public function getSelectCountSql()
+    {
+        $countSelect = parent::getSelectCountSql();
+        $countSelect->reset(Select::GROUP);
+
+        return $countSelect;
+    }
+
+    /**
+     * Returns pairs identifier - title for unique identifiers
+     * and pairs identifier|entity_id - title for non-unique after first
+     *
+     * @return array
+     */
+    public function toOptionIdArray()
+    {
+        $res = [];
+        $existingIdentifiers = [];
+        foreach ($this as $item) {
+            $identifier = $item->getData('identifier');
+
+            $data['value'] = $identifier;
+            $data['label'] = $item->getData('title');
+
+            if (in_array($identifier, $existingIdentifiers)) {
+                $data['value'] .= '|' . $item->getData($this->getIdFieldName());
+            } else {
+                $existingIdentifiers[] = $identifier;
+            }
+
+            $res[] = $data;
+        }
+
+        return $res;
+    }
+}
diff --git a/Model/ResourceModel/CategoryWidget.php b/Model/ResourceModel/CategoryWidget.php
new file mode 100755
index 0000000..3ef608c
--- /dev/null
+++ b/Model/ResourceModel/CategoryWidget.php
@@ -0,0 +1,269 @@
+<?php
+namespace NicolasBejean\CategoryWidget\Model\ResourceModel;
+
+use \NicolasBejean\CategoryWidget\Api\Data\CategoryWidgetInterface;
+use \Magento\Framework\DB\Select;
+use \Magento\Framework\EntityManager\EntityManager;
+use \Magento\Framework\EntityManager\MetadataPool;
+use \Magento\Framework\Exception\LocalizedException;
+use \Magento\Framework\Model\AbstractModel;
+use \Magento\Framework\Model\ResourceModel\Db\AbstractDb;
+use \Magento\Framework\Model\ResourceModel\Db\Context;
+use \Magento\Store\Model\Store;
+use \Magento\Store\Model\StoreManagerInterface;
+use \Exception;
+use \NicolasBejean\CategoryWidget\Model\CategoryWidget as CategoryWidgetModel;
+
+/**
+ * Class CategoryWidget
+ *
+ * @category PHP
+ * @package  NicolasBejean\CategoryWidget\Model\ResourceModel
+ * @author   Nicolas Béjean <nicolas@bejean.eu>
+ * @license  https://github.com/nicolasbejean/category-widget/blob/master/licence.txt BSD Licence
+ * @link     https://www.bejean.eu
+ */
+class CategoryWidget extends AbstractDb
+{
+    /**
+     * Store manager
+     *
+     * @var StoreManagerInterface
+     */
+    protected $storeManager;
+
+    /**
+     * @var EntityManager
+     */
+    protected $entityManager;
+
+    /**
+     * @var MetadataPool
+     */
+    protected $metadataPool;
+
+    /**
+     * @param Context                   $context
+     * @param StoreManagerInterface     $storeManager
+     * @param EntityManager             $entityManager
+     * @param MetadataPool              $metadataPool
+     * @param string                    $connectionName
+     */
+    public function __construct(
+        Context                         $context,
+        StoreManagerInterface           $storeManager,
+        EntityManager                   $entityManager,
+        MetadataPool                    $metadataPool,
+        $connectionName                 = null
+    ) {
+        $this->storeManager             = $storeManager;
+        $this->entityManager            = $entityManager;
+        $this->metadataPool             = $metadataPool;
+
+        parent::__construct($context, $connectionName);
+    }
+
+    /**
+     * Initialize resource model
+     *
+     * @return void
+     */
+    protected function _construct()
+    {
+        $this->_init('nicolasbejean_categorywidget', 'entity_id');
+    }
+
+    /**
+     * @inheritDoc
+     * @throws Exception
+     */
+    public function getConnection()
+    {
+        return $this->metadataPool->getMetadata(CategoryWidgetInterface::class)->getEntityConnection();
+    }
+
+    /**
+     * Perform operations before object save
+     *
+     * @param AbstractModel $object
+     * @return $this
+     * @throws LocalizedException
+     */
+    protected function _beforeSave(AbstractModel $object)
+    {
+        if (!$this->getIsUniqueCategoryWidgetToStores($object)) {
+            throw new LocalizedException(
+                __('A category widget identifier with the same properties already exists in the selected store.')
+            );
+        }
+        return $this;
+    }
+
+    /**
+     * @param AbstractModel $object
+     * @param mixed $value
+     * @param null $field
+     * @return bool|int|string
+     * @throws LocalizedException
+     * @throws Exception
+     */
+    private function getCategoryWidgetId(AbstractModel $object, $value, $field = null)
+    {
+        $entityMetadata = $this->metadataPool->getMetadata(CategoryWidgetInterface::class);
+        if (!is_numeric($value) && $field === null) {
+            $field = 'identifier';
+        } elseif (!$field) {
+            $field = $entityMetadata->getIdentifierField();
+        }
+        $entityId = $value;
+        if ($field != $entityMetadata->getIdentifierField() || $object->getStoreId()) {
+            $select = $this->_getLoadSelect($field, $value, $object);
+            $select->reset(Select::COLUMNS)
+                ->columns($this->getMainTable() . '.' . $entityMetadata->getIdentifierField())
+                ->limit(1);
+            $result = $this->getConnection()->fetchCol($select);
+            $entityId = count($result) ? $result[0] : false;
+        }
+        return $entityId;
+    }
+
+    /**
+     * Load an object
+     *
+     * @param CategoryWidgetModel|AbstractModel $object
+     * @param mixed $value
+     * @param string $field field to load by (defaults to model id)
+     * @return $this
+     * @throws LocalizedException
+     */
+    public function load(AbstractModel $object, $value, $field = null)
+    {
+        $categoryWidgetId = $this->getCategoryWidgetId($object, $value, $field);
+        if ($categoryWidgetId) {
+            $this->entityManager->load($object, $categoryWidgetId);
+        }
+        return $this;
+    }
+
+    /**
+     * Retrieve select object for load object data
+     *
+     * @param string $field
+     * @param mixed $value
+     * @param CategoryWidgetModel|AbstractModel $object
+     * @return Select
+     * @throws LocalizedException
+     * @throws Exception
+     */
+    protected function _getLoadSelect($field, $value, $object)
+    {
+        $entityMetadata = $this->metadataPool->getMetadata(CategoryWidgetInterface::class);
+        $linkField = $entityMetadata->getLinkField();
+
+        $select = parent::_getLoadSelect($field, $value, $object);
+
+        if ($object->getStoreId()) {
+            $stores = [(int)$object->getStoreId(), Store::DEFAULT_STORE_ID];
+
+            $select->join(
+                ['niis' => $this->getTable('nicolasbejean_categorywidget_store')],
+                $this->getMainTable() . '.' . $linkField . ' = niis.' . $linkField,
+                ['store_id']
+            )
+                ->where('is_active = ?', 1)
+                ->where('niis.store_id in (?)', $stores)
+                ->order('store_id DESC')
+                ->limit(1);
+        }
+
+        return $select;
+    }
+
+    /**
+     * Check for unique of path of category widgetto selected store(s).
+     *
+     * @param AbstractModel $object
+     * @return bool
+     * @SuppressWarnings(PHPMD.BooleanGetMethodName)
+     * @throws LocalizedException
+     * @throws Exception
+     */
+    public function getIsUniqueCategoryWidgetToStores(AbstractModel $object)
+    {
+        $entityMetadata = $this->metadataPool->getMetadata(CategoryWidgetInterface::class);
+        $linkField = $entityMetadata->getLinkField();
+
+        if ($this->storeManager->isSingleStoreMode()) {
+            $stores = [Store::DEFAULT_STORE_ID];
+        } else {
+            $stores = (array)$object->getData('store_id');
+        }
+
+        $select = $this->getConnection()->select()
+            ->from(['nii' => $this->getMainTable()])
+            ->join(
+                ['niis' => $this->getTable('nicolasbejean_categorywidget_store')],
+                'nii.' . $linkField . ' = niis.' . $linkField,
+                []
+            )
+            ->where('nii.identifier = ?', $object->getData('identifier'))
+            ->where('niis.store_id IN (?)', $stores);
+
+        if ($object->getId()) {
+            $select->where('nii.' . $entityMetadata->getIdentifierField() . ' <> ?', $object->getId());
+        }
+
+        if ($this->getConnection()->fetchRow($select)) {
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * Get store ids to which specified item is assigned
+     *
+     * @param int $id
+     * @return array
+     * @throws LocalizedException
+     * @throws Exception
+     */
+    public function lookupStoreIds($id)
+    {
+        $connection = $this->getConnection();
+
+        $entityMetadata = $this->metadataPool->getMetadata(CategoryWidgetInterface::class);
+        $linkField = $entityMetadata->getLinkField();
+
+        $select = $connection->select()
+            ->from(['niis' => $this->getTable('nicolasbejean_categorywidget_store')], 'store_id')
+            ->join(
+                ['nii' => $this->getMainTable()],
+                'niis.' . $linkField . ' = nii.' . $linkField,
+                []
+            )
+            ->where('nii.' . $entityMetadata->getIdentifierField() . ' = :entity_id');
+
+        return $connection->fetchCol($select, ['entity_id' => (int)$id]);
+    }
+
+    /**
+     * @param AbstractModel $object
+     * @return $this
+     * @throws Exception
+     */
+    public function save(AbstractModel $object)
+    {
+        $this->entityManager->save($object);
+        return $this;
+    }
+
+    /**
+     * @inheritDoc
+     */
+    public function delete(AbstractModel $object)
+    {
+        $this->entityManager->delete($object);
+        return $this;
+    }
+}
diff --git a/Model/ResourceModel/CategoryWidget/Collection.php b/Model/ResourceModel/CategoryWidget/Collection.php
new file mode 100755
index 0000000..4d12d7f
--- /dev/null
+++ b/Model/ResourceModel/CategoryWidget/Collection.php
@@ -0,0 +1,103 @@
+<?php
+namespace NicolasBejean\CategoryWidget\Model\ResourceModel\CategoryWidget;
+
+use \Magento\Store\Model\Store;
+use \NicolasBejean\CategoryWidget\Api\Data\CategoryWidgetInterface;
+use \NicolasBejean\CategoryWidget\Model\CategoryWidget as CategoryWidgetModel;
+use \NicolasBejean\CategoryWidget\Model\ResourceModel\CategoryWidget as CategoryWidgetResourceModel;
+use \NicolasBejean\CategoryWidget\Model\ResourceModel\AbstractCollection;
+use \Exception;
+
+/**
+ * Class Collection
+ *
+ * @category PHP
+ * @package  NicolasBejean\CategoryWidget\Model\ResourceModel\CategoryWidget
+ * @author   Nicolas Béjean <nicolas@bejean.eu>
+ * @license  https://github.com/nicolasbejean/category-widget/blob/master/licence.txt BSD Licence
+ * @link     https://www.bejean.eu
+ */
+class Collection extends AbstractCollection
+{
+    /**
+     * @var string
+     */
+    protected $_idFieldName = 'entity_id';
+
+    /**
+     * Event prefix
+     *
+     * @var string
+     */
+    protected $eventPrefix = 'categorywidget_collection';
+
+    /**
+     * Event object
+     *
+     * @var string
+     */
+    protected $eventObject = 'categorywidget_collection';
+
+    /**
+     * Perform operations after collection load
+     *
+     * @return AbstractCollection
+     * @throws Exception
+     */
+    protected function _afterLoad()
+    {
+        $entityMetadata = $this->metadataPool->getMetadata(CategoryWidgetInterface::class);
+
+        $this->performAfterLoad('nicolasbejean_categorywidget_store', $entityMetadata->getLinkField());
+
+        return parent::_afterLoad();
+    }
+
+    /**
+     * Define resource model
+     *
+     * @return void
+     */
+    protected function _construct()
+    {
+        $this->_init(CategoryWidgetModel::class, CategoryWidgetResourceModel::class);
+        $this->_map['fields']['store'] = 'store_table.store_id';
+        $this->_map['fields']['entity_id'] = 'main_table.entity_id';
+    }
+
+    /**
+     * Returns pairs entity_id - identifier
+     *
+     * @return array
+     */
+    public function toOptionArray()
+    {
+        return $this->_toOptionArray('entity_id', 'identifier');
+    }
+
+    /**
+     * Add filter by store
+     *
+     * @param int|array|Store $store
+     * @param bool $withAdmin
+     * @return $this
+     */
+    public function addStoreFilter($store, $withAdmin = true)
+    {
+        $this->performAddStoreFilter($store, $withAdmin);
+
+        return $this;
+    }
+
+    /**
+     * Join store relation table if there is store filter
+     *
+     * @return void
+     * @throws Exception
+     */
+    protected function _renderFiltersBefore()
+    {
+        $entityMetadata = $this->metadataPool->getMetadata(CategoryWidgetInterface::class);
+        $this->joinStoreRelationTable('nicolasbejean_categorywidget_store', $entityMetadata->getLinkField());
+    }
+}
diff --git a/Model/ResourceModel/CategoryWidget/Grid/Collection.php b/Model/ResourceModel/CategoryWidget/Grid/Collection.php
new file mode 100755
index 0000000..290ec83
--- /dev/null
+++ b/Model/ResourceModel/CategoryWidget/Grid/Collection.php
@@ -0,0 +1,158 @@
+<?php
+namespace NicolasBejean\CategoryWidget\Model\ResourceModel\CategoryWidget\Grid;
+
+use \Magento\Framework\Api\ExtensibleDataInterface;
+use \Magento\Framework\Api\Search\SearchResultInterface;
+use \Magento\Framework\Api\Search\AggregationInterface;
+use \Magento\Framework\Api\SearchCriteriaInterface;
+use \Magento\Framework\Data\Collection\Db\FetchStrategyInterface;
+use \Magento\Framework\Data\Collection\EntityFactoryInterface;
+use \Magento\Framework\DB\Adapter\AdapterInterface;
+use \Magento\Framework\EntityManager\MetadataPool;
+use \Magento\Framework\Event\ManagerInterface;
+use \Magento\Framework\Model\ResourceModel\Db\AbstractDb;
+use \Magento\Framework\View\Element\UiComponent\DataProvider\Document;
+use \Magento\Store\Model\StoreManagerInterface;
+use \NicolasBejean\CategoryWidget\Model\ResourceModel\CategoryWidget\Collection as CategoryWidgetCollection;
+use \Psr\Log\LoggerInterface;
+
+/**
+ * Class Collection
+ *
+ * @category PHP
+ * @package  NicolasBejean\CategoryWidget\Model\ResourceModel\CategoryWidget\Grid
+ * @author   Nicolas Béjean <nicolas@bejean.eu>
+ * @license  https://github.com/nicolasbejean/category-widget/blob/master/licence.txt BSD Licence
+ * @link     https://www.bejean.eu
+ */
+class Collection extends CategoryWidgetCollection implements SearchResultInterface
+{
+    /**
+     * @var AggregationInterface
+     */
+    protected $aggregations;
+
+    /**
+     * @param EntityFactoryInterface        $entityFactory
+     * @param LoggerInterface               $logger
+     * @param FetchStrategyInterface        $fetchStrategy
+     * @param ManagerInterface              $eventManager
+     * @param StoreManagerInterface         $storeManager
+     * @param MetadataPool                  $metadataPool
+     * @param string                        $mainTable
+     * @param string                        $eventPrefix
+     * @param string                        $eventObject
+     * @param string                        $resourceModel
+     * @param string                        $model
+     * @param AdapterInterface              $connection
+     * @param AbstractDb                    $resource
+     *
+     * @SuppressWarnings(PHPMD.ExcessiveParameterList)
+     */
+    public function __construct(
+        EntityFactoryInterface              $entityFactory,
+        LoggerInterface                     $logger,
+        FetchStrategyInterface              $fetchStrategy,
+        ManagerInterface                    $eventManager,
+        StoreManagerInterface               $storeManager,
+        MetadataPool                        $metadataPool,
+        string                              $mainTable,
+        string                              $eventPrefix,
+        string                              $eventObject,
+        string                              $resourceModel,
+        string                              $model = Document::class,
+        AdapterInterface                    $connection = null,
+        AbstractDb                          $resource = null
+    ) {
+        $this->_eventPrefix                 = $eventPrefix;
+        $this->_eventObject                 = $eventObject;
+
+        parent::__construct(
+            $entityFactory,
+            $logger,
+            $fetchStrategy,
+            $eventManager,
+            $storeManager,
+            $metadataPool,
+            $connection,
+            $resource
+        );
+
+        $this->_init($model, $resourceModel);
+        $this->setMainTable($mainTable);
+    }
+
+    /**
+     * @return AggregationInterface
+     */
+    public function getAggregations()
+    {
+        return $this->aggregations;
+    }
+
+    /**
+     * @param AggregationInterface $aggregations
+     * @return $this
+     */
+    public function setAggregations($aggregations)
+    {
+        $this->aggregations = $aggregations;
+        return $this;
+    }
+
+    /**
+     * Get search criteria.
+     *
+     * @return SearchCriteriaInterface|null
+     */
+    public function getSearchCriteria()
+    {
+        return null;
+    }
+
+    /**
+     * Set search criteria.
+     *
+     * @param SearchCriteriaInterface $searchCriteria
+     * @return $this
+     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+     */
+    public function setSearchCriteria(SearchCriteriaInterface $searchCriteria = null)
+    {
+        return $this;
+    }
+
+    /**
+     * Get total count.
+     *
+     * @return int
+     */
+    public function getTotalCount()
+    {
+        return $this->getSize();
+    }
+
+    /**
+     * Set total count.
+     *
+     * @param int $totalCount
+     * @return $this
+     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+     */
+    public function setTotalCount($totalCount)
+    {
+        return $this;
+    }
+
+    /**
+     * Set items list.
+     *
+     * @param ExtensibleDataInterface[] $items
+     * @return $this
+     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+     */
+    public function setItems(array $items = null)
+    {
+        return $this;
+    }
+}
diff --git a/Model/ResourceModel/CategoryWidget/Relation/Store/ReadHandler.php b/Model/ResourceModel/CategoryWidget/Relation/Store/ReadHandler.php
new file mode 100755
index 0000000..d2ced62
--- /dev/null
+++ b/Model/ResourceModel/CategoryWidget/Relation/Store/ReadHandler.php
@@ -0,0 +1,53 @@
+<?php
+namespace NicolasBejean\CategoryWidget\Model\ResourceModel\CategoryWidget\Relation\Store;
+
+use \Magento\Framework\Exception\LocalizedException;
+use \NicolasBejean\CategoryWidget\Model\ResourceModel\CategoryWidget;
+use \Magento\Framework\EntityManager\Operation\ExtensionInterface;
+use \Exception;
+
+/**
+ * Class ReadHandler
+ *
+ * @category PHP
+ * @package  NicolasBejean\CategoryWidget\Model\ResourceModel\CategoryWidget\Relation\Store
+ * @author   Nicolas Béjean <nicolas@bejean.eu>
+ * @license  https://github.com/nicolasbejean/category-widget/blob/master/licence.txt BSD Licence
+ * @link     https://www.bejean.eu
+ */
+class ReadHandler implements ExtensionInterface
+{
+    /**
+     * @var CategoryWidget
+     */
+    protected $resourceCategoryWidget;
+
+    /**
+     * @param CategoryWidget           $resourceCategoryWidget
+     */
+    public function __construct(
+        CategoryWidget                 $resourceCategoryWidget
+    ) {
+        $this->resourceCategoryWidget  = $resourceCategoryWidget;
+    }
+
+    /**
+     * @param object $entity
+     * @param array $arguments
+     * @return object
+     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+     * @throws Exception
+     */
+    public function execute($entity, $arguments = [])
+    {
+        if ($entity->getId()) {
+            try {
+                $stores = $this->resourceCategoryWidget->lookupStoreIds((int)$entity->getId());
+            } catch (Exception $e) {
+            }
+            $entity->setData('store_id', $stores);
+            $entity->setData('stores', $stores);
+        }
+        return $entity;
+    }
+}
diff --git a/Model/ResourceModel/CategoryWidget/Relation/Store/SaveHandler.php b/Model/ResourceModel/CategoryWidget/Relation/Store/SaveHandler.php
new file mode 100755
index 0000000..c3021b6
--- /dev/null
+++ b/Model/ResourceModel/CategoryWidget/Relation/Store/SaveHandler.php
@@ -0,0 +1,84 @@
+<?php
+namespace NicolasBejean\CategoryWidget\Model\ResourceModel\CategoryWidget\Relation\Store;
+
+use \Magento\Framework\EntityManager\Operation\ExtensionInterface;
+use \NicolasBejean\CategoryWidget\Api\Data\CategoryWidgetInterface;
+use \NicolasBejean\CategoryWidget\Model\ResourceModel\CategoryWidget;
+use \Magento\Framework\EntityManager\MetadataPool;
+use \Exception;
+
+/**
+ * Class SaveHandler
+ *
+ * @category PHP
+ * @package  NicolasBejean\CategoryWidget\Model\ResourceModel\CategoryWidget\Relation\Store
+ * @author   Nicolas Béjean <nicolas@bejean.eu>
+ * @license  https://github.com/nicolasbejean/category-widget/blob/master/licence.txt BSD Licence
+ * @link     https://www.bejean.eu
+ */
+class SaveHandler implements ExtensionInterface
+{
+    /**
+     * @var MetadataPool
+     */
+    protected $metadataPool;
+
+    /**
+     * @var CategoryWidget
+     */
+    protected $resourceCategoryWidget;
+
+    /**
+     * @param MetadataPool          $metadataPool
+     * @param CategoryWidget           $resourceCategoryWidget
+     */
+    public function __construct(
+        MetadataPool                $metadataPool,
+        CategoryWidget                 $resourceCategoryWidget
+    ) {
+        $this->metadataPool         = $metadataPool;
+        $this->resourceCategoryWidget  = $resourceCategoryWidget;
+    }
+
+    /**
+     * @param object $entity
+     * @param array $arguments
+     * @return object
+     * @throws Exception
+     */
+    public function execute($entity, $arguments = [])
+    {
+        $entityMetadata = $this->metadataPool->getMetadata(CategoryWidgetInterface::class);
+        $linkField = $entityMetadata->getLinkField();
+
+        $connection = $entityMetadata->getEntityConnection();
+
+        $oldStores = $this->resourceCategoryWidget->lookupStoreIds((int)$entity->getId());
+        $newStores = (array)$entity->getStores();
+
+        $table = $this->resourceCategoryWidget->getTable('nicolasbejean_categorywidget_store');
+
+        $delete = array_diff($oldStores, $newStores);
+        if ($delete) {
+            $where = [
+                $linkField . ' = ?' => (int)$entity->getData($linkField),
+                'store_id IN (?)' => $delete,
+            ];
+            $connection->delete($table, $where);
+        }
+
+        $insert = array_diff($newStores, $oldStores);
+        if ($insert) {
+            $data = [];
+            foreach ($insert as $storeId) {
+                $data[] = [
+                    $linkField => (int)$entity->getData($linkField),
+                    'store_id' => (int)$storeId,
+                ];
+            }
+            $connection->insertMultiple($table, $data);
+        }
+
+        return $entity;
+    }
+}
diff --git a/Model/Template/Filter.php b/Model/Template/Filter.php
new file mode 100755
index 0000000..29b889c
--- /dev/null
+++ b/Model/Template/Filter.php
@@ -0,0 +1,36 @@
+<?php
+namespace NicolasBejean\CategoryWidget\Model\Template;
+
+use \Magento\Email\Model\Template\Filter as FilterExtends;
+use \Magento\Framework\Exception\NoSuchEntityException;
+
+/**
+ * Class Filter
+ *
+ * @category PHP
+ * @package  NicolasBejean\CategoryWidget\Model\Template
+ * @author   Nicolas Béjean <nicolas@bejean.eu>
+ * @license  https://github.com/nicolasbejean/category-widget/blob/master/licence.txt BSD Licence
+ * @link     https://www.bejean.eu
+ */
+class Filter extends FilterExtends
+{
+    /**
+     * Whether to allow SID in store directive: AUTO
+     *
+     * @var bool
+     */
+    protected $useSessionInUrl;
+
+    /**
+     * Setter whether SID is allowed in store directive
+     *
+     * @param bool $flag
+     * @return $this
+     */
+    public function setUseSessionInUrl($flag)
+    {
+        $this->_useSessionInUrl = (bool)$flag;
+        return $this;
+    }
+}
diff --git a/Model/Template/FilterProvider.php b/Model/Template/FilterProvider.php
new file mode 100755
index 0000000..932fb31
--- /dev/null
+++ b/Model/Template/FilterProvider.php
@@ -0,0 +1,74 @@
+<?php
+namespace NicolasBejean\CategoryWidget\Model\Template;
+
+use \Magento\Framework\Filter\Template;
+use \Magento\Framework\ObjectManagerInterface;
+use \Exception;
+
+/**
+ * Class FilterProvider
+ *
+ * @category PHP
+ * @package  NicolasBejean\CategoryWidget\Model\Template
+ * @author   Nicolas Béjean <nicolas@bejean.eu>
+ * @license  https://github.com/nicolasbejean/category-widget/blob/master/licence.txt BSD Licence
+ * @link     https://www.bejean.eu
+ */
+class FilterProvider
+{
+    /**
+     * @var ObjectManagerInterface
+     */
+    protected $objectManager;
+
+    /**
+     * @var string
+     */
+    /** TODO : A Delete ?  */
+    protected $categoryWidgetFilter;
+
+    /**
+     * @var array
+     */
+    protected $instanceList;
+
+    /**
+     * @param ObjectManagerInterface $objectManager
+     * @param string $categoryWidgetFilter
+     */
+    public function __construct(
+        ObjectManagerInterface $objectManager,
+        $categoryWidgetFilter = Filter::class
+    ) {
+        $this->objectManager = $objectManager;
+        $this->categoryWidgetFilter = $categoryWidgetFilter;
+    }
+
+    /**
+     * @param string $instanceName
+     * @return Template
+     * @throws Exception
+     */
+    protected function _getFilterInstance($instanceName)
+    {
+        if (!isset($this->instanceList[$instanceName])) {
+            $instance = $this->objectManager->get($instanceName);
+
+            if (!$instance instanceof Template) {
+                throw new Exception('Template filter ' . $instanceName . ' does not implement required interface');
+            }
+            $this->instanceList[$instanceName] = $instance;
+        }
+
+        return $this->instanceList[$instanceName];
+    }
+
+    /**
+     * @return Template
+     * @throws Exception
+     */
+    public function getCategoryWidgetFilter()
+    {
+        return $this->_getFilterInstance($this->categoryWidgetFilter);
+    }
+}
-- 
GitLab