Gli observer, un caso di utilizzo concreto

Magento ha guadagnato una vasta popolarità nello sviluppo di piattaforme dedicate all’e-commerce grazie anche alla scelta implementativa di adottare vari design pattern molto apprezzati dagli sviluppatori tra i quali il pattern ‘Event Observer’. Nell’universo Magento il pattern ‘Event Observer’ fornisce un valido meccanismo per estendere Magento stesso, senza la necessità di sovrascrivere le classi del core. Questo offre agli sviluppatori la possibilità di iniettare della logica personalizzata o alterare il normale flusso di particolari funzioni del sistema.

In questo articolo verrà mostrato nel concreto l’uso degli observer, sviluppando un modulo di Magento con l’obiettivo di inviare una email all’e-commerce manager di uno store (o a qualsiasi altro indirizzo email) al cambio di stato di un ordine. Questo, per aggiornare quindi non solo sugli acquisti conclusi con successo (operazione gestita di default dal sistema) ma, per esempio, anche per quei tentativi non portati a termine o per gli ordini cancellati.

Durante la spiegazione della creazione del modulo, che chiameremo OrderStatusNotifier, verranno toccati vari argomenti delicati ed importanti di Magento come: la gestione delle ACL (Access Control List), la creazione di template per l’invio di email di sistema e relative traduzioni, lo sviluppo di source model ed altro, che non sono oggetto di discussione di questo articolo. I lettori sono invitati comunque ad un approfondimento sugli stessi.

Supponendo ‘WaPoNe’ come vendor del modulo, partiamo con la stesura dei file richiesti per registrare un’estensione Magento: app/etc/modules/WaPoNe_OrderStatusNotifier.xml

<?xml version="1.0" encoding="UTF-8"?>
<config>
 <modules>
 <WaPoNe_OrderStatusNotifier>
 <active>true</active>
 <codePool>local</codePool>
 </WaPoNe_OrderStatusNotifier>
 </modules>
</config>

e app/code/local/WaPoNe/OrderStatusNotifier/etc/config.xml

<config>
 <modules>
 <WaPoNe_OrderStatusNotifier>
 <version>1.0.0</version>
 </WaPoNe_OrderStatusNotifier>
 </modules>
</config>

A questo punto possiamo registrare un “model”, visto che ci serve un observer, oltre a configurare l’evento che farà partire l’observer stesso. Quindi andiamo ad aggiungere il sottostante codice nel tag <config> di app/code/local/WaPoNe/etc/config.xml

<global>
 <models>
 <WaPoNe_OrderStatusNotifier>
   <class>WaPoNe_OrderStatusNotifier_Model</class>
 </WaPoNe_OrderStatusNotifier>
 </models>
 <events>
 <sales_order_save_after>
        <observers>
        <mail_status_change>
        <type>singleton</type>
            <class>WaPoNe_OrderStatusNotifier/observer</class>
        <method>notify</method>
        </mail_status_change>
        </observers>
 </sales_order_save_after>
 </events>
 </global>

L’evento che si occuperà di far scattare il nostro observer sarà sales_order_save_after

Durante il salvataggio di un ordine, il sistema quindi lancerà la dispatch() in question, che verrà agganciata dal nostro observer. Questo effettuerà una chiamata al metodo notify() della classe WaPoNe_OrderStatusNotifier_Model_Observer; vediamo come implementarla.

All’interno della directory app/code/local/WaPoNe/OrderStatusNotifier aggiungiamo la cartella Model/ e al suo interno il file Observer.php con questo contenuto:

<?php
 
class WaPoNe_OrderStatusNotifier_Model_Observer
{
 
       public function notify($event)
       {
       // Check 'wapone_orederstatusnotifier_prevent_observer' registry variable to prevent to fire observer more times
       if(!Mage::registry('wapone_orederstatusnotifier_prevent_observer')) {
       $order = $event->getOrder();
 
       //Order Statuses to notify
       $statuses_to_notify = 
$this->_getStatuses('orderstatusnotifier/orderstatusnotifier_group/statuses');
 
       if (in_array($order->getStatus(), $statuses_to_notify)) {
               // Send mail
               $this->_sendEmail($order);
       }
 
       // Assign value to 'wapone_orederstatusnotifier_prevent_observer' registry variable
       Mage::register('wapone_orederstatusnotifier_prevent_observer', true);
       }
       }
 
