Your IP : 127.0.0.1


Current Path : /home/dev2.destoffenstraat.com/app/Firebear/ImportExport/Model/Export/Adapter/
Upload File :
Current File : /home/dev2.destoffenstraat.com/app/Firebear/ImportExport/Model/Export/Adapter/GoogleSheet.php

<?php
/**
 * @copyright: Copyright © 2017 Firebear Studio. All rights reserved.
 * @author   : Firebear Studio <fbeardev@gmail.com>
 */

namespace Firebear\ImportExport\Model\Export\Adapter;

use Exception;
use Google_Client;
use Google_Exception;
use Google_Service_Exception;
use Google_Service_Sheets;
use Google_Service_Sheets_BatchUpdateSpreadsheetRequest;
use Google_Service_Sheets_Request;
use Google_Service_Sheets_Sheet;
use Google_Service_Sheets_ValueRange;
use Magento\Framework\App\CacheInterface;
use Magento\Framework\Filesystem;
use Magento\Framework\Filesystem\DirectoryList;
use Psr\Log\LoggerInterface;
use RuntimeException;

/**
 * GoogleSheet Adapter
 * Requires composer packages google/apiclient:^2.0
 *
 * phpcs:disable PSR2.Classes.PropertyDeclaration.Underscore
 * @SuppressWarnings(PHPMD.CamelCasePropertyName)
 * @link https://developers.google.com/sheets/api/quickstart/php
 */
class GoogleSheet extends AbstractAdapter
{
    /**
     * Api constants
     */
    const DIMENSION_ROWS = 'ROWS';
    const DIMENSION_COLS = 'COLUMNS';

    /**
     * Cache storage
     *
     * @var CacheInterface
     */
    protected $_cache;

    /**
     * Directory list
     *
     * @var DirectoryList
     */
    protected $_directoryList;

    /**
     * Sheets Api
     *
     * @var Google_Service_Sheets
     */
    protected $_sheetsService;

    /**
     * Count of rows to push to the Google Sheet by single API request.
     *
     * @see GoogleSheet::getBatchSize()
     * @var int
     */
    protected $_batchMaxSize = 1000;

    /**
     * Starting cell template
     *
     * @see GoogleSheet::getRange()
     * @var string
     */
    protected $_range = 'A%s:%s';

    /**
     * Current cursor position. Used to append data to the document
     *
     * @var int
     */
    protected $_linesCounter = 1;

    /**
     * Rows prepared to be sent to the Google Sheet
     *
     * @var array
     */
    protected $_exportQueue = [];

    /**
     * JSON string with configuration for API authorization
     *
     * @var string
     */
    protected $_authConfig;

    /**
     * Google sheet id
     *
     * @var string
     */
    protected $_sheetId;

    /**
     * Google spreadsheet id
     *
     * @var string
     */
    protected $_spreadsheetId;

    /**
     * Access token
     *
     * @var string
     */
    protected $_accessToken;

    /**
     * Sheet columns count
     *
     * @var int
     */
    protected $_currentColumnsCount;

    /**
     * Sheet rows count
     *
     * @var int
     */
    protected $_currentRowsCount;

    /**
     * Sheet title
     *
     * @var string
     */
    protected $_sheetTitle;

    /**
     * GoogleSheet constructor.
     * @param Filesystem $filesystem
     * @param LoggerInterface $logger
     * @param DirectoryList $directoryList
     * @param CacheInterface $cache
     * @param null $destination
     * @param string $destinationDirectoryCode
     * @param array $data
     * @throws \Magento\Framework\Exception\LocalizedException
     */
    public function __construct(
        Filesystem $filesystem,
        LoggerInterface $logger,
        DirectoryList $directoryList,
        CacheInterface $cache,
        $destination = null,
        $destinationDirectoryCode = \Magento\Framework\App\Filesystem\DirectoryList::VAR_DIR,
        array $data = []
    ) {
        $this->_directoryList = $directoryList;
        $this->_cache = $cache;

        $this->_authConfig = $this->getAuthConfig($data['export_source']['signing_key_file_path']);
        $this->_accessToken = $this->getAccessToken();
        $this->_spreadsheetId = $data['export_source']['spreadsheet_id'];
        $this->_sheetId = $data['export_source']['sheet_id'];

        parent::__construct($filesystem, $logger, $destination, $destinationDirectoryCode, $data);
    }

    /**
     * Get auth config
     *
     * @param string $filePath
     * @return string
     * @throws RuntimeException
     */
    protected function getAuthConfig($filePath)
    {
        $rootPath = $this->_directoryList->getRoot();
        $absolutePath = $rootPath . DIRECTORY_SEPARATOR . $filePath;
        if (!file_exists($absolutePath)) {
            throw new RuntimeException('Auth file ' . $filePath . ' isn\'t readable or the file has been removed');
        }

        return file_get_contents($absolutePath);
    }

