This is a revised article originally written in September 2014 when I started to play with Magento 2 for the first time. Since there were a lot of changes in Magento 2, this article also needed some refreshment. We will glance over the backend, and proceed with the development of a simple module. For this task, I picked up the payment gateway API integration. This relatively simple task will help us demonstrate some key changes in Magento 2.

More precisely, we will be focusing on implementation of Stripe payment gateway. Even though Stripe has a rich set of features, here we will be focusing only on most basic functionalities to get you started with Magento 2 extension development.

NOTEMagento 2 source code is under constant changes. Although code was tested on early Magento 2.1., it is now outdated. Stay tuned for an update.

Module setup

As you probably already know, a lot has changed in Magento 2, and Module setup and code structure is not an exception. One of the first things you will notice is absence of code pools that we are used to from previous version. Inside the app/code folder we will create our namespace folder. In the end, our file/folder structure would look like this:

Before we proceed, there is one more thing that needs to be taken care of. Stripe comes with its own set of PHP libraries for integration, and they need to be included as well. This, however, should be managed by Composer. If you take a look at composer.json, you will notice require line. By installing this sample extension through composer, Stripe library will be placed under vendor/stripe folder, and will be available through autoloader in our code.

Next big change has been introduced in XML configuration files. Magento 2 has introduced XML scheme for each configuration type that needs to be followed, or otherwise the module will not work. First one that we will create, is module.xml that replaces configuration which was previously placed under app/etc/modules/namespace_modulename.xml. As in magento 1, it is used to declare module and its dependencies:

<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../lib/internal/Magento/Framework/Module/etc/module.xsd">
    <module name="Guru_Stripe" setup_version="1.0.0">
        <sequence>
            <module name="Magento_Sales" />
            <module name="Magento_Payment" />
            <module name="Magento_Directory" />
            <module name="Magento_Config" />
        </sequence>
    </module>
</config>

There are also some changes in other config files, but I’ll let you have fun exploring them yourselves.

Payment implementation

So far, we have created our module file structure, we have created module configuration files and we have integrated our library files. It is now time to proceed with the payment integration.
Anyone who integrated payment gateway in Magento knows the importance of implementing proper admin settings, since the system will handle most of the things automatically. Lets take a look at our etc/adminhtml/system.xml file:

<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../Magento/Config/etc/system_file.xsd">
    <system>
        <section id="payment">
            <group id="guru_stripe" translate="label" type="text" sortOrder="50" showInDefault="1" showInWebsite="1" showInStore="1">
                <label>Stripe</label>
                <comment>
                    <![CDATA[<a href="https://stripe.com/" target="_blank">Click here to sign up for Stripe account</a>]]>
                </comment>
                <field id="active" translate="label" type="select" sortOrder="1" showInDefault="1" showInWebsite="1" showInStore="0">
                    <label>Enabled</label>
                    <source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
                </field>
                <field id="title" translate="label" type="text" sortOrder="2" showInDefault="1" showInWebsite="1" showInStore="1">
                    <label>Title</label>
                </field>
                <field id="api_key" translate="label" type="obscure" sortOrder="3" showInDefault="1" showInWebsite="1" showInStore="0">
                    <label>Api Key</label>
                    <backend_model>Magento\Config\Model\Config\Backend\Encrypted</backend_model>
                </field>
                <field id="debug" translate="label" type="select" sortOrder="4" showInDefault="1" showInWebsite="1" showInStore="0">
                    <label>Debug</label>
                    <source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
                </field>
                <field id="cctypes" translate="label" type="multiselect" sortOrder="5" showInDefault="1" showInWebsite="1" showInStore="0">
                    <label>Credit Card Types</label>
                    <source_model>Guru\Stripe\Model\Source\Cctype</source_model>
                </field>
                <field id="sort_order" translate="label" type="text" sortOrder="100" showInDefault="1" showInWebsite="1" showInStore="0">
                    <label>Sort Order</label>
                </field>
                <field id="allowspecific" translate="label" type="allowspecific" sortOrder="50" showInDefault="1" showInWebsite="1" showInStore="0">
                    <label>Payment from Applicable Countries</label>
                    <source_model>Magento\Payment\Model\Config\Source\Allspecificcountries</source_model>
                </field>
                <field id="specificcountry" translate="label" type="multiselect" sortOrder="51" showInDefault="1" showInWebsite="1" showInStore="0">
                    <label>Payment from Specific Countries</label>
                    <source_model>Magento\Directory\Model\Config\Source\Country</source_model>
                </field>
                <field id="min_order_total" translate="label" type="text" sortOrder="98" showInDefault="1" showInWebsite="1" showInStore="0">
                    <label>Minimum Order Total</label>
                </field>
                <field id="max_order_total" translate="label" type="text" sortOrder="99" showInDefault="1" showInWebsite="1" showInStore="0">
                    <label>Maximum Order Total</label>
                    <comment>Leave empty to disable limit</comment>
                </field>
            </group>
        </section>
    </system>
</config>

There are only three fields that we need to handle through our code: api_keymin_order_total and max_order_total. As I previously said, Magento will handle the rest through abstract classes by default.

Speaking of classes, it is finally time to implement our Payment class. Due to the nature of Stripe, we will be extending \Magento\Payment\Model\Method\Cc. Besides setting usual config through protected variables, we also have to pass Stripe library to our class to respect dependency injection and testability. Therefore we will begin our class with the following snippet:

namespace Guru\Stripe\Model;
 
class Payment extends \Magento\Payment\Model\Method\Cc
{
    const CODE = 'guru_stripe';
 
    protected $_code = self::CODE;
 
    protected $_isGateway                   = true;
    protected $_canCapture                  = true;
    protected $_canCapturePartial           = true;
    protected $_canRefund                   = true;
    protected $_canRefundInvoicePartial     = true;
 
    protected $_stripeApi = false;
 
    protected $_countryFactory;
 
    protected $_minAmount = null;
    protected $_maxAmount = null;
    protected $_supportedCurrencyCodes = array('USD');
 
    protected $_debugReplacePrivateDataKeys = ['number', 'exp_month', 'exp_year', 'cvc'];
 
    public function __construct(
        \Magento\Framework\Model\Context $context,
        \Magento\Framework\Registry $registry,
        \Magento\Framework\Api\ExtensionAttributesFactory $extensionFactory,
        \Magento\Framework\Api\AttributeValueFactory $customAttributeFactory,
        \Magento\Payment\Helper\Data $paymentData,
        \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig,
        \Magento\Payment\Model\Method\Logger $logger,
        \Magento\Framework\Module\ModuleListInterface $moduleList,
        \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate,
        \Magento\Directory\Model\CountryFactory $countryFactory,
        \Stripe\Stripe $stripe,
        array $data = array()
    ) {
        parent::__construct(
            $context,
            $registry,
            $extensionFactory,
            $customAttributeFactory,
            $paymentData,
            $scopeConfig,
            $logger,
            $moduleList,
            $localeDate,
            null,
            null,
            $data
        );
 
        $this->_countryFactory = $countryFactory;
 
        $this->_stripeApi = $stripe;
        $this->_stripeApi->setApiKey(
            $this->getConfigData('api_key')
        );
 
        $this->_minAmount = $this->getConfigData('min_order_total');
        $this->_maxAmount = $this->getConfigData('max_order_total');
    }

At this point, we have covered almost all differences in module development between Magento 2 and 1. From this point on, most of the code would be the same between those two versions, as far as our Stripe integration is concerned.

Lets proceed with implementation of our most important function, capture():

/**
 * Payment capturing
 *
 * @param \Magento\Payment\Model\InfoInterface $payment
 * @param float $amount
 * @return $this
 * @throws \Magento\Framework\Validator\Exception
 */
public function capture(\Magento\Payment\Model\InfoInterface $payment, $amount)
{
  //throw new \Magento\Framework\Validator\Exception(__('Inside Stripe, throwing donuts :]'));
 
  /** @var \Magento\Sales\Model\Order $order */
  $order = $payment->getOrder();
 
  /** @var \Magento\Sales\Model\Order\Address $billing */
  $billing = $order->getBillingAddress();
 
  try {
      $requestData = [
          'amount'        => $amount * 100,
          'currency'      => strtolower($order->getBaseCurrencyCode()),
          'description'   => sprintf('#%s, %s', $order->getIncrementId(), $order->getCustomerEmail()),
          'card'          => [
              'number'            => $payment->getCcNumber(),
              'exp_month'         => sprintf('%02d',$payment->getCcExpMonth()),
              'exp_year'          => $payment->getCcExpYear(),
              'cvc'               => $payment->getCcCid(),
              'name'              => $billing->getName(),
              'address_line1'     => $billing->getStreetLine(1),
              'address_line2'     => $billing->getStreetLine(2),
              'address_city'      => $billing->getCity(),
              'address_zip'       => $billing->getPostcode(),
              'address_state'     => $billing->getRegion(),
              'address_country'   => $billing->getCountryId(),
              // To get full localized country name, use this instead:
              // 'address_country'   => $this->_countryFactory->create()->loadByCode($billing->getCountryId())->getName(),
          ]
      ];
 
      $charge = \Stripe\Charge::create($requestData);
      $payment
          ->setTransactionId($charge->id)
          ->setIsTransactionClosed(0);
 
  } catch (\Exception $e) {
      $this->debugData(['request' => $requestData, 'exception' => $e->getMessage()]);
      $this->_logger->error(__('Payment capturing error.'));
      throw new \Magento\Framework\Validator\Exception(__('Payment capturing error.'));
  }
 
  return $this;
}

As usual, we will fetch billing information through payment object. Credit card info is than passed to Stripes API which handles the rest. In case of success, we will add this transaction to Magento’s list of transactions, and we are basically done here. It is important to note here, that transaction id should be set to transaction ID received by the payment gateway, since this will be used later on.

Another important feature for payment method is ability to issue a refund from Magento admin. So lets proceed and implement our refund() function:

/**
 * Payment refund
 *
 * @param \Magento\Payment\Model\InfoInterface $payment
 * @param float $amount
 * @return $this
 * @throws \Magento\Framework\Validator\Exception
 */
public function refund(\Magento\Payment\Model\InfoInterface $payment, $amount)
{
  $transactionId = $payment->getParentTransactionId();
 
  try {
      \Stripe\Charge::retrieve($transactionId)->refund();
  } catch (\Exception $e) {
      $this->debugData(['transaction_id' => $transactionId, 'exception' => $e->getMessage()]);
      $this->_logger->error(__('Payment refunding error.'));
      throw new \Magento\Framework\Validator\Exception(__('Payment refunding error.'));
  }
 
  $payment
      ->setTransactionId($transactionId . '-' . \Magento\Sales\Model\Order\Payment\Transaction::TYPE_REFUND)
      ->setParentTransactionId($transactionId)
      ->setIsTransactionClosed(1)
      ->setShouldCloseParentTransaction(1);
 
  return $this;
}

Basically, we are fetching transaction ID, which is then passed to API which handles refund communications. All we need to do here is to properly handle errors and mark transactions. And yes, by handling errors I do mean throwing Exception from inside catch block, in order to notify Magento of an error. Reason for try..catch block in the first place was to sanitize data, since response from server might have sensitive information. This also applies to capture functionality.

In Magento 2, checkout has been rewritten as JS client side application, that is communicating with core system through API. Considering that, PHP part itself is not enough for integration to work. We will proceed by adding two more JS files through layout update XML(check link, it is to large to be listed here). Following files have been added:

  • view/frontend/web/js/view/payment/stripe-payments.js
  • view/frontend/web/js/view/payment/method-renderer/stripe-method.js

Their purpose is to provide configuration for UI component used in checkout. For now, we will leave like that, explaining how this works, requires its own article.

And that is basically it. There may be a few more things to tune up, but our module is ready. Full code can be retrieved at our GIT repository here. An don’t forget to keep an eye on our repository. We plan to implement integration with tokens to avoid sending credit card info to server.

So far we have covered most of the basic things that are required for module development. However, our module is really simple: it lacks any template files, controllers, blocks, composer config, etc. But we will cover all that in our future articles. This is just a warmup to get you started. I hope it was useful, and that you enjoyed reading it.

In case you feel you need some extra help regarding Magento 2, we can offer you a detailed custom report based on our technical audit – feel free to get in touch and see what we can do for you!