Skip to content

Instantly share code, notes, and snippets.

@julesbravo
Created January 14, 2019 18:27
Show Gist options
  • Save julesbravo/503a28609ad39f6aab397d86926f1d78 to your computer and use it in GitHub Desktop.
Save julesbravo/503a28609ad39f6aab397d86926f1d78 to your computer and use it in GitHub Desktop.
SearchSpring Generator.php
<?php
/**
* Helper to fetch all data and write feed
* Copyright (C) 2017 SearchSpring
*
* This file is part of SearchSpring/Feed.
*
* For the full copyright and license information, please view the LICENSE.txt
* file that was distributed with this source code.
*/
namespace SearchSpring\Feed\Helper;
use \Magento\Framework\AppInterface as AppInterface;
use \Magento\Framework\App\Http as Http;
use \Magento\Framework\App\Request\Http as RequestHttp;
use \Magento\Framework\App\Response\Http as ResponseHttp;
use \Magento\Framework\App\State as State;
use \Magento\Catalog\Api\ProductRepositoryInterface as ProductRepositoryInterface;
use \Magento\Catalog\Model\Product\Visibility as ProductVisibility;
use \Magento\CatalogInventory\Model\Stock\StockItemRepository as StockItemRepository;
use \Magento\Catalog\Helper\Image as ImageHelper;
use \Magento\Catalog\Model\CategoryRepository as CategoryRepository;
use \Magento\Catalog\Model\Product\OptionFactory as ProductOptionFactory;
use \Magento\Catalog\Model\ResourceModel\Product\CollectionFactory as ProductCollectionFactory;
use \Magento\Catalog\Model\ResourceModel\Eav\Attribute as AttributeFactory;
use \Magento\CatalogInventory\Api\StockRegistryInterface as StockRegistryInterface;
use \Magento\CatalogInventory\Helper\Stock as StockFilter;
use \Magento\Framework\App\Filesystem\DirectoryList as DirectoryList;
use \Magento\Framework\View\LayoutInterface as LayoutInterface;
use \Magento\Eav\Model\Config as EavConfig;
use \Magento\Store\Model\StoreManagerInterface as StoreManagerInterface;
use \Magento\Review\Model\RatingFactory as RatingFactory;
use \Magento\ConfigurableProduct\Model\Product\Type\Configurable as Configurable;
use \Magento\GroupedProduct\Model\Product\Type\Grouped as Grouped;
class Generator extends \Magento\Framework\App\Helper\AbstractHelper {
protected $productRecord = array();
protected $categoryCache = array();
protected $fields = array();
protected $objectManager;
protected $request;
protected $response;
protected $state;
protected $productCollectionFactory;
protected $productOptionFactory;
protected $productRepositoryInterface;
protected $productVisibility;
protected $stockItemRepository;
protected $productImageHelper;
protected $categoryRepository;
protected $attributeFactory;
protected $rating;
protected $stockFilter;
protected $stockRegistryInterface;
protected $layoutInterface;
protected $eavConfig;
protected $storeManager;
protected $storeId;
protected $count = 100;
protected $page = 1;
protected $thumbWidth = 250;
protected $thumbHeight = 250;
protected $keepAspectRatio = 1;
protected $hierarchySeparator = '/';
protected $multiValuedSeparator = '|';
protected $includeUrlHierarchy = false;
protected $includeOutOfStock = false;
protected $includeJSONConfig = false;
protected $includeChildPrices = false;
protected $includeTierPricing = false;
// Extra image types to include, by default we only include product_thumbnail_image
protected $imageTypes = array();
protected $ignoreFields;
protected $filename = '';
protected $feedPath;
protected $tmpFile;
protected $tmpFilename;
public function __construct(
RequestHttp $request,
ResponseHttp $response,
State $state,
ProductVisibility $productVisibility,
ProductOptionFactory $productOptionFactory,
ProductCollectionFactory $productCollectionFactory,
ProductRepositoryInterface $productRepository,
StockItemRepository $stockItemRepository,
ImageHelper $productImageHelper,
CategoryRepository $categoryRepository,
AttributeFactory $attributeFactory,
RatingFactory $ratingFactory,
StockFilter $stockFilter,
StockRegistryInterface $stockRegistryInterface,
LayoutInterface $layoutInterface,
StoreManagerInterface $storeManager,
DirectoryList $directoryList,
EavConfig $eavConfig
) {
$this->request = $request;
$this->response = $response;
$this->_state = $state;
$this->productCollectionFactory = $productCollectionFactory;
$this->productOptionFactory = $productOptionFactory;
$this->productRepository = $productRepository;
$this->productVisibility = $productVisibility;
$this->stockItemRepository = $stockItemRepository;
$this->productImageHelper = $productImageHelper;
$this->categoryRepository = $categoryRepository;
$this->attributeFactory = $attributeFactory;
$this->rating = $ratingFactory->create();
$this->stockFilter = $stockFilter;
$this->stockRegistryInterface = $stockRegistryInterface;
$this->layoutInterface = $layoutInterface;
$this->storeManager = $storeManager;
$this->productEntityTypeId = $eavConfig->getEntityType(\Magento\Catalog\Model\Product::ENTITY)->getEntityTypeId();
$this->eavConfig = $eavConfig;
$this->storeId = $this->request->getParam('store', 'default');
$this->storeManager->setCurrentStore($this->storeId);
$this->count = $this->request->getParam('count', 100);
$this->page = $this->request->getParam('page', 1);
if($this->page == 0) {
$this->page = 1;
}
$this->thumbWidth = $this->request->getParam('thumbWidth', 250);
$this->thumbHeight = $this->request->getParam('thumbHeight', 250);
$this->keepAspectRatio = $this->request->getParam('keepAspectRatio', 1);
$this->hierarchySeparator = $this->request->getParam('hierarchySeparator', '/');
$this->multiValuedSeparator = $this->request->getParam('multiValuedSeparator', '|');
$this->includeUrlHierarchy = $this->request->getParam('includeUrlHierarchy', 0);
$this->includeJSONConfig = $this->request->getParam('includeJSONConfig', 0);
$this->includeChildPrices = $this->request->getParam('includeChildPrices', 0);
$this->includeTierPricing = $this->request->getParam('includeTierPricing', 0);
$this->imageTypes = $this->request->getParam('imageTypes', array());
if(!is_array($this->imageTypes)) {
throw new \Exception('Image types must be an array. Example: imageTypes[]=product_small_image');
}
$this->includeOutOfStock = $this->request->getParam('includeOutOfStock', 0);
$this->ignoreFields = $this->request->getParam('ignoreFields', array());
if(!is_array($this->ignoreFields)) {
throw new \Exception('Ignore fields must be an array. Example: ignoreFields[]=description');
}
$filename = $this->request->getParam('filename', '');
$this->feedPath = $this->request->getParam('path', $directoryList->getPath('media') . '/searchspring');
$this->tmpFilename = 'searchspring-' . $this->storeId . ($filename ? '-' . $filename : '') . '.tmp.csv';
if(!is_dir($this->feedPath)) {
mkdir($this->feedPath, 0755, true);
}
// TODO explore using CSV writer built into Magento, it looks like it can only write whole file and not append
$this->tmpFile = fopen($this->feedPath . '/' . $this->tmpFilename, 'a');
}
public function generate()
{
$this->getFields();
if($this->page == 1) {
$this->writeHeader();
}
$collection = $this->getProductCollection();
foreach($collection as $product) {
$this->productRecord = array();
$this->addProductAttributesToRecord($product);
$this->addChildAttributesToRecord($product);
$this->addOptionsToRecord($product);
$this->addImagesToRecord($product);
$this->addStockInfoToRecord($product);
$this->addCategoriesToRecord($product);
$this->addRatingsToRecord($product);
$this->addPricesToRecord($product);
if($this->includeJSONConfig) {
$this->addJSONConfig($product);
}
$this->setRecordValue('saleable', $product->isSaleable());
$this->setRecordValue('url', $product->getProductUrl());
$this->writeRecord();
}
// Check if we're on last page
if($collection->getSize() <= $this->page * $this->count) {
// If on last page write feed file and send complete
$this->moveFeed();
$this->response->setBody('Complete');
} else {
// Else let regenerator know to request next page
$this->response->setBody('Continue|'. ($this->page+1));
}
$this->response->setHttpResponseCode(200);
fclose($this->tmpFile);
}
protected function moveFeed() {
$filename = 'searchspring-' . $this->storeId . '.csv';
rename($this->feedPath . '/' . $this->tmpFilename, $this->feedPath . '/' . $filename);
}
protected function getProductCollection() {
$collection = $this->productCollectionFactory->create()
->addAttributeToSelect('*')
// TODO COMMENT, FOR TESTING ONLY
// ->addAttributeToFilter('entity_id', array('eq' => 1))
->setVisibility($this->productVisibility->getVisibleInSiteIds())
->addAttributeToFilter(
'status', array('eq' => \Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED)
)
->setPageSize($this->count)
->setCurPage($this->page);
if(!$this->includeOutOfStock) {
$this->stockFilter->addInStockFilterToCollection($collection);
}
return $collection;
}
protected function addProductAttributesToRecord($product) {
$attributes = $product->getAttributes();
foreach($attributes as $attribute) {
$code = $attribute->getAttributeCode();
$value = $this->getProductAttribute($product, $attribute);
var_dump($code, $value);
$this->setRecordValue($code, $value);
}
}
protected function getProductAttribute($product, $attribute) {
$code = $attribute->getAttributeCode();
if($attribute->usesSource()) {
$value = $product->getAttributeText($code);
} else {
$value = $product->getData($code);
}
if(is_object($value)) {
if($value instanceof \Magento\Framework\Phrase) {
$value = $value->getText();
} else {
throw new \Exception("Unknown value object type " . get_class($value));
}
}
return $value;
}
protected function addChildAttributesToRecord($product) {
$otherChildAttributes = array(
"mb_adapter_option",
"mb_blade_bolt_pattern",
"mb_blade_feature",
"mb_break_range_required",
"mb_break_away_type",
"mb_digitrak_locater_model",
"mb_duct_size",
"mb_fastream_cutter_block",
"mb_oem",
"mb_pilot_cutting_diameter",
"mb_pullback_type",
"mb_pulling_sling_cable",
"mb_pulling_sling_legs",
"mb_quick_disconnect_type",
"mb_reamer_cutting_size",
"mb_reamer_shaft_size",
"mb_rig_model",
"mb_soil_type_best",
"mb_swivel_capacities",
"mb_swivel_connection",
"mb_swivel_thread",
"mb_thread_connection_type",
"mb_transmitter_diameter",
"mb_transmitter_front_connect",
"mb_transmitter_housing_type",
"mb_transmitter_type"
);
$childAttributes = array();
$childAttributeIds = array();
if(Configurable::TYPE_CODE === $product->getTypeId()) {
$attributes = $product->getTypeInstance(true)->getConfigurableAttributes($product);
foreach($attributes as $attribute) {
$productAttribute = $attribute->getProductAttribute();
if($productAttribute) {
$childAttributes[] = $productAttribute;
$childAttributeIds[] = $productAttribute->getId();
}
}
foreach($otherChildAttributes as $attribute) {
$productAttribute = $this->eavConfig->getAttribute("catalog_product", $attribute);
if($productAttribute) {
$childAttributes[] = $productAttribute;
$childAttributeIds[] = $productAttribute->getId();
}
}
$children = $product->getTypeInstance()->getUsedProducts($product);
foreach($children as $child) {
$fullChild = $this->productRepository->getById($child->getId());
foreach($childAttributes as $childAttribute) {
$code = $childAttribute->getAttributeCode();
$value = $this->getProductAttribute($fullChild, $childAttribute);
if($value !== false) {
$this->setRecordValue($code, $value);
}
}
// NOTE We're not using child_qty anymore as that should be
// taken care of by saleable. If there is a need adding it here
// should be easy.
$this->setRecordValue('child_sku', $child->getSku());
$this->setRecordValue('child_name', $child->getName());
if($this->includeChildPrices) {
$price = $child->getPriceInfo()->getPrice('final_price')->getMinimalPrice()->getValue();
$this->setRecordValue('child_final_price', $price);
}
}
}
if(Grouped::TYPE_CODE === $product->getTypeId()) {
foreach($otherChildAttributes as $attribute) {
$productAttribute = $this->eavConfig->getAttribute("catalog_product", $attribute);
if($productAttribute) {
$childAttributes[] = $productAttribute;
$childAttributeIds[] = $productAttribute->getId();
}
}
$children = $product->getTypeInstance()->getAssociatedProducts($product);
foreach($children as $child) {
$fullChild = $this->productRepository->getById($child->getId());
foreach($childAttributes as $childAttribute) {
$code = $childAttribute->getAttributeCode();
$value = $this->getProductAttribute($fullChild, $childAttribute);
if($value !== false) {
$this->setRecordValue($code, $value);
}
}
$this->setRecordValue('child_sku', $child->getSku());
$this->setRecordValue('child_name', $child->getName());
if($this->includeChildPrices) {
$price = $child->getPriceInfo()->getPrice('final_price')->getMinimalPrice()->getValue();
$this->setRecordValue('child_final_price', $price);
}
}
};
}
protected function addOptionsToRecord($product) {
$options = $this->productOptionFactory->create()->getProductOptionCollection($product);
foreach($options as $option) {
// Add drop down options to data
if($option->getType() == 'drop_down') {
// Clean up option title for a field name
$field = 'option_' . $this->textToFieldName($option->getTitle());
$values = $option->getValues();
foreach($values as $value) {
$this->setRecordValue($field, $value->getTitle());
}
}
}
}
protected function addImagesToRecord($product) {
$this->setRecordValue('cached_thumbnail', $this->getThumbnail($product));
foreach($this->imageTypes as $type) {
$this->setRecordValue('cached_'.$type, $this->getThumbnail($product, $type));
}
}
protected function getThumbnail($product, $type = 'product_thumbnail_image') {
if($this->keepAspectRatio) {
$resizedImage = $this->productImageHelper->init($product, $type)
->constrainOnly(TRUE)
->keepAspectRatio(TRUE)
->keepTransparency(TRUE)
->keepFrame(FALSE)
->resize($this->thumbWidth, $this->thumbHeight)
->getUrl();
} else {
$resizedImage = $this->productImageHelper->init($product, $type)
->resize($this->thumbWidth, $this->thumbHeight)
->getUrl();
}
return $resizedImage;
}
protected function addStockInfoToRecord($product) {
$stockItem = $this->stockRegistryInterface->getStockItem($product->getId());
$this->setRecordValue('in_stock', $stockItem->getIsInStock());
$this->setRecordValue('stock_qty', $stockItem->getQty());
}
protected function addCategoriesToRecord($product) {
$categoryIds = $product->getCategoryIds();
$categoryNames = array();
$categoryHierarchy = array();
if($this->includeUrlHierarchy) {
$urlHierarchy = array();
}
foreach($categoryIds as $categoryId) {
$category = $this->loadCategory($categoryId);
if(!$category['is_active']) {
continue;
}
$categoryNames[] = $category['name'];
foreach($category['hierarchy'] as $hierarchy) {
$categoryHierarchy[] = $hierarchy;
}
if($this->includeUrlHierarchy) {
foreach($category['url_hierarchy'] as $url) {
$urlHierarchy[] = $url;
}
}
}
$this->setRecordValue('categories', $categoryNames);
$this->setRecordValue('category_ids', $categoryIds);
$this->setRecordValue('category_hierarchy', array_unique($categoryHierarchy));
if($this->includeUrlHierarchy) {
$this->setRecordValue('url_hierarchy', array_unique($urlHierarchy));
}
}
protected function loadCategory($categoryId, $skipLevels = false) {
// TODO Ignore root categories? ex. Root Catalog
// TODO Use a Magento 2 cache instead of a variable that is cleared on page 1.
if(!isset($this->categoryCache[$categoryId])) {
$category = $this->categoryRepository->get($categoryId);
$categoryName = $category->getName();
$categoryPath = $category->getPath();
$categoryHierarchy = array();
if($this->includeUrlHierarchy) {
$categoryUrl = $category->getUrl();
$urlHierarchy = array();
}
if(!$skipLevels) {
$levels = explode('/', $categoryPath);
$currentHierarchy = array();
foreach($levels as $level) {
if($level == $categoryId) {
$currentCategoryName = $categoryName;
if($this->includeUrlHierarchy) {
$currentCategoryUrl = $categoryUrl;
}
} else {
$levelCategory = $this->loadCategory($level, true);
$currentCategoryName = $levelCategory['name'];
if($this->includeUrlHierarchy) {
$currentCategoryUrl = $levelCategory['url'];
}
}
$currentHierarchy[] = $currentCategoryName;
$hierarchy = implode($this->hierarchySeparator, $currentHierarchy);
$categoryHierarchy[] = $hierarchy;
if($this->includeUrlHierarchy) {
$urlHierarchy[] = $hierarchy . '[' . $currentCategoryUrl . ']';
}
}
}
$catCache = array(
'name' => $categoryName,
'hierarchy' => $categoryHierarchy,
'is_active' => $category->getIsActive()
);
if($this->includeUrlHierarchy) {
$catCache['url'] = $categoryUrl;
$catCache['url_hierarchy'] = $urlHierarchy;
}
$this->categoryCache[$categoryId] = $catCache;
}
return $this->categoryCache[$categoryId];
}
protected function addRatingsToRecord($product) {
$rating = $this->rating->getEntitySummary($product->getId(), $this->storeId);
if($rating && $rating->getCount() > 0) {
$this->setRecordValue('rating', 5 * ($rating->getSum() / $rating->getCount()/100));
$this->setRecordValue('rating_count', $rating->getCount());
}
}
protected function addJSONConfig($product) {
if(Configurable::TYPE_CODE === $product->getTypeId()) {
$configBlock = $this->layoutInterface->createBlock("\Magento\ConfigurableProduct\Block\Product\View\Type\Configurable")->setData('product', $product);
$this->setRecordValue('json_config', $configBlock->getJsonConfig());
$swatchBlock = $this->layoutInterface->createBlock("\Magento\Swatches\Block\Product\Renderer\Configurable")->setData('product', $product);
$this->setRecordValue('swatch_json_config', $swatchBlock->getJsonSwatchConfig());
}
}
protected function addPricesToRecord($product) {
$price = $product->getPriceInfo()->getPrice('final_price')->getMinimalPrice()->getValue();
$this->setRecordValue('final_price', $price);
if($this->includeTierPricing) {
$tierPrice = $product->getTierPrice();
$this->setRecordValue('tier_pricing', json_encode($tierPrice));
}
}
protected function getFields() {
// TODO Cache this in a magento cache instead of building it each time. Clear on page 1.
$this->fields = array(
// Core Magento ID Fields
'entity_id',
'type_id',
'attribute_set_id',
// SearchSpring Generated Fields
'cached_thumbnail',
'stock_qty',
'in_stock',
'categories',
'category_hierarchy',
'saleable',
'url',
'final_price',
'rating',
'rating_count',
'child_sku',
'child_name'
);
if($this->includeUrlHierarchy) {
$this->fields[] = 'url_hierarchy';
}
if($this->includeChildPrices) {
$this->fields[] = 'child_final_price';
}
if($this->includeJSONConfig) {
$this->fields[] = 'json_config';
$this->fields[] = 'swatch_json_config';
}
if($this->includeTierPricing) {
$this->fields[] = 'tier_pricing';
}
foreach($this->imageTypes as $type) {
$this->fields[] = 'cached_'.$type;
}
$attributes = $this->attributeFactory->getCollection();
$attributes->addFieldToFilter('entity_type_id', $this->productEntityTypeId);
foreach($attributes as $attribute) {
$field = $attribute->getAttributeCode();
$this->fields[] = $field;
}
$options = $this->productOptionFactory->create()
->getCollection()
->addTitleToResult($this->storeId);
foreach($options as $option) {
$this->fields[] = 'option_' . $this->textToFieldName($option->getTitle());
}
// Remove ignored fields
$this->fields = array_diff($this->fields, $this->ignoreFields);
}
protected function textToFieldName($text) {
return strtolower(preg_replace('/_+/', '_', preg_replace('/[^a-z0-9_]+/i', '_', trim($text))));
}
protected function setRecordValue($field, $value) {
if(in_array($field, $this->ignoreFields)) {
return;
}
// Don't bother adding if the value is empty
if(is_null($value) || $value == array() || $value == '') {
return;
}
if(!isset($this->productRecord[$field])) {
$this->productRecord[$field] = array();
}
if(!is_array($value)) {
$this->productRecord[$field][] = $value;
} else {
$this->productRecord[$field] = array_merge($this->productRecord[$field], $value);
}
}
protected function writeHeader() {
fputcsv($this->tmpFile, $this->fields);
}
protected function writeRecord() {
$row = array();
foreach($this->fields as $field) {
if(isset($this->productRecord[$field])) {
$value = $this->productRecord[$field];
// If value is an array of arrays or objects then json encode value
if(is_array(current($value)) || is_object(current($value))) {
$row[] = json_encode($value);
} else {
$row[] = implode($this->multiValuedSeparator, array_unique($value));
}
} else {
$row[] = '';
}
}
// Start custom CSV write to handle escaped JSON
$delimiter = ",";
$delimiter_esc = preg_quote($delimiter, '/');
$enclosure = '"';
$enclosure_esc = preg_quote($enclosure, '/');
$output = array();
foreach ($row as $field) {
$output[] = preg_match("/(?:${delimiter_esc}|${enclosure_esc}|\s)/", $field) ? (
$enclosure . str_replace($enclosure, $enclosure . $enclosure, $field) . $enclosure
) : $field;
}
fwrite($this->tmpFile, join($delimiter, $output) . "\n");
// End custom CSV write
}
}
?>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment