Model/Feed/CategoryCollection.php (303 lines of code) (raw):

<?php /** * Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved */ namespace Facebook\BusinessExtension\Model\Feed; use Facebook\BusinessExtension\Helper\FBEHelper; use Facebook\BusinessExtension\Helper\HttpClient; use Magento\Catalog\Model\Category; use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory; use Magento\Framework\HTTP\Client\Curl; class CategoryCollection { protected $catalogId; /** * @var CollectionFactory */ private $productCollectionFactory; /** * @var FBEHelper */ private $fbeHelper; /** * @var Curl */ private $curl; /** * @var array */ private $categoryMap = []; /** * @var HttpClient */ private $httpClient; /** * @var \Magento\Store\Model\StoreManagerInterface */ protected $_storeManager; protected $_categoryCollection; /** * Constructor * @param CollectionFactory $productCollectionFactory * @param \Magento\Store\Model\StoreManagerInterface $storeManager * @param \Magento\Catalog\Model\ResourceModel\Category\CollectionFactory $categoryCollection * @param FBEHelper $helper * @param Curl $curl * @param HttpClient $httpClient */ public function __construct( CollectionFactory $productCollectionFactory, \Magento\Store\Model\StoreManagerInterface $storeManager, \Magento\Catalog\Model\ResourceModel\Category\CollectionFactory $categoryCollection, FBEHelper $helper, Curl $curl, HttpClient $httpClient ) { $this->_storeManager = $storeManager; $this->_categoryCollection = $categoryCollection; $this->productCollectionFactory = $productCollectionFactory; $this->fbeHelper = $helper; $this->curl = $curl; $this->categoryMap = $this->fbeHelper->generateCategoryNameMap(); $this->httpClient = $httpClient; } /** * @param Category $category * get called after user save category, if it is new leaf category, we will create new collection on fb side, * if it is changing existed category, we just update the corresponding fb collection. * @return null */ public function makeHttpRequestAfterCategorySave(Category $category) { $set_id = $this->getFBProductSetID($category); $this->fbeHelper->log("setid for it is:". (string)$set_id); if ($set_id) { $response = $this->updateCategoryWithFB($category, $set_id); return $response; } if (!$category->hasChildren()) { $response = $this->pushNewCategoryToFB($category); return $response; } $this->fbeHelper->log("category is neither leaf nor" ." used to be leaf (no existing set id found), won't update with fb"); } /** * TODO move it to helper or common class * @return string|null */ public function getCatalogID() { if ($this->catalogId == null) { $this->catalogId = $this->fbeHelper->getConfigValue('fbe/catalog/id'); } return $this->catalogId; } /** * @param Category $category * this method try to get fb product set id from Magento DB, return null if not exist * @return string|null */ public function getFBProductSetID(Category $category) { $key = $this->getCategoryKey($category); return $this->fbeHelper->getConfigValue($key); } /** * @param Category $category * compose the key for a given category * @return string */ public function getCategoryKey(Category $category) { return 'permanent/fbe/catalog/category/'.$category->getPath(); } /** * @param Category $category * if the category is Tops we might create "Default Category > Men > Tops" * @return string */ public function getCategoryPathName(Category $category) { $id = (string)$category->getId(); if (array_key_exists($id, $this->categoryMap)) { return $this->categoryMap[$id]; } return $category->getName(); } /** * @param Category $category * @param string $setID * save key with a fb product set id */ public function saveFBProductSetID(Category $category, string $setID) { $key = $this->getCategoryKey($category); $this->fbeHelper->saveConfig($key, $setID); } /** * @param Category $category * when getLevel() == 1 then it is root category * @return Category */ public function getRootCategory(Category $category) { $this->fbeHelper->log( "searching root category for ". $category->getName(). ' level:'.$category->getLevel() ); if ($category->getLevel() == 1) { return $category; } $parentCategory = $category->getParentCategory(); while ($parentCategory->getLevel() && $parentCategory->getLevel()>1) { $parentCategory = $parentCategory->getParentCategory(); } $this->fbeHelper->log("root category being returned".$parentCategory->getName()); return $parentCategory; } /** * @param Category $category * get the leave node in category tree, recursion is being used. * @return Category[] */ public function getBottomChildrenCategories(Category $category) { $this->fbeHelper->log( "searching bottom category for ". $category->getName(). ' level:'.$category->getLevel() ); if (!$category->hasChildren()) { $this->fbeHelper->log("no child category for ". $category->getName()); return [$category]; } $leaf_categories = []; $child_categories = $category->getChildrenCategories(); foreach ($child_categories as $child_category) { $sub_leaf_categories = $this->getBottomChildrenCategories($child_category); foreach ($sub_leaf_categories as $category) { $leaf_categories[] = $category; } } $this->fbeHelper->log( "number of leaf category being returned for ". $category->getName() . ": ".count($leaf_categories) ); return $leaf_categories; } /** * @param Category $category * get all children node in category tree, recursion is being used. * @return Category[] */ public function getAllChildrenCategories(Category $category) { $this->fbeHelper->log("searching children category for ". $category->getName()); $all_children_categories = []; // including not only direct child, but also child's child.... array_push($all_children_categories, $category); $children_categories = $category->getChildrenCategories(); // direct children only foreach ($children_categories as $children_category) { $sub_children_categories = $this->getAllChildrenCategories($children_category); foreach ($sub_children_categories as $category) { $all_children_categories[] = $category; } } return $all_children_categories; } /** * @return Category * @throws \Magento\Framework\Exception\LocalizedException */ public function getAllActiveCategories() { $categories = $this->_categoryCollection->create() ->addAttributeToSelect('*') ->addAttributeToFilter('is_active', 1) ->setStore($this->_storeManager->getStore()); return $categories; } /** * initial collection call after fbe installation, please not we only push leaf category to collection, * this means if a category contains any category, we won't create a collection for it. * @return string|null */ public function pushAllCategoriesToFbCollections() { $resArray = []; $access_token = $this->fbeHelper->getAccessToken(); if ($access_token == null) { $this->fbeHelper->log("can't find access token, abort pushAllCategoriesToFbCollections"); return; } $this->fbeHelper->log("pushing all categories to fb collections"); $categories = $this->getAllActiveCategories(); foreach ($categories as $category) { $syncEnabled =$category->getData("sync_to_facebook_catalog"); if ($syncEnabled === "0") { $this->fbeHelper->log("user disabled category sync ".$category->getName()); continue; } $this->fbeHelper->log("user enabled category sync ".$category->getName()); $set_id = $this->getFBProductSetID($category); $this->fbeHelper->log("setid for it is:". (string)$set_id); if ($set_id) { $response = $this->updateCategoryWithFB($category, $set_id); $resArray[] = $response; continue; } if (!$category->hasChildren()) { $response = $this->pushNewCategoryToFB($category); $resArray[] = $response; } } return json_encode($resArray); } /** * @param Category $category * call the api creating new product set * https://developers.facebook.com/docs/marketing-api/reference/product-set/ * @return string|null */ public function pushNewCategoryToFB(Category $category) { $this->fbeHelper->log("pushing category to fb collections: ".$category->getName()); $access_token = $this->fbeHelper->getAccessToken(); if ($access_token == null) { $this->fbeHelper->log("can't find access token, won't push new catalog category "); return; } $response = null; try { $url = $this->getCategoryCreateApi(); if ($url == null) { return; } $params = [ 'access_token' => $access_token, 'name' => $this->getCategoryPathName($category), 'filter' => $this->getCategoryProductFilter($category), ]; $this->curl->post($url, $params); $response = $this->curl->getBody(); } catch (\Exception $e) { $this->fbeHelper->logException($e); } $this->fbeHelper->log("response from fb: ".$response); $response_obj = json_decode($response, true); if (array_key_exists('id', $response_obj)) { $set_id = $response_obj['id']; $this->saveFBProductSetID($category, $set_id); $this->fbeHelper->log(sprintf("saving category %s and set_id %s", $category->getName(), $set_id)); } return $response; } /** * @param Category $category * create filter params for product set api * https://developers.facebook.com/docs/marketing-api/reference/product-set/ * e.g. {'retailer_id': {'is_any': ['10', '100']}} * @return string */ public function getCategoryProductFilter(Category $category) { $product_collection = $this->productCollectionFactory->create(); $product_collection->addAttributeToSelect('sku'); $product_collection->distinct(true); $product_collection->addCategoriesFilter(['eq' => $category->getId()]); $product_collection->getSelect()->limit(10000); $this->fbeHelper->log("collection count:".(string)count($product_collection)); $ids = []; foreach ($product_collection as $product) { array_push($ids, "'".$product->getId()."'"); } $filter = sprintf("{'retailer_id': {'is_any': [%s]}}", implode(',', $ids)); // $this->fbeHelper->log("filter:".$filter); return $filter; } /** * compose api creating new category (product set) e.g. * https://graph.facebook.com/v7.0/$catalogId/product_sets * @return string | null */ public function getCategoryCreateApi() { $catalogId = $this->getCatalogID(); if ($catalogId == null) { $this->fbeHelper->log("cant find catalog id, can't make category create api"); } $category_path = "/" . $catalogId . "/product_sets"; $category_create_api = $this->fbeHelper::FB_GRAPH_BASE_URL . $this->fbeHelper->getAPIVersion() . $category_path; $this->fbeHelper->log("Category Create API - " . $category_create_api); return $category_create_api; } /** * @param string $set_id * compose api creating new category (product set) e.g. * https://graph.facebook.com/v7.0/$catalogId/product_sets * @return string */ public function getCategoryUpdateApi(string $set_id) { $set_path = "/" . $set_id ; $set_update_api = $this->fbeHelper::FB_GRAPH_BASE_URL . $this->fbeHelper->getAPIVersion() . $set_path; $this->fbeHelper->log("product set update API - " . $set_update_api); return $set_update_api; } /** * @param Category $category * @param string $set_id * call the api update existing product set * https://developers.facebook.com/docs/marketing-api/reference/product-set/ * @return string|null */ public function updateCategoryWithFB(Category $category, string $set_id) { $access_token = $this->fbeHelper->getAccessToken(); if ($access_token == null) { $this->fbeHelper->log("can't find access token, won't update category with fb "); } $response = null; try { $url = $this->getCategoryUpdateApi($set_id); $params = [ 'access_token' => $access_token, 'name' => $this->getCategoryPathName($category), 'filter' => $this->getCategoryProductFilter($category), ]; $this->curl->post($url, $params); $response = $this->curl->getBody(); $this->fbeHelper->log("update category api response from fb:". $response); } catch (\Exception $e) { $this->fbeHelper->logException($e); } return $response; } /** * delete all existing product set on fb side * @return null */ public function deleteAllCategoryFromFB() { $categories = $this->getAllActiveCategories(); foreach ($categories as $category) { $this->deleteCategoryFromFB($category); } } /** * @param Category $category * call the api delete existing product set under this category * When user deletes a category on magento, we first get all sub categories(including itself), and check if we * have created a collection set on fb side, if yes then we make delete api call. * https://developers.facebook.com/docs/marketing-api/reference/product-set/ * @return null */ public function deleteCategoryAndSubCategoryFromFB(Category $category) { $children_categories = $this->getAllChildrenCategories($category); foreach ($children_categories as $children_category) { $this->deleteCategoryFromFB($children_category); } } /** * @param Category $category * call the api delete existing product set * this should be a low level function call, simple * https://developers.facebook.com/docs/marketing-api/reference/product-set/ * @return null */ public function deleteCategoryFromFB(Category $category) { $access_token = $this->fbeHelper->getAccessToken(); if ($access_token == null) { $this->fbeHelper->log("can't find access token, won't do category delete"); return; } $this->fbeHelper->log("category name:". $category->getName()); $set_id = $this->getFBProductSetID($category); if ($set_id == null) { $this->fbeHelper->log("cant find product set id, won't make category delete api"); return; } $set_path = "/" . $set_id . "?access_token=". $access_token; $url = $this->fbeHelper::FB_GRAPH_BASE_URL . $this->fbeHelper->getAPIVersion() . $set_path; // $this->fbeHelper->log("product set deletion API - " . $url); $response_body = null; try { $response_body = $this->httpClient->makeDeleteHttpCall($url); if (strpos($response_body, 'true') !== false) { $configKey = $this->getCategoryKey($category); } else { $this->fbeHelper->log("product set deletion failed!!! "); } } catch (\Exception $e) { $this->fbeHelper->logException($e); } } }