Nginx setting for A/B Testing container

Nginx setting for A/B Testing container

The solution is based on browser cookie “ab_test”.

If user first time to access the website, we will randomly dispatch to the backend server, then we set cookie value with the server type(main/test), then for the next request, we will check the cookie value, and dispath it to the previous server.

  • 1. define constants
map $host $MAIN_SERVER { default 127.0.0.2; }
map $host $TEST_SERVER { default 127.0.0.3; }
  • 2. retrive some variables such as client cookie
map $http_user_agent $mobile_agent{
    default 0;
    ~*(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge\s|maemo|midp|mmp|netfront|opera\sm(ob|in)i|palm(\sos)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows\s(ce|phone)|xda|xiino 1;
}

split_clients "app${remote_addr}${http_user_agent}${date_gmt}" $ab_test_token {
        100% "main";
        * "test";
}

map $cookie_ABTest $ABTEST_COOKIE_VALUE {
        default    $ab_test_token;
        "test"     "test";
        "main" "main";
}

map "${ABTEST_COOKIE_VALUE}" $server_host {
        default $MAIN_SERVER;
        "main"  $MAIN_SERVER;
        "test"  $TEST_SERVER;
}

map $https $req_port {
        default 80;
        "on"  443;
}

map $ABTEST_COOKIE_VALUE $cookie_expires {
        default 1;
}

  • 3. define proxy gateway
server {
        server_name youservername.here;

        listen 80;
        listen 443 ssl;

        #ssl setting begin
        ssl_certificate     /path/to/cert/file.crt;
        ssl_certificate_key /path/to/cert/file.key;
        ssl_protocols        TLSv1.2;
        ssl_ciphers         HIGH:!aNULL:!MD5;
        #ssl setting end;

        #get real ip begin
        set_real_ip_from  172.31.0.0/16;
        set_real_ip_from  127.0.0.1/32;
        real_ip_header    X-Forwarded-For;
        real_ip_recursive on;

        set $real_x_forward_for $HTTP_X_FORWARDED_FOR;
        if ($real_x_forward_for = "" ) {
                set $real_x_forward_for $proxy_add_x_forwarded_for;
        }
        #get real ip end

        # get domain name without "www" begin
        set $domain $host;
        if ($domain ~ "www\.(.*)" ) {
                set $domain $1;
        }
        # get domain name end

        location / {
                proxy_redirect off;
                proxy_cache    off;
                proxy_set_header Host $host;
                proxy_set_header X-Forwarded-For $real_x_forward_for;
                proxy_set_header X-Forwarded-Ssl $https;
                add_header       Front-End-Https $https;
                add_header       Set-Cookie "ABTest=${ABTEST_COOKIE_VALUE};Path=/;Max-Age=${cookie_expires};Domain=.${domain};";
                proxy_pass $scheme://$server_host:$server_port;
        }
}
  • 4. set main server
server {
        listen 127.0.0.2:80;
        listen 127.0.0.2:443 ssl;

        #ssl setting begin
        ssl_certificate     /path/to/cert/file.crt;
        ssl_certificate_key /path/to/cert/file.key;
        ssl_protocols        TLSv1.2;
        ssl_ciphers         HIGH:!aNULL:!MD5;
        #ssl setting end;

        set_real_ip_from  127.0.0.1/32;
        set_real_ip_from  172.31.0.0/16;
        real_ip_header    X-Forwarded-For;
        real_ip_recursive on;

        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

        location / {
            #limit_req   zone=one  burst=1 nodelay;
        }
}
  • 5. set test server
server {
        listen 127.0.0.3:80;
        listen 127.0.0.3:443 ssl;

        #ssl setting begin
        ssl_certificate     /path/to/cert/file.crt;
        ssl_certificate_key /path/to/cert/file.key;
        ssl_protocols        TLSv1.2;
        ssl_ciphers         HIGH:!aNULL:!MD5;
        #ssl setting end;

        set_real_ip_from  127.0.0.1/32;
        set_real_ip_from  172.31.0.0/16;
        real_ip_header    X-Forwarded-For;
        real_ip_recursive on;

        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

        location / {
        limit_req   zone=one  burst=1 nodelay;
        }
}

cron job runner

A wrapper for cron job log

When we setup a cron job, we redirect log to a /tmp/path file. But sometimes the /tmp/path will be delete by system.

So I wrote a very simple script to make sure the directory will be created, and the file will be sync to s3 automaticlly.

  • 1. script
#!/bin/bash
CMD=$1
IFS='>' read -ra PARTS <<< "$CMD"    #Convert string to array

#Print all names from array
LOGPART=''
for i in "${PARTS[@]}"; do
    LOGPART=$i
done

if [ $LOGPART != '/dev/null' ]; then
        if [ ! -f $LOGPART ]; then
                mkdir -p $LOGPART
                rm -rf $LOGPART
        fi
fi
eval $CMD

LOGPART=`echo "${LOGPART}" | sed -e 's/^[ \t]*//'`
S3Backup="aws s3 cp ${LOGPART} s3://urlpath/log${LOGPART} --profile logiam > /dev/null"
eval $S3Backup
  • 2. use it
* * * * * cronrunner.sh "php yourfile.php params >> /tmp/log/log1/logs.txt"

Magento2 MageStore Onestepcheckout bug when using braintree

Magento2 MageStore Onestepcheckout bug when using braintree

We purchased Magestore OneStepCheckout module for magento2 community edition. Recently we setuped braintree payment gateway.

But soon we some time when we go to checkout page and input correct credit card details and place order, and payment can not be processed, it returns invalid card numbers.

It defenerly is not the credit card’s problem.

So we just use chrome debuger to check the details, we found there are some js error log in the console.

“Can not replace element of id #credit-card-number” “Can not replace element of id #expiration-month” “Can not replace element of id #expiration-year”

At first we thought that’s magento braintree module’s bug, but when we debug into the UI component(I don’t like UI component), and it seems some time this component is rendered 3 or 4 times.

Finally we found the problem is because onestepcheckout assemble address, shipping, cart detals options in one page, and all these parts are UI component. Once these part got databinding, and it will update payment infomation which will re-calculate avalible payment method lists and set back to knockoutjs payment-service(which is an observerarray).

But for this version knockoutjs doesn’t handle multiple process access same observer array.

All these payment method UI component renderer are called by observerarray subscriber, so it will render many times.

If some time we are not lucky, last renderer not finish yet, and payment methods list are updated, it will render again. Then the error shows.

  • 1. Define the new javascript that you overwrite from core module

We just override the default payment-service, put all payment list to the queue first, and set a 2secs timer, if in w2secs there’s no methodlist come in, we render the last one.

Put the Js file to your module view/frontend/web/js/payment-service.js

/**
 * Copyright © 2016 Magento. All rights reserved.
 * See COPYING.txt for license details.
 */
define(
    [
        'underscore',
        'Magento_Checkout/js/model/quote',
        'Magento_Checkout/js/model/payment/method-list',
        'Magento_Checkout/js/action/select-payment-method'
    ],
    function (_, quote, methodList, selectPaymentMethod) {
        'use strict';
        var freeMethodCode = 'free';

        return {
            isFreeAvailable: false,
            methods_queue:[],
            timer:null,
            /**
             * Populate the list of payment methods
             * @param {Array} methods
             */
            setPaymentMethods: function (methods) {
                var self = this,
                    freeMethod,
                    filteredMethods,
                    methodIsAvailable;

                freeMethod = _.find(methods, function (method) {
                    return method.method === freeMethodCode;
                });
                this.isFreeAvailable = freeMethod ? true : false;

                if (self.isFreeAvailable && freeMethod && quote.totals().grand_total <= 0) {
                    methods.splice(0, methods.length, freeMethod);
                    selectPaymentMethod(freeMethod);
                }
                filteredMethods = _.without(methods, freeMethod);

                if (filteredMethods.length === 1) {
                    selectPaymentMethod(filteredMethods[0]);
                } else if (quote.paymentMethod()) {
                    methodIsAvailable = methods.some(function (item) {
                        return item.method === quote.paymentMethod().method;
                    });
                    //Unset selected payment method if not available
                    if (!methodIsAvailable) {
                        selectPaymentMethod(null);
                    }
                }
                self.methods_queue.push(methods);
                if (self.timer != null) {
                        clearTimeout(self.timer);
                }
                self.timer = setTimeout(function() {
                        console.log('setpaymentmethod time out trigered');
                        methodList(self.methods_queue.pop());
                        self.methods_queue = [];
                }, 2000);
                //methodList(methods);
            },
            /**
             * Get the list of available payment methods.
             * @returns {Array}
             */
            getAvailablePaymentMethods: function () {
                var methods = [],
                    self = this;
                _.each(methodList(), function (method) {
                    if (self.isFreeAvailable && (
                        quote.totals().grand_total <= 0 && method.method === freeMethodCode ||
                        quote.totals().grand_total > 0 && method.method !== freeMethodCode
                        ) || !self.isFreeAvailable
                    ) {
                        methods.push(method);
                    }
                });

                return methods;
            }
        };
    }
);

  • 2. Define it in your requirejs-config.js
var config = {
    map: {
        '*': {
            "Magento_Checkout/js/model/payment-service" : 'Your_ModuleName/js/payment-service'
        }
    },
    "shim": {
        "Your_ModuleName/js/payment-service": {
                "exports": "Magento_Checkout/js/model/payment-service"
            }
        }
};

Then enjoy it!

Magento2 Fulltext Indexer using swap table

Magento2 Fulltext Indexer using swap table

Indexing in Magento2 is much faster than Magento1, but for fulltext indexing, still have few seconds user can search nothing.

I’ve developed the fulltext indexer plugin using swap table. Just index fulltext to a temporary table first, after indexing finished, swap the temporary table to the working table.

1. Define the plugin class

<?php
namespace Your\ModuleName\Plugin\Indexer\CatalogSearch;

use Magento\Framework\Search\Request\DimensionFactory;
use Magento\Framework\Indexer\ScopeResolver\IndexScopeResolver;
use Magento\Framework\App\ResourceConnection;

class IndexerHandler {
    private $dimensionFactory;
    private $indexScopeResolver;
    private $resource;

    private $isFullReindex = false;
    private $swapDimensions;

    private $indexName = \Magento\CatalogSearch\Model\Indexer\Fulltext::INDEXER_ID;

    private $getIndexNameClosure;

    public function __construct(
        DimensionFactory $dimensionFactory,
        IndexScopeResolver $indexScopeResolver,
        ResourceConnection $resource
    ) {
        $this->dimensionFactory = $dimensionFactory;
        $this->indexScopeResolver = $indexScopeResolver;
        $this->resource = $resource;
    }

    /**
     * {@inheritdoc}
     */
    public function aroundCleanIndex($origin, $processor, $dimensions)
    {
        $this->isFullReindex = true;
        $this->swapDimensions = [];
        foreach($dimensions as $di) {
            $this->swapDimensions[] = $this->dimensionFactory->create(['name' => 'swap', 'value' => $di->getValue()]);
        }
   
        $processor($this->swapDimensions);
    }

    public function aroundSaveIndex($origin,$processor, $dimensions, $documents)
    {
        $processor($this->isFullReindex ? $this->swapDimensions : $dimensions, $documents);
        if ($this->isFullReindex) {
            $swapTableName = $this->getTableName($this->swapDimensions);
            $originTableName = $this->getTableName($dimensions);

            $tmpTable = $originTableName . '_del';
            $this->resource->getConnection()
                ->query(sprintf('DROP TABLE IF EXISTS %s;', $tmpTable));

            $this->resource->getConnection()
                ->query(sprintf('CREATE TABLE IF NOT EXISTS %s(id int);', $originTableName));

            $this->resource->getConnection()
                ->query(sprintf('RENAME TABLE %s TO %s, %s To %s;', 
                        $originTableName, 
                        $tmpTable, 
                        $swapTableName, 
                        $originTableName));
        }
    }

    private function getTableName($dimensions) {
        return $this->indexScopeResolver->resolve($this->indexName, $dimensions);
    }
}

Add it to plugin settings “etc/di.xml”

    <type name="Magento\CatalogSearch\Model\Indexer\IndexerHandler">
        <plugin name="fulltext_table_swap"
                type="Your\ModuleName\Plugin\Indexer\CatalogSearch\IndexerHandler"
                sortOrder="1" />
    </type>

All done!

Laravel ElasticSuit

Laravel ElasticSuit

Total Downloads Latest Stable Version Latest Unstable Version License

This is a package to integrate Elasticsearch to Laravel5

It makes you do Elasticsearch just using Eloquent’s API.

Installation

  1. Require this package with composer:
composer require yong/elasticsuit dev-master
  1. Add service provider to config/app.php
Yong\ElasticSuit\Service\Provider;
  1. Add elasticsearch node configuration to the “connections” node of config/database.php
        'elasticsearch' => [
            'hosts'=>['127.0.0.1:9200'],
            'ismultihandle'=>0,
            'database'=> 'db*',
            'prefix' => '',
            'settings'=> ['number_of_shards'=>2,'number_of_replicas'=>0]
        ],

Usage

  1. Define a model for a elasticsearch type

class TestModel extends \Yong\ElasticSuit\Elasticsearch\Model {
    protected $connection = 'elasticsearch';
    protected $table = 'testmodel';

    //relations
    public function Childmodel () {
        return $this->hasOne(OtherModel::class, '_id');
    }
}
  1. Create a new document

$testmodel = new TestModel();
$testmodel->first_name = 'firstname';
$testmodel->last_name = 'lastname';
$testmodel->age = 20;
$testmodel->save();
  1. Search a collection
$collection = TestModel::where('first_name', 'like', 'firstname')
    ->whereIn('_id', [1,2,3,4,5])
    ->whereNotIn('_id', [5,6,7,8,9])
    ->where('_id', '=', 1)
    ->where('age', '>', 18)
    ->orWhere('last_name', 'like', 'lastname')
    ->whereNull('nick_name')
    ->whereNotNull('age')
    ->whereMultMatch(['last_name', 'description'], 'search words', '60%')
    ->skip(10)
    ->forPage(1, 20)
    ->take(10)
    ->limit(10)
    ->select(['first_name', 'last_name', 'age'])
    ->get();

* also support sum(), avg(), min(), max(), stats(), count()
* but not for all fields, only numeric fields can use aggregate

  1. Relations It also support relations, but remember so far just support using default _id as primary key.
    //get relations
    TestModel::with('childmodel')->where('first_name', 'like', 'firstname')->get();

License

And of course:

MIT: http://rem.mit-license.org

Composer use private git packages

Composer use private git packages

  • Why doing this?

Some time your will not want your company’s code goes to public project, but you want composer to manage packages.

Here’s an example for how to let composer use private packages, and put these packages to customize folder.

  • 1. Create mycomposer.php to do these automaticly

Here we use laravel as an example.

<?php

$now = strtotime('now');
$tmp_work_folder = sprintf('/tmp/mycomposer_%s', $now);
$current_folder = __DIR__;
$composer_json = $current_folder . '/composer.json';
$mycomposer_json = $current_folder . '/mycomposer.json';

if(!file_exists($composer_json)) {
    rename($current_folder, $tmp_work_folder);
    exec('composer create-project --prefer-dist laravel/laravel ' . $current_folder);
    exec(sprintf('cp -a %s/* %s', $tmp_work_folder, $current_folder));
}

if (file_exists($mycomposer_json)) {
    $composer_configs = json_decode(file_get_contents($composer_json), true);
    $mycomposer_configs = json_decode(file_get_contents($mycomposer_json), true);

    $requires = [];
    foreach( $mycomposer_configs['repositories'] as $package_config) {
        $requires[$package_config['package']['name']] = '*';
    }
    $mycomposer_configs['require'] = $requires;

    $new_composer_configs = array_replace_recursive($composer_configs, $mycomposer_configs);
    $new_composer_configs['repositories'] = $mycomposer_configs['repositories'];
    file_put_contents($composer_json, json_encode($new_composer_configs, JSON_PRETTY_PRINT));

    chdir($current_folder);
    exec("cd $current_folder && composer update");
}
  • 2. Create a JSON file name it as mycomposer.json
{
        "repositories": [
            {
                "type": "package",
                "package": {
                    "name": "../your/path/Module1",
                    "version":"0.0.0",
                    "source": {
                        "url": "https://url_to_your_git_repostory1.git",
                        "type": "git",
                        "reference": "master"
                    }
                }
            },
            {
                "type": "package",
                "package": {
                    "name": "../your/path/Module2",
                    "version":"0.0.0",
                    "source": {
                        "url": "https://url_to_your_git_repostory2.git",
                        "type": "git",
                        "reference": "master"
                    }
                }
            }
        ],

        "autoload": {
             "psr-4": {
                    "YourNameSpace\\": "your/path"
                }
            }
        },

        "require": {
        },

        "require-dev": {
        }
}
  • 3. put mycomposer.php and mycomposer.json to your new project path and run the command:
