Shoppingfeed Prestashop module developper guide

Shoppingfeed Prestashop module developper guide

{
  "require": {
    "shoppingfeed/php-sdk": "0.2.1-beta.1"
  },
  "config": {
    "vendor-dir": "vendor"
  },
  "autoload": {
    "files": [
      "vendor/prefixed/guzzlehttp/guzzle/src/functions_include.php",
      "vendor/prefixed/guzzlehttp/psr7/src/functions_include.php",
      "vendor/prefixed/guzzlehttp/promises/src/functions_include.php"
    ],
    "psr-4": {
      "SfGuzzle\\GuzzleHttp\\":          "vendor/prefixed/guzzlehttp/guzzle/src/",
      "SfGuzzle\\GuzzleHttp\\Psr7\\":    "vendor/prefixed/guzzlehttp/psr7/src/",
      "SfGuzzle\\GuzzleHttp\\Promise\\": "vendor/prefixed/guzzlehttp/promises/src/",
      "ShoppingFeed\\Sdk\\":             "vendor/prefixed/shoppingfeed/php-sdk/src/"
    }
  }
}

The "directly included files" (e.g. vendor/prefixed/guzzlehttp/guzzle/src/functions_include.php) must be modified by hand, as they use namespaces in strings.

if (!function_exists('SfGuzzle\GuzzleHttp\uri_template')) {
    require __DIR__ . '/functions.php';
}

You can find which files should be included directly by looking at the libraries own composer.json files (e.g. /guzzlehttp/guzzle/composer.json).
You can also check these files to fill the psr-4 namespaces in the main composer.json.

Autoloading

As of now, the only way to prevent Composer from autoloading Guzzle is to rename or remove the folder. Using the "exclude-from-classmap" property does not work, since the namespace can still be resolved using the filesystem.
We don't have to do it for the SDK however, since we're not trying to prevent Composer from loading it, but to have it loaded from a different path.

Nevertheless, the PHP script moves all the "original" libraries to the 202 folder to solve those problems.

Usage

From the command line :

class ShoppingfeedProductSyncStockActions extends ShoppingfeedProductSyncActions
{
    // ...
}

Since saving and retrieving updates to process is similar for every synchronizable fields, the abstract ShoppingfeedProductSyncActions is the one implementing those features. Every "field process" (e.g. ShoppingfeedProductSyncStockActions) should implement the remaining methods. Note that when real-time synchronization is selected, the ShoppingfeedProductSyncActions::saveProduct method will automatically forward to the ShoppingfeedProductSyncActions::getBatch method to run the synchronization process.

Processing the updates with batch synchronization

As previously mentioned, the syncProduct controller will process batch synchronization; it is responsible for checking which fields should be updated.

class ShoppingfeedSyncProductModuleFrontController extends CronController
{
    protected function processCron($data)
    {
        // Every field to synchronize should follow this pattern
        if(Configuration::get(Shoppingfeed::STOCK_SYNC_ENABLED)) {
            $actions[ShoppingfeedProduct::ACTION_SYNC_STOCK] = array(
                'actions_suffix' => 'Stock'
            );
        }

        // ...

        foreach($actions as $action => $actionData) {
            $this->processAction($action, $actionData['actions_suffix']);
        }

        // ...

    }

    // ...

    protected function processAction($action, $actions_suffix)
    {
        // ...

        // The 'actions_suffix' is used to find the Actions class
        $handler->setConveyor(array(
            'id_shop' => $shop['id_shop'],
            // The 'action' is the value saved in a ShoppingfeedProduct
            'product_action' => $action,
        ));
        $processResult = $handler->process('shoppingfeedProductSync' . $actions_suffix);

        // ...
    }
}

2. Mapping to Shopping Feed's catalog

Source: 02-features_products/02-product_mapping.md

The module uses the method Shoppingfeed::mapReference to get a product's reference for the Shopping Feed catalog. By default, this reference is {ps_id_product} (e.g. 1) or {ps_id_product}_{ps_id_product_attribute} if a declination is updated (e.g. 1_12).

To specify a different Shopping Feed reference, one may either :

  • Override the module's class and rewrite the mapReference method,
  • Create a module and hook it to the ShoppingfeedMapProductReference hook

// The reference is passed... by reference, so that you may tweak it.
Hook::exec(
    'ShoppingfeedMapProductReference', // hook_name
    array(
        'ShoppingFeedProduct' => &$sfProduct,
        'reference' => &$reference
    ) // hook_args
);

Note that having the method return false (weak comparison) will skip the update for the product and remove it from the updates list.


3. Stock synchronization

Source: 02-features_products/03-stock_sync.md

Detecting changes

A product's stock may change either when modified from the back-office, or during an order process.

Stock changes are detected through the actionUpdateQuantity hook. The updated product is saved as a ShoppingfeedProduct using the ActionsHandler component from Classlib; whether the update should be queued or immediately sent to the Shopping Feed API is managed in the abstract Action class.

Processing changes

Updated products are always saved in the database before being sent. The ProcessLogger component from Classlib is used to track every update process.

If real-time synchronization is selected, the ActionsHandler will execute the update chain immediately after saving the product. If not, a CRON task managed with Classlib's ProcessMonitor component will process the saved updates using the same chain of actions.


4. Price synchronization

Source: 02-features_products/04-price_sync.md

Detecting changes

A product's price may change only when modified from the back-office. Since there is no hook specific to price changes, the actionObjectProductUpdateBefore and actionObjectCombinationUpdateBefore are used to detect price changes.

Only products whose prices were updated are added to the updates list. However, a combination's price may change due to the "original" product's price changing.
Therefore, when a product's price change is detected in the actionObjectProductUpdateBefore hook, an id_product is saved in the classlib Registry so that the actionObjectCombinationUpdateBefore hook can check whether an updated combination should be added to the updates list.

Note : if a product or combination is updated, but does not pass Prestashop's validation, it might still be added to the list if its price has changed. But this shouldn't happen often, and since it will probably be re-updated with correct values right away, it shouldn't be a problem.

The updated product is saved as a ShoppingfeedProduct using the ActionsHandler component from Classlib; whether the update should be queued or immediately sent to the Shopping Feed API is managed in the abstract Actions class.

Processing changes

Updated products are always saved in the database before being sent. The ProcessLogger component from Classlib is used to track every update process.

If real-time synchronization is selected, the ActionsHandler will execute the update chain in the actionObjectProductUpdateAfter and actionObjectCombinationUpdateAfter hooks. If not, a CRON task managed with Classlib's ProcessMonitor component will process the saved updates using the same chain of actions.


5. Products feed synchronization

Source: 02-features_products/05-product_sync.md

Overview

The XML feed is not computed when Shoppingfeed call it. In fact each time a product is updated (on actionObjectProductUpdateBefore or actionObjectCombinationUpdateBefore), a serialized version of the product is put in cache (on the database).

Add or modify content on the feed

You can used 3 hooks called just before put in cache the serialized product:

  • shoppingfeedSerialize
  • shoppingfeedSerializePrice
  • shoppingfeedSerializeStock

The array content of the product are set by reference, so you can modify it as you want.


Orders

use ShoppingfeedAddon\OrderImport\RuleAbstract;
use ShoppingfeedAddon\OrderImport\RuleInterface;
use ShoppingFeed\Sdk\Api\Order\OrderResource;

use ShoppingfeedClasslib\Extensions\ProcessLogger\ProcessLoggerHandler;

class MyOwnRules extends RuleAbstract implements RuleInterface
{

    public function isApplicable(OrderResource $apiOrder)
    {
        $apiOrderData = $apiOrder->toArray();
        // apiOrderData give you all order data coming from API
        // put here conditions to set this rules according to apiOrderData
        // for instance according to the marketplace, the carrier, ...

        return true;
    }

    /**
     * @inheritdoc
     */
    public function getConditions()
    {
        return $this->l('My description of condition 'MyOwnRules');
    }

    /**
     * @inheritdoc
     */
    public function getDescription()
    {
        return $this->l('My description of what to do 'MyOwnRules');
    }

}

After defining your own class you can add one or several event schedule to be called back during the import process :

  • onPreProcess
  • onCarrierRetrieval
  • onVerifyOrder
  • onCustomerCreation
  • onCustomerRetrieval
  • beforeBillingAddressCreation
  • beforeBillingAddressSave
  • beforeShippingAddressCreation
  • beforeShippingAddressSave
  • checkProductStock
  • onCartCreation
  • afterCartCreation
  • afterOrderCreation
  • onPostProcess

You can also add configuration form on your backoffice "Specific rules" by adding a method getConfigurationSubform.

    public function getConfigurationSubform()
    {
        return array(
            array(
                'type' => 'switch',
                'label' => $this->l('My label', 'MyOwnRules'),
                'name' => 'mybeautyfullname',
                'is_bool' => true,
                'values' => array(
                    array(
                        'value' => 1,
                    ),
                    array(
                        'value' => 0,
                    )
                ),
            )
        );
    }

    public function getDefaultConfiguration()
    {
        return array('mybeautyfullname' => true);
    }

    // sample usage
    public function onPreProcess($params)
    {
        if (empty($this->configuration['mybeautyfullname'])) {
            return false;
        }
        // rest of the process
    }