      /* Retrieving order statuses selected */
      private function _getStatuses($param)
      {
      $statuses = Mage::getStoreConfig($param);
      $arr_result = array();
 
      if (!empty($statuses)):
      $arr_result = explode(",", $statuses);
      endif;
 
      return $arr_result;
      }
 
      /* Retrieving order status label */
      private function _getOrderStatusLabel($orderStatus)
      {
      $orderStatusLabel = "";
 
      $statuses = 
Mage::getModel('sales/order_status')->getResourceCollection()->getData();
      foreach ($statuses as $status)
      {
      if($status["status"] == $orderStatus)
               return $status["label"];
      }
      return $orderStatusLabel;
      }
 
      /* Sending email */
      private function _sendEmail($order)
      {
      $orderStatusLabel = $this->_getOrderStatusLabel($order->getStatus());
 
      $emailTemplate = Mage::getModel('core/email_template');
 
      // Get sender email address (System->Configuration->Order Status Notifier)
      $salesData['name'] = 
Mage::getStoreConfig('orderstatusnotifier/orderstatusnotifier_group/sender_name');
      $salesData['email'] = 
Mage::getStoreConfig('orderstatusnotifier/orderstatusnotifier_group/sender_email');
 
     // Get receiver email addresses (System->Configuration->Order Status Notifier)
     $receivers = explode(";",
Mage::getStoreConfig('orderstatusnotifier/orderstatusnotifier_group/receiver_emails'));
 
    // Loading email template
  $emailTemplate->loadDefault('wapone_order_status_notifier');
 
   // Email Subject is set in the email template
   // $emailTemplate->setTemplateSubject($email_subject);
 
   $emailTemplate->setSenderName($salesData['name']);
 $emailTemplate->setSenderEmail($salesData['email']);
 
   $emailTemplateVariables['order'] = $order;
   $emailTemplateVariables['store'] = Mage::app()->getStore();
   $emailTemplateVariables['order_status'] = $orderStatusLabel;
 $emailTemplateVariables['username'] = $order->getCustomerFirstname() . ' ' . 
$order->getCustomerLastname();
   $emailTemplateVariables['order_id'] = $order->getIncrementId();
   $emailTemplateVariables['store_name'] = $order->getStoreName();
   $emailTemplateVariables['store_url'] = 
Mage::getBaseUrl(Mage_Core_Model_Store::URL_TYPE_WEB);
  $emailTemplateVariables['payment_method'] = 
$order->getPayment()->getMethodInstance()->getTitle();
 
  $emailTemplate->send($receivers, $order->getStoreName(), 
$emailTemplateVariables);
       }
}

Il metodo notify() si occuperà semplicemente di prelevare gli stati dell’ordine per i quali inviare la email, configurati nel backend del modulo, e, se lo stato attuale dell’ordine è uno di questi, chiamare la funzione _sendEmail(), che prepara le variabili da utilizzare nel template della email (caricato nella riga $emailTemplate->loadDefault(‘wapone_order_status_notifier’)) e chiama il metodo send() della classe Mage_Core_Model_Email_Template.

Occorre quindi anche preparare un template per l’email da inviare. Per fare questo abbiamo bisogno di:

1. Aggiungere la configurazione in app/code/local/WaPoNe/etc/config.xml nel tag <global>

<template>
       <email>
       <wapone_order_status_notifier module="WaPoNe_OrderStatusNotifier">
       <label>Order Status Notifier</label>
       <file>wapone/order_status_notifier.html</file>
       <type>html</type>
       </wapone_order_status_notifier>
       </email>
</template>

2. Creare il template order_status_notifier.html sotto la cartella app/locale/en_US/template/email/wapone/, come definito in <file>wapone/order_status_notifier.html</file> del config.xml

<!--@subject Order Status Notifier: {{var store.getFrontendName()}}, 
Order #{{var order.increment_id}} @-->
 