php mycomposer.php

it’s done!

Magento2 Overwrite Core module's javascript

Magento2 How to overwrite Core module’s javascript

We purchased Magestore OneStepCheckout module for magento2 community edition. But for this module when we go to checkout page, update qty/remove item or just update shipping details, it will update payment methods.

If user have input payment details, then they want to update itme/shipping detials, because it will update payment methods as well, and all these input details will gone.

That’s why I need to overwrite core module’s javascript.

  • 1. Define the new javascript that you overwrite from core module

Put the Js file to your module view/frontend/web/js/payment-service.js

/**
 * Copyright © 2016 Magento. All rights reserved.
 * See COPYING.txt for license details.
 */
define(
    [
        'underscore',
        'Magento_Checkout/js/model/quote',
        'Magento_Checkout/js/model/payment/method-list',
        'Magento_Checkout/js/action/select-payment-method'
    ],
    function (_, quote, methodList, selectPaymentMethod) {
        'use strict';
        var freeMethodCode = 'free';

        return {
            isFreeAvailable: false,
            /**
             * Populate the list of payment methods
             * @param {Array} methods
             */
            setPaymentMethods: function (methods) {
                var self = this,
                    freeMethod,
                    filteredMethods,
                    methodIsAvailable;

                freeMethod = _.find(methods, function (method) {
                    return method.method === freeMethodCode;
                });
                this.isFreeAvailable = freeMethod ? true : false;

                if (self.isFreeAvailable && freeMethod && quote.totals().grand_total <= 0) {
                    methods.splice(0, methods.length, freeMethod);
                    selectPaymentMethod(freeMethod);
                }
                filteredMethods = _.without(methods, freeMethod);

                if (filteredMethods.length === 1) {
                    selectPaymentMethod(filteredMethods[0]);
                } else if (quote.paymentMethod()) {
                    methodIsAvailable = methods.some(function (item) {
                        return item.method === quote.paymentMethod().method;
                    });
                    //Unset selected payment method if not available
                    if (!methodIsAvailable) {
                        selectPaymentMethod(null);
                    } else {
                        selectPaymentMethod(quote.paymentMethod());
                    }
                }

                var oldMethods = methodList();
                if (oldMethods.length === methods.length) {
                    for(var i=0; i < oldMethods.length; i++) {
                        if (oldMethods[i].title != methods[i].title) {
                            methodList(methods);
                            break;
                        }
                    }
                } else {
                    methodList(methods);
                }
                //methodList(methods);
            },
            /**
             * Get the list of available payment methods.
             * @returns {Array}
             */
            getAvailablePaymentMethods: function () {
                var methods = [],
                    self = this;
                _.each(methodList(), function (method) {
                    if (self.isFreeAvailable && (
                        quote.totals().grand_total <= 0 && method.method === freeMethodCode ||
                        quote.totals().grand_total > 0 && method.method !== freeMethodCode
                        ) || !self.isFreeAvailable
                    ) {
                        methods.push(method);
                    }
                });

                return methods;
            }
        };
    }
);
  • 2. Define it in your requirejs-config.js
var config = {
    map: {
        '*': {
            "Magento_Checkout/js/model/payment-service" : 'Your_ModuleName/js/payment-service'
        }
    },
    "shim": {
        "Your_ModuleName/js/payment-service": {
                "exports": "Magento_Checkout/js/model/payment-service"
            }
        }
};

Then enjoy it!

Magento2 knockoutjs work with angularjs

Magento2 Let angularjs work with knockout.js

  • Why doing this?

Before Mangento2 we already using angularjs and also developed many angularjs resources(expecially modals). So we just want to reuse these resources.

Here’s an example for Mangento2 admin panel. (For frontend, I recommend you just only use knockout.js)

  • 1. Create a Block class Container Tab

This container tab is for containing angularjs template.

You can contain many templates you want by passing “child_templates” parameters via layout.xml

<?php

namespace Yong\Angularjs\Block\Adminhtml;

use Magento\Backend\Block\Widget\Tab;
use Magento\Backend\Block\Widget\Tabs;
use Magento\Backend\Block\Widget;

class ContainerTab extends Tab {
    /**
     * Prepare html output
     *
     * @return string
     */
    protected function _toHtml() {
        if ($this->hasData('child_templates')) {
            foreach($this->getData('child_templates') as $name => $childTemplate) {
                $child = $this->addChild($name,
                    'Magento\Backend\Block\Widget', 
                    ['template'=>$childTemplate]
                );
            }
        }
        
        return $this->getChildHtml();
    }
}
  • 2. Create a UI Modifier to prepare knockoutjs swap data fields
<?php
namespace Yong\Angularjs\Ui\DataProvider\Product\Form;

use Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\AbstractModifier;
use Magento\Catalog\Api\Data\ProductAttributeInterface;
use Magento\Framework\Stdlib\ArrayManager;
use Magento\Catalog\Model\Locator\LocatorInterface;

use Magento\Framework\UrlInterface;
use Magento\Ui\Component\Container;
use Magento\Ui\Component\Form\Fieldset;
use Magento\Ui\Component\Form;
use Magento\Ui\Component\DynamicRows;

/**
 * Customize Price field
 */
class Modifier extends AbstractModifier
{
    const FIELD_NAME = 'angularjs_swap_data';
    const FIELDSET_NAME = 'angularjs_swap_sets';

    /**
     * @var ArrayManager
     */
    protected $arrayManager;

    /**
     * @var LocatorInterface
     */
    protected $locator;

    private $swapFieldNames;

    /**
     * @param LocatorInterface $locator
     * @param ArrayManager $arrayManager
     */
    public function __construct(
        LocatorInterface $locator,
        ArrayManager $arrayManager,
        $swapFieldNames = [self::FIELD_NAME]
    ) {
        $this->locator = $locator;
        $this->arrayManager = $arrayManager;
        if (!is_array($swapFieldNames)) {
            $swapFieldNames = [$swapFieldNames];
        }
        $this->swapFieldNames = $swapFieldNames;
    }

    /**
     * {@inheritdoc}
     */
    public function modifyMeta(array $meta)
    {
        $this->meta = $meta;
        $this->addFieldset();

        return $this->meta;
    }

    public function modifyData(array $data) {
        return $data;   
    }

    protected function addFieldset()
    {
        $children = [];
        $i = 0;
        foreach($this->swapFieldNames as $swapFieldName) {
            $children[$swapFieldName] = $this->getFieldConfig($swapFieldName, 10*$i++);
        }
        $this->meta = array_replace_recursive(
            $this->meta,
            [
                static::FIELDSET_NAME => [
                    'arguments' => [
                        'data' => [
                            'config' => [
                                'label' =>'',
                                'componentType' => Fieldset::NAME,
                                'dataScope' => 'data',
                                'collapsible' => true,
                                "visible" => true,
                                'sortOrder' => 10,
                            ],
                        ],
                    ],
                    'children' => $children
                ],
            ]
        );

        return $this;
    }

    protected function getFieldConfig($swapFieldName, $sortOrder)
    {
        return [
            'arguments' => [
                'data' => [
                    'config' => [
                        'label' => '',
                        'componentType' => \Magento\Ui\Component\Form\Field::NAME,
                        'formElement' => \Magento\Ui\Component\Form\Element\Input::NAME,
                        'dataScope' => $swapFieldName,
                        'dataType' => \Magento\Ui\Component\Form\Element\DataType\Text::NAME,
                        'sortOrder' => $sortOrder,
                        "visible" => true,
                        'required' => true,
                    ],
                ],
            ],
            'children' => [],
        ];
    }
}

  • 3. Reserve knockoutjs swap fields in di.xml
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
    <type name="Yong\Angularjs\Ui\DataProvider\Product\Form\Modifier">
        <arguments>
            <argument name="locator" xsi:type="object">Magento\Catalog\Model\Locator\LocatorInterface</argument>
            <argument name="arrayManager" xsi:type="object">Magento\Framework\Stdlib\ArrayManager</argument>
            <argument name="swapFieldNames" xsi:type="string">angularjs_swap_data</argument>
        </arguments>
    </type>
    <virtualType name="angularjs_swap_dataset1"
        type="Yong\Angularjs\Ui\DataProvider\Product\Form\Modifier" >
        <arguments>
            <argument name="swapFieldNames" xsi:type="array">
                <item name="field1" xsi:type="string">angularjs_swap_data_1</item>
                <item name="field2" xsi:type="string">angularjs_swap_data_2</item>
            </argument>
        </arguments>
    </virtualType>
    
    <virtualType name="Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\Pool">
        <arguments>
            <argument name="modifiers" xsi:type="array">
                <item name="comboproduct" xsi:type="array">
                    <item name="class" xsi:type="string">angularjs_swap_dataset1</item>
                    <item name="sortOrder" xsi:type="number">125</item>
                </item>
            </argument>
        </arguments>
    </virtualType>
</config>
  • 4. Put your container tab to layout.xml
<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
    <update handle="catalog_product_form"/>
    <body> 
        <referenceContainer name="product_form" >
            <block class="Yong_Angularjs\Block\Adminhtml\ContainerTab">
                <arguments>
                    <argument name="config" xsi:type="array">
                        <item name="label" xsi:type="string" translate="true">Angularjs tab</item>
                        <item name="collapsible" xsi:type="boolean">true</item>
                        <item name="opened" xsi:type="boolean">true</item>
                        <item name="sortOrder" xsi:type="string">2</item>
                        <item name="canShow" xsi:type="boolean">true</item>
                        <item name="componentType" xsi:type="string">fieldset</item>
                    </argument>
                    <argument name="child_templates" xsi:type="array">
                        <item name="subblock1" xsi:type="string">Yong_Angularjs::catalog/product/edit/tab/angularjs.tab.phtml</item>
                    </argument>
                </arguments>
            </block>
        </referenceContainer>
    </body>
</page>

  • 5. Add js resource and config requirejs-config.js
var config = {
    map: {
        '*': {
            angular: 'Yong_Angularjs/bower/angular/angular.min',
            angularBootstrap: 'Yong_Angularjs/bower/angular-bootstrap/ui-bootstrap-tpls.min',
            ngstorage: 'Yong_Angularjs/bower/ngstorage/ngStorage.min',
            mymodalService: 'Yong_Angularjs/js/mymodalService',
            modalScript: 'Yong_Angularjs/js/modalScript',
            syncKo: 'Yong_Angularjs/js/syncKo',
            mytabController: 'Yong_Angularjs/mytabController'
        }
    },
    "shim": {
        "Yong_Angularjs/bower/angular/angular.min": {
                "exports": "angular"
            },
        "Yong_Angularjs/bower/angular-bootstrap/ui-bootstrap-tpls.min": {
                "exports": "angularBootstrap"
            },
        "Yong_Angularjs/bower/ngstorage/ngStorage.min": {
                "exports": "ngStorage"
            },
        "Yong_Angularjs/js/mymodalService": {
                "exports": "mymodalService"
            },
        "Yong_Angularjs/js/modalScript": {
                "exports": "modalScript"
            },
        "Yong_Angularjs/js/syncKo": {
                "exports": "syncKo"
            },
        "Yong_Angularjs/mytabController": {
                "exports": "mytabController"
            }
        }
};
  • 6. Define angularjs directive “syncko” to sync angularjs object to knockoutjs swap data field(formart as JSON)
define(['jquery'], function($){
    'use strict';
    function syncKo() {
        return function($scope, element, attrs) {
            $scope.$watch(attrs.from, function(newValue, oldValue) {
                if (newValue !== undefined) {
                    $('input[name='+ attrs.to +']').val(JSON.stringify(newValue)).trigger('change');
                }
            }, true);
        };
    }
    return syncKo;
});
  • 7. Use knockoutjs swap fields in angularjs template html <div class="container fieldset-wrapper" ng:controller="controller" id='ng-controller' style="margin-left:20px;margin-top: 0" sync-ko from='products' to='angularjs_swap_data_1'>

Development Tips

useful tips

  • grep get ip addr: shell grep -E -o "([0-9]{1,3}[\.]){3}[0-9]{1,3}"
  • unique grep result: shell grep -E -o "([0-9]{1,3}[\.]){3}[0-9]{1,3}" | cut -d ' ' -f 4 | sort -u
  • inotify:

install

 yum --enablerepo=epel -y install inotify-tools 

monitor.sh

#!/bin/bash
TARGET=$1
ACTION=$2
while inotifywait -e modify ${TARGET}; do
        /bin/bash ${ACTION} ${TARGET}
done

action.sh

#!/bin/bash
#/var/log/nginx/error.log

TARGET=$1
cat ${TARGET} | grep 'limiting requests' | grep -E -o "([0-9]{1,3}[\.]){3}[0-9]{1,3}" | cut -d ' ' -f 4 | sort -u

use it:

./monitor.sh /var/log/nginx/error.log ./action.sh

Magento2 Debugbar

Magento2 Debugbar

Total Downloads Latest Stable Version Latest Unstable Version License

This is a package to integrate PHP Debug Bar with Magento 2.

It lightly dynamic inject and collect debug info for Magento2 for development/production mode. You can configure enable it by specific IP address or specific cookie name and value matched, and you can extend it by your custom access control functions.

It bootstraps some Collectors to work with Magento2 and implements a couple custom DataCollectors, specific for Magento2. It is configured to display Redirects and (jQuery) Ajax Requests. (Shown in a dropdown) Read the documentation for more configuration options.

This package includes some custom collectors: - QueryCollector: Show queries, including binding + timing - ControllerCollector: Show information about the current/redirect Route Action. - TemplateCollector: Show the currently loaded template files. - ModelCollector: Show the loaded Models - ProfileCollector: Shows the Magento2 profiler details - RequestCollector: The default RequestCollector via PHPDebugbar - MemoryCollector: The default MemoryCollector via PHPDebugbar - MessagesCollector: The default MessagesCollector via PHPDebugbar

And it also replace Magento default exception handler as Whoops Error Handler.Read filp/whoops for more details.

It also provides config interface for easy dynamic extend your functionality.

Installation

Require this package with composer:

composer require yong/magento2debugbar dev-master

After updating composer, add ‘phpdebugbar’ configuration to app/etc/env.php

  'phpdebugbar' =>
  array (
    'enabled' => 1,
    'enable_checker' =>
    array (
      'cookie' =>
      array(
        'name' => 'php_debugbar',
        'value' => 'cookievalue'
      ),
    ),
  )

and then run

bin/magento module:enable Yong_Magento2DebugBar

Usage

Enable/Disable: go to file app/etc/env.php, set ‘enabled’ of array phpdebugbar as 0 for disable, 1 for enable(but still need cookie pair check)

enable_checker: When phpdebugbar is enabled, it will do further check, you need to set your cookie pair in app/etc/env.php, and also your browser has the same cookie pair. Then phpdebugbar will be launched. Otherwise it will not be launched and will collect nothing, it will not affect the performance. So you can deploy it on your production environment.

License

And of course:

MIT: http://rem.mit-license.org

Magento2 Let Category Name support last character +

Magento2 Let Category Name support last character +

The website I just developed which has some categories name the last character is “+”, and when we import product from CSV, it will automatically removed by Magento.

Normally it’s fine until we found issue below:

We have two categories:

1. CategoryName
2. CategoryName+

So we have to fix it.

Just simply using plugin to correct it and convert “+” as “-plus”.

Define the plugin class

<?php

namespace Your\ModuleName\Plugin\Framework\Filter;

class TranslitUrl
{
    public function aroundFilter($origin, $process, $string)
    {
        $p1 = substr($string, 0, -1);
        $p2 = substr($string, -1);

        if ($p2 === '+') {
            $p2 = '-plus';
        }

        if (strpos($string, 'AJ1800') > 0) {
             echo "$string => $p1 . $p2";
        }

        $string = $p1 . $p2;

        return $process($string);
    }
}

Add it to plugin settings “etc/di.xml”

    <type name="Magento\Framework\Filter\TranslitUrl">
        <plugin name="urltranslit_fix"
                type="Your\ModuleName\Plugin\Framework\Filter\TranslitUrl"
                sortOrder="1" />
    </type>

about

Yongcheng Tech host by AWS serverless