    /**
     * Get access token
     *
     * @return string
     * @throws RuntimeException
     */
    protected function getAccessToken()
    {
        $authConfig = json_decode($this->_authConfig, true);
        if (empty($authConfig['private_key_id'])) {
            throw new RuntimeException('Auth file doesn\'t contain all necessary settings to get Auth Token');
        }

        return $authConfig['private_key_id'];
    }

    /**
     * Collect items into batches and only then run single api request
     *
     * @throws Google_Exception
     * @throws Exception
     * @inheritdoc
     */
    public function writeRow(array $rowData)
    {
        if ($this->_headerCols === null) {
            $this->_headerCols = array_keys($rowData);
            $this->_exportQueue[] = $this->_headerCols;
        }

        $this->_exportQueue[] = array_values($rowData);
        if (count($this->_exportQueue) && count($this->_exportQueue) >= $this->getBatchSize()) {
            $this->runExportQueue();
        }

        return $this;
    }

    /**
     * Run export queue
     *
     * @throws Exception
     */
    protected function runExportQueue()
    {
        if (!count($this->_exportQueue)) {
            // Nothing to export
            return;
        }

        try {
            $this->exportQueue();
        } catch (Exception $exception) {
            if ($exception instanceof Google_Service_Exception) {
                $reason = $exception->getErrors()[0]['reason'];
                if ($reason == 'rateLimitExceeded') {
                    // Try to do short delay before run api requests again
                    $this->doDelay(10);
                }
            }

            throw $exception;
        }
    }

    /**
     * Export rows in queue to the specified sheet
     *
     * @throws Google_Exception
     */
    protected function exportQueue()
    {
        if ($this->_linesCounter == 1) {
            $this->fetchSheetInformation();
            $this->clearTable();

            $this->prepareTableColumns(count($this->_headerCols));
        }

        $this->prepareTableRows(count($this->_exportQueue));

        $this->insertRows();
        $this->postExportQueue();
    }

    /**
     * Fetch target table information
     *
     * @throws Google_Exception
     */
    protected function fetchSheetInformation()
    {
        /** @var Google_Service_Sheets_Sheet[] $sheets */
        $sheets = $this->getApi()->spreadsheets->get($this->_spreadsheetId);
        $sheet = $this->getSheetById($this->_sheetId, $sheets);

        $this->_currentColumnsCount = $sheet->getProperties()->getGridProperties()->getColumnCount();
        $this->_currentRowsCount = $sheet->getProperties()->getGridProperties()->getRowCount();
        $this->_sheetTitle = $sheet->getProperties()->getTitle();

        $this->doDelay();
    }

    /**
     * Get target sheet by sheetId
     *
     * @param string $sheetId
     * @param Google_Service_Sheets_Sheet[] $sheets
     * @return Google_Service_Sheets_Sheet
     * @throws RuntimeException
     */
    protected function getSheetById($sheetId, $sheets)
    {
        $neededSheet = null;
        foreach ($sheets as $sheet) {
            if ($sheet->getProperties()->getSheetId() == $sheetId) {
                $neededSheet = $sheet;
                break;
            }
        }

        if ($neededSheet === null) {
            throw new RuntimeException('Failed to find sheet #' . $sheetId);
        }

        return $neededSheet;
    }

    /**
     * Reset table to empty document state with 1 ros and column.
     * Google gives only 5000000 cells(~40000 products) per all tabs in document
     *
     * @throws Google_Exception
     */
    protected function clearTable()
    {
        $requests = [
            new Google_Service_Sheets_Request([
                'deleteDimension' => [
                    'range' => [
                        'sheetId' => $this->_sheetId,
                        'startIndex' => 0,
                        'endIndex' => $this->_currentRowsCount - 1,
                        'dimension' => self::DIMENSION_ROWS
                    ]
                ]
            ]),
            new Google_Service_Sheets_Request([
                'deleteDimension' => [
                    'range' => [
                        'sheetId' => $this->_sheetId,
                        'startIndex' => 0,
                        'endIndex' => $this->_currentColumnsCount - 1,
                        'dimension' => self::DIMENSION_COLS
                    ]
                ]
            ]),
        ];

        $this->getApi()->spreadsheets->batchUpdate(
            $this->_spreadsheetId,
            new Google_Service_Sheets_BatchUpdateSpreadsheetRequest([
                'requests' => $requests
            ])
        );

        $this->_currentRowsCount = 1;
        $this->_currentColumnsCount = 1;

        $this->doDelay();
    }

    /**
     * In case if table has not enough cols we need to add them
     *
     * @param int $neededColumnsCount
     * @throws Google_Exception
     */
    protected function prepareTableColumns($neededColumnsCount)
    {
        if ($this->_currentColumnsCount >= $neededColumnsCount) {
            return;
        }

        $requests = [
            new Google_Service_Sheets_Request([
                'insertDimension' => [
                    'range' => [
                        'sheetId' => $this->_sheetId,
                        'startIndex' => $this->_currentColumnsCount - 1,
                        'endIndex' => $neededColumnsCount - 1,
                        'dimension' => self::DIMENSION_COLS
                    ]
                ]
            ])
        ];

        $this->getApi()->spreadsheets->batchUpdate(
            $this->_spreadsheetId,
            new Google_Service_Sheets_BatchUpdateSpreadsheetRequest([
                'requests' => $requests
            ])
        );

        $this->_currentColumnsCount = $neededColumnsCount;
        $this->doDelay();
    }

    /**
     * In case if table has not enough rows we need to add them
     *
     * @param int $rowsToAdd
     * @throws Google_Exception
     */
    protected function prepareTableRows($rowsToAdd)
    {
        if ($this->_linesCounter + $rowsToAdd < $this->_currentRowsCount) {
            return;
        }

        $startIndex = $this->_linesCounter - 1;
        $requests = [
            new Google_Service_Sheets_Request([
                'insertDimension' => [
                    'range' => [
                        'sheetId' => $this->_sheetId,
                        'startIndex' => $startIndex,
                        'endIndex' => $startIndex + $rowsToAdd,
                        'dimension' => self::DIMENSION_ROWS
                    ]
                ]
            ])
        ];

        $this->getApi()->spreadsheets->batchUpdate(
            $this->_spreadsheetId,
            new Google_Service_Sheets_BatchUpdateSpreadsheetRequest([
                'requests' => $requests
            ])
        );

        $this->_currentRowsCount += $rowsToAdd;
        $this->doDelay();
    }

    /**
     * Insert rows to the table
     *
     * @throws Google_Exception
     */
    protected function insertRows()
    {
        $options = ['valueInputOption' => 'RAW'];
        $body = new Google_Service_Sheets_ValueRange(['values' => $this->_exportQueue]);

        $this->getApi()
            ->spreadsheets_values
            ->update($this->_spreadsheetId, $this->getRange(), $body, $options);
    }

    /**
     * Get api client to work with sheet values
     *
     * @return Google_Service_Sheets
     * @throws Google_Exception
     */
    protected function getApi()
    {
        if ($this->_sheetsService === null) {
            $client = new Google_Client();
            $client->setApplicationName('');
            $client->setScopes(Google_Service_Sheets::SPREADSHEETS);
            $client->setAuthConfig(json_decode($this->_authConfig, true));
            $client->setAccessToken($this->_accessToken);

            $this->_sheetsService = new Google_Service_Sheets($client);
        }

        return $this->_sheetsService;
    }

    /**
     * Get entities count
     *
     * @return int
     */
    public function getEntitiesCount()
    {
        return $this->_cache->load('export_entities_count');
    }

    /**
     * Get num rows per API request
     *
     * @return int
     */
    protected function getBatchSize()
    {
        $entitiesCount = $this->getEntitiesCount();
        if ($this->_batchMaxSize > $entitiesCount) {
            return $entitiesCount;
        }

        return $this->_batchMaxSize;
    }

    /**
     * Get current cell number
     *
     * @return string
     */
    protected function getRange()
    {
        $columnId = $this->getNameFromNumber($this->_currentColumnsCount);
        $cellRange = sprintf($this->_range, $this->_linesCounter, $columnId);

        return $this->_sheetTitle . '!' . $cellRange;
    }

    /**
     * Get google sheet column a1 notation string
     *
     * @param int $num
     * @return string
     */
    protected function getNameFromNumber(int $num)
    {
        $numeric = $num % 26;
        $codePoint = 65 + $numeric;
        if ($codePoint < 65) {
            $codePoint = 65;
        } elseif ($codePoint > 90) {
            $codePoint = 90;
        }
        $letter = chr($codePoint);
        $num2 = $num / 26;

        if ($num2 > 0) {
            return $this->getNameFromNumber((int)$num2 - 1) . $letter;
        } else {
            return $letter;
        }
    }

    /**
     * Clear queue, memory, etc
     */
    protected function postExportQueue()
    {
        $this->_linesCounter += count($this->_exportQueue);
        $this->_exportQueue = [];
    }

    /**
     * Google Sheets API has limitation 100 requests per 100 seconds
     *
     * @param int $seconds
     */
    protected function doDelay($seconds = 1)
    {
        sleep($seconds);
    }

    /**
     * Should return something or the log file would have not information
     *
     * @see \Firebear\ImportExport\Model\Export::export()
     * @inheritdoc
     * @throws Exception
     */
    public function getContents()
    {
        $this->runExportQueue();
        return '1';
    }
}