<!--@styles
body,td { color:#2f2f2f; font:11px/1.35em Verdana, Arial, Helvetica, sans-serif; }
@-->
 
<body style="background:#F6F6F6; font-family:Verdana, Arial, Helvetica, sans-serif; 
font-size:12px; margin:0; padding:0;">
<div style="background:#F6F6F6; font-family:Verdana, Arial, Helvetica, sans-serif; 
font-size:12px; margin:0; padding:0;">
        <table cellspacing="0" cellpadding="0" border="0" width="100%">
        <tr>
        <td align="center" valign="top" style="padding:20px 0 20px 0">
                <table bgcolor="#FFFFFF" cellspacing="0" cellpadding="10" border="0" 
width="650" style="border:1px solid #E0E0E0;">
               <!-- [ header starts here] -->
               <tr>
               <td valign="top"><a href="{{store url=""}}"><img src="{{var logo_url}}" 
alt="{{var logo_alt}}" style="margin-bottom:10px;" border="0"/></a></td>
               </tr>
               <!-- [ middle starts here] -->
               <tr>
               <td valign="top">
                       <p style="font-size:12px; line-height:16px; margin:0;">
                     <b>{{htmlescape var=$order.getCustomerName()}} ({{var 
order.customer_email}}) has changed order status in: {{var order_status}}</b>
                      </p>
              </tr>
              <tr>
              <td>
                      <h2 style="font-size:18px; font-weight:normal; margin:0;">Order #{{var 
order.increment_id}} <small>(placed on {{var 
order.getCreatedAtFormated('long')}})</small></h2>
              </td>
              </tr>
              <tr>
              <td>
                   <table cellspacing="0" cellpadding="0" border="0" width="650">
                    <thead>
                    <tr>
                    <th align="left" width="325" bgcolor="#EAEAEA" style="font-size:13px; 
padding:5px 9px 6px 9px; line-height:1em;">Billing Information:</th>
                     <th width="10"></th>
                     <th align="left" width="325" bgcolor="#EAEAEA" style="font-size:13px; 
padding:5px 9px 6px 9px; line-height:1em;">Payment Method:</th>
                     </tr>
                     </thead>
                     <tbody>
                     <tr>
                     <td valign="top" style="font-size:12px; padding:7px 9px 9px 9px; 
border-left:1px solid #EAEAEA; border-bottom:1px solid 
#EAEAEA; border-right:1px solid #EAEAEA;">
                            {{var order.getBillingAddress().format('html')}}
                       </td>
                        <td>&nbsp;</td>
                        <td valign="top" style="font-size:12px; padding:7px 9px 9px 9px; border-left:1px solid 
#EAEAEA; border-bottom:1px solid #EAEAEA; border-right:1px solid #EAEAEA;">
                                      {{var payment_method}}
                                  </td>
                                  </tr>
                                  </tbody>
                                  </table>
                             <br/>
                                  {{depend order.getIsNotVirtual()}}
                                  <table cellspacing="0" cellpadding="0" border="0" width="650">
                                  <thead>
                                  <tr>
                                  <th align="left" width="325" bgcolor="#EAEAEA" style="font-size:13px; 
padding:5px 9px 6px 9px; line-height:1em;">Shipping Information:</th>
                     <th width="10"></th>
                    <th align="left" width="325" bgcolor="#EAEAEA" style="font-size:13px; 
padding:5px 9px 6px 9px; line-height:1em;">Shipping Method:</th>
                     </tr>
                     </thead>
                     <tbody>
                     <tr>
                     <td valign="top" style="font-size:12px; padding:7px 9px 9px 9px; 
border-left:1px solid #EAEAEA; border-bottom:1px solid #EAEAEA; border-right:1px solid 
#EAEAEA;">
                           {{var order.getShippingAddress().format('html')}}
                        &nbsp;
                        </td>
                        <td>&nbsp;</td>
                        <td valign="top" style="font-size:12px; padding:7px 9px 9px 9px; 
border-left:1px solid #EAEAEA; border-bottom:1px solid #EAEAEA; border-right:1px solid 
#EAEAEA;">
                           {{var order.getShippingDescription()}}
                        &nbsp;
                        </td>
                        </tr>
                        </tbody>
                        </table>
                        <br/>
                        {{/depend}}
 
                        <!-- Order Items -->
                        {{layout handle="sales_email_order_items" order=$order}}
 
                        <p style="font-size:12px; margin:0 0 10px 0">{{var 
order.getEmailCustomerNote()}}</p>
              </td>
              </tr>
              </table>
       </td>
       </tr>
       </table>
</div>
</body>

Quello che manca è la parte della configurazione dei parametri del modulo. Si vorrebbe permettere all’utente di scegliere il nome e l’indirizzo email del sender della email, gli indirizzi di posta elettronica che invece devono ricevere il messaggio di posta e gli stati dell’ordine che si vogliono monitorare.

Tutto questo è possibile grazie al file app/code/local/WaPoNe/etc/system.xml così fatto:

<?xml version="1.0"?>
<config>
       <sections>
       <orderstatusnotifier translate="label" module="adminhtml">
       <label>Order Status Notifier</label>
       <tab>wapone</tab>
       <frontend_type>text</frontend_type>
       <sort_order>3</sort_order>
       <show_in_default>1</show_in_default>
       <show_in_website>1</show_in_website>
       <show_in_store>1</show_in_store>
       <groups>
              <orderstatusnotifier_group translate="label">
              <label>Order Status Notifier</label>
           <frontend_type>text</frontend_type>
           <sort_order>6</sort_order>
           <show_in_default>1</show_in_default>
          <show_in_website>1</show_in_website>
           <show_in_store>1</show_in_store>
             <fields>
             <sender_name translate="label">
                   <label>Sender Name</label>
                 <frontend_type>text</frontend_type>
             <backend_model>adminhtml/system_config_backend_email_sender</backend_model>
               <validate>validate-emailSender</validate>
               <sort_order>1</sort_order>
               <show_in_default>1</show_in_default>
            </sender_name>
            <sender_email translate="label">
                  <label>Sender Email</label>
             <frontend_type>text</frontend_type>
             <validate>validate-email</validate>
            <backend_model>adminhtml/system_config_backend_email_address</backend_model>
              <sort_order>2</sort_order>
              <show_in_default>1</show_in_default>
           </sender_email>
           <receiver_emails>
             <label>Receiver Emails</label>
             <comment><![CDATA[Insert receiver emails separeted by ';'<br />Do not insert ';' at the end.<br />Ex: [email­protected];[email­protected]]]></comment>
             <frontend_type>text</frontend_type>
             <sort_order>3</sort_order>
             <show_in_default>1</show_in_default>
             <show_in_website>1</show_in_website>
             <show_in_store>1</show_in_store>
          </receiver_emails>
          <statuses translate="label">
             <label>Statuses</label>
             <frontend_type>multiselect</frontend_type>
          <source_model>WaPoNe_OrderStatusNotifier/system_config_source_orderstates</source_model>
            <sort_order>4</sort_order>
            <show_in_default>1</show_in_default>
          </statuses>
          </fields>
     </orderstatusnotifier_group>
  </groups>
  </orderstatusnotifier>
  </sections>
</config>

In questo file si è scelto di voler prepopolare la multiselect ‘status’ con gli stati di Magento e per fare questo è stato necessario l’utilizzo di un source model personalizzato creando il file Orderstatus.php in app/code/local/WaPoNe/OrderStatusNotifier/Model/System/Config/Source

<?php
 
class WaPoNe_OrderStatusNotifier_Model_System_Config_Source_Orderstatus
{
       public function getOrderStatusesOption()
       {
       $statuses = 
Mage::getModel('sales/order_status')->getResourceCollection()->getData();
      $orderStatuses = array();
      foreach ($statuses as $status)
      {
      $orderStatuses[$status["status"]] = $status["label"];
      }
      return $orderStatuses;
      }
 
      public function toOptionArray()
      {
      $options = array();
      foreach ($this->getOrderStatusesOption() as $code => $label) {
      $options[] = array(
             'value' => $code,
             'label' => $label
      );
      }
 
      return $options;
      }

Un ultimo passaggio, non meno importante degli altri, riguarda la definizione dei permessi sul modulo (ACL); per questo motivo c’è bisogno di creare nella cartella app/code/local/WaPoNe/etc/ il file adminhtml.xml:

<?xml version="1.0"?>
<config>
   <acl>
       <resources>
           <admin>
                 <children>
                 <system>
                 <children>
                         <config>
                       <children>
                         <orderstatusnotifier>
                           <title>Order Status Notifier</title>
                        </orderstatusnotifier>
                     </children>
                       </config>
                    </children>
               </system>
               </children>
        </admin>
        </resources>
        </acl>
</config>

Conclusa la fase di sviluppo non ci resta che configurare il modulo ed aspettare gli ordini dei clienti.
Grazie alla duttilità del pattern ‘Event Observer’ questo modulo può essere riadattato per altre funzionalità come, per esempio, notificare l’iscrizione al portale da parte di un nuovo utente, informare sull’inserimento di un prodotto nella wishlist del cliente e tant’altro ancora.

L’intero modulo è scaricabile e raggiungibile all’indirizzo web: https://github.com/WaPoNe/WaPoNe_OrderStatusNotifier

Categoria: