PSR-1: Basic Coding Standard

Nein, in diesem Beitrag geht es nicht um die PSR-1 aus PIXEL GUN 3D, hier geht es um Coding Standards und zwar den der PHP Framework Interop Group, wie man standardkonformen PHP-Code schreibt. Von dieser Gruppe wurden mehrere Spezifikationen zu PHP verfasst. Die PSR-1, um die es hier maßgeblich geht, beschäftigt sich mit allgemeinen Vorgaben zu Inhalt/Struktur einer PHP-Datei und Namensgebungen.

Die PSR-1 (PHP Standard Recommendation 1) befasst sich, wie der Name „Basic Coding Standard“ schon sagt, mit dem allgemeinen, grundlegendem Programmierstil. Also zum Beispiel den Tags, der Namensgebung und dem zu verwendenden Zeichensatz.

Allgemeines


PHP-Tags

Damit der Code standardkonform ist, muss der PHP-Tag ausgeschrieben werden <?php ?>. Alternativ kann man die Kurzform des echo-Tags benutzen <?= ?>, andere Varianten sollen nicht genutzt werden.

Zeichensatz

Es soll nur UTF-8 ohne BOM verwendet werden.

Inhalt

Eine PHP-Datei darf nicht gleichzeitig Deklarationen und sogenannte „side-effects“ enthalten.

Zu den Side-Effects gehören unter anderem:

  • Ausgaben
  • Auswerfen von Ausnahmen oder Fehlermeldungen
  • Externe Verbindungen herstellen
  • Explizites Aufrufen von „require“ und „include“
  • Änderungen von statischen oder globalen Variablen
  • Ein- oder Auslesen einer Datei



Namespaces und Klassen

Namespaces müssen einer der beiden „autoloading“ Richtlinien (PSR-0 oder PSR-4) entsprechen. Diese enthalten unter anderem Spezifikationen zu der Vergabe von vollqualifizierten Klassennamen. Ziel ist, dass der Code bei Bedarf einheitlich und automatisch nachgeladen werden kann.

Namensgebung

  • Klassennamen müssen nach der StudlyCaps Schreibweise vergeben werden
  • Konstantennamen müssen komplett groß geschrieben werden. Einzelne Wörter werden dabei mit Unterstrichen voneinander getrennt
  • Methodennamen müssen nach der camelCase() Schreibweise vergeben werden
  • Attributnamen müssen nach keiner bestimmten Konvention geschrieben werden. Es sollte allerdings darauf geachtet werden, dass innerhalb eines Bereichs eine einheitliche Namensgebung verwendet wird.



Weitere Informationen gibt es auf der Seite der PHP Framework
Interop Group: www.php-fig.org

WordPress: Inhalts-Editor auf bestimmten Template-Seiten verstecken

Manche WordPress-Seiten benötigen aufgrund des Templates keinen Editor für den Seiteninhalt. Dieser ist beispielsweise über sog Custom-Fields, die bspw. mit dem Plugin ACF gepflegt. Damit Benutzer den Inhalt der Seite nicht trotzdem über den Editor pflegen wollen und sich wundern, warum dieser nicht angezeigt wird, empfiehlt es sich diesen auszublenden.

Um den Inhalts-Editor für Seiten eines bestimmten Tepmlate-Typs auszublenden wird folgende Action in die Datei functions.php des Themes eingefügt.

add_action( 'admin_init', 'hide_editor' );

function hide_editor() {
      global $pagenow;<br>
    if( !( 'post.php' == $pagenow ) ) return;
<br>
    global $post;
    //ID des Posts
    $post_id = $_GET['post'] ? $_GET['post'] : $_POST['post_ID'];
    if( !isset( $post_id ) ) return;
<br>
    // Verstecken des Editors auf einer Seite mit einem bestimmten Titel
    $homepgname = get_the_title($post_id);
    if($homepgname == 'Seitentitel'){ 
       remove_post_type_support('page', 'editor');
    }
    // Verstecken des Editors auf Seiten eines bestimmten Seitentemplates
    // Ermitteln des Names des Seitentemplates
    $template_file = get_post_meta($post_id, '_wp_page_template', true);
    if($template_file == '[Seitentemplate].php'){
       remove_post_type_support('page', 'editor');
      }
}


Dabei muss der Titel der Seite oder der Name der Template-Datei festgelegt werden. Wird diese Seite nun editiert, ist der Editor für den Inhalt der Seite nicht mehr sichtbar.

Erstellen einer benutzerdefinierten Versandmethode in Magento

Auch wenn Magento sehr viele Versandarten mit den größeren Versanddienstleistern anbietet, braucht man hin und wieder noch eigene Versandmethoden wie beispielsweise die Abholung (In-Store Pickup). In einigen Fällen braucht man noch die eigene Logik für den Versand oder die Berechnung der Kosten, sodass es keine andere Alternative gibt, als eine eigene Methode zu definieren und zu entwicklen.

Wie auch immer das Szenario ist, es ist relativ einfachm eine Versandmethode als Modul in Magento zu erstellen. Hierzu braucht man nur die dazugehörigen Konfigurationsdateien und ein Modell, das die Logik der Versandmethode implementiert. In diesem Artikel werden wir genau dieses umsetzen am Beispiel einer relativ einfachen Versandmethode „Abholung“.

An der Stelle sei vorrausgesetzt, dass der Modulerstellungsprozess von Magento klar ist.

Auf einen Blick

Es wird ein einfaches Versandmodul mit dem Namen „Abholung“ erstellt. Hier eine Liste der Dateien, die für das Modul benötigt werden:

  • app/etc/modules/CodingPioneers_All.xml: Eine Datei, um das Modul zu aktivieren
  • app/code/local/CodingPioneers/Customshippingmethod/etc/config.xml: Die Modul-Konfiguration, welche die Versandmethode nach den Magento-Konventionen deklariert
  • app/code/local/CodingPioneers/Customshippingmethod/etc/system.xml: Die Konfigurationsdatei, die die Backend-Konfigurationsmöglichkeiten des Moduls beschreibt
  • app/code/local/CodingPioneers/Customshippingmethod/Model/Demo.php: Das Modell zur Berechnung der Gebühren
  • app/code/local/CodingPioneers/Customshippingmethod/Helper/Data.php: Eine unterstützdende Datei, die von dem Magento Übersetzungssystem genutzt wird

Anlegen von Dateien und Verzeichnissen

Zunächst wird die Datei zur Modulaktivierung benötigt. Über die Autoloading-Hooks im Magento werden dann per Namenskonvention die erforderlichen weiteren Dateien hinzugeladen. Diese Datei wird wie folgt angelegt:

"app/etc/modules/CodingPioneers_All.xml"

Danach muss nachfolgender Text in die Dateien eingefügt werden. Als Namespace dient hier „CodingPioneers“ und „Abholung“ als Modulname. Dies aktiviert das Modul „Abholung“.

<?xml version="1.0"?>
<config>
  <modules>
    <CodingPioneers_Abholung>
      <active>true</active>
      <codePool>local</codePool>
      <depends>
        <Mage_Shipping />
      </depends>
    </CodingPioneers_Abholung>
  </modules>
</config>

Als Nächstes muss die Modul-Konfigurationsdatei "app/code/local/CodingPioneers/Abholung/etc/config.xml" angelegt werden und folgender Inhalt eingefügt werden:

<?xml version="1.0"?>
<config>   <modules>     <CodingPioneers_Abholung>       <version>1.0</version>     </CodingPioneers_Abholung>   </modules>   <global>     <models>       <CodingPioneers_Abholung>         <class>CodingPioneers_Abholung_Model</class>       </CodingPioneers_Abholung>     </models>     <helpers>       <CodingPioneers_Abholung>         <class>CodingPioneers_Abholung_Helper</class>       </CodingPioneers_Abholung>     </helpers>   </global>   <default>     <carriers>       <CodingPioneers_Abholung>         <active>1</active>         <sallowspecific>1</sallowspecific>         <model>CodingPioneers_Abholung/demo</model>         <name>Abholung</name>         <price>0.00</price>         <title>Abholung</title>       </CodingPioneers_Abholung>     </carriers>   </default> </config>

In der Datei "config.xml" wurden zunächst das Modell und die Helferklasse in der Sektion <global> deklariert. Das in diesem Beitrag wichtige Tag <carriers> wurde unter der Sektion <default> deklariert. Dieses teilt Magento mit, dass eine neue Versandmethode angehängt wird, wie diese unter dem Tag <CodingPioneers_Abholung> deklariert wurde. Dieser sollte zwingend einzigartig sein, um nicht mit anderen Versandmethoden in Konflikt zu geraten.

Andere wichtige Tags sind <sallowspecific> und <model>. Der Tag <sallowspecific> steht für „Versende an alle ausgewählten Länder“. Dies erlaubt, dass die zutreffenden Länder ausgewählt werden können, für die die Versandmethode verfügbar sein soll. Der Tag <model> definiert den Ort des Modells für diese Versandmethode.

Als nächstes muss die Datei "system.xml" angelegt werden. Datei stellt die Back-End-Einstellungen für diese Versandmethode zur Verfügung. Dazu wird die Datei "app/code/local/CodingPioneers/Abholung/etc/system.xml" mit dem folgendem Inhalt angelegt:

<?xml version="1.0" encoding="UTF-8"?>
<config>   <sections>     <carriers>       <groups>         <CodingPioneers_Abholung translate="label">           <label>CodingPioneers Demo Shipping Method</label>           <sort_order>1</sort_order>           <show_in_default>1</show_in_default>           <show_in_website>0</show_in_website>           <show_in_store>0</show_in_store>           <fields>             <active translate="label">               <label>Enabled</label>               <frontend_type>select</frontend_type>               <source_model>adminhtml/system_config_source_yesno</source_model>               <sort_order>1</sort_order>               <show_in_default>1</show_in_default>               <show_in_website>0</show_in_website>               <show_in_store>0</show_in_store>             </active>             <title translate="label">               <label>Shipping Method Name</label>               <frontend_type>text</frontend_type>               <sort_order>20</sort_order>               <show_in_default>1</show_in_default>               <show_in_website>0</show_in_website>               <show_in_store>0</show_in_store>             </title>             <sallowspecific translate="label"               <label>For selected countries only</label>               <frontend_type>select</frontend_type>               <frontend_class>shipping-applicable-country</frontend_class>
        <source_model>adminhtml/system_config_source_shipping_allspecificcountries</source_model>               <sort_order>30</sort_order>               <show_in_default>1</show_in_default>               <show_in_website>0</show_in_website>               <show_in_store>0</show_in_store>             </sallowspecific>             <specificcountry translate="label">               <label>Ship to Specific Countries</label>               <frontend_type>multiselect</frontend_type>               <sort_order>31</sort_order>               <source_model>adminhtml/system_config_source_country</source_model>               <show_in_default>1</show_in_default>               <show_in_website>0</show_in_website>               <show_in_store>0</show_in_store>               <can_be_empty>1</can_be_empty>             </specificcountry>           </fields>         </CodingPioneers_Abholung>       </groups>     </carriers>   </sections> </config>

Dies erzeugt das Formular zur Konfiguration mit den Feldern wie unter dem <fields>-Tag zu sehen. Auf diesen Punkt können wir später noch einmal zurück.

Nun zur Datei mit dem Modell "app/code/local/CodingPioneers/Abholung/Model/Demo.php" mit folgendem Inhalt:

<?php
// app/code/local/CodingPioneers/Abholung/Model
class CodingPioneers_Abholung_Model_Demo
  extends Mage_Shipping_Model_Carrier_Abstract
  implements Mage_Shipping_Model_Carrier_Interface
{
  protected $_code = 'CodingPioneers_Abholung';
 
  public function collectRates(Mage_Shipping_Model_Rate_Request $request)
  {
    $result = Mage::getModel('shipping/rate_result');
    $result->append($this->_getDefaultRate());
 
    return $result;
  }
 
  public function getAllowedMethods()
  {
    return array(
      'CodingPioneers_Abholung' => $this->getConfigData('name'),
    );
  }
 
  protected function _getDefaultRate()
  {
    $rate = Mage::getModel('shipping/rate_result_method');
     
    $rate->setCarrier($this->_code);
    $rate->setCarrierTitle($this->getConfigData('title'));
    $rate->setMethod($this->_code);
    $rate->setMethodTitle($this->getConfigData('name'));
    $rate->setPrice($this->getConfigData('price'));
    $rate->setCost(0);
     
    return $rate;
  }
}

Um an der Stelle ein wenig mehr ins Detail zu gehen: Es wurde die $_code Variable auf ‚CodingPioneers_Abholung‘ gesetzt, welche vorher in der Datei "config.xml" gesetzt wurde. Die Methode getAllowedMethods übergibt einen Array von Versandmethoden im Frontend im Bezahlprozess. In diesem Fall wurde nur eine Versandmethode deklariert, daher wird nur diese zurückgegeben.

Weiter wird die Funktion collectRates von Magento aufgerufen, um die Versandpreise für die verschiedenen Versandoptionen zu berechnen. Die Implementierung ist gemäß der Magento-Konvention. Wirklich wichtig ist hier die Funktion _getDefaultRate, in der die Logik zur Berechnung für die Versandoption beschrieben ist. Auch wenn wir in diesem Fall nur einen festen Preis in der Datei "config.xml" angegeben haben. Je nach Bedürfnissen kann dies an die eigenen Anforderungen angepasst werden.

Zuletzt muss noch die Helfer-Datei "app/code/local/CodingPioneers/Abholung/Helper/Data.php" angelegt werden, welche vom Magento-Übersetzungssystem genutzt wird:

<?php
class CodingPioneers_Abholung_Helper_Data extends Mage_Core_Helper_Abstract
{
}


Nun sollten alle Dateien dort sein, wo sie hingehören und es sollte funktionieren! Um das zu prüfen, müssen Sie sich im Backend einloggen, das Modul aktivieren und den Cache leeren. Unter System ⇒ Konfituration ⇒ Verkäufe ⇒ Versandarten sollten Sie die Versandart mit dem Titel „Abholung“ gelistet sehen.

Im Frontend kann man die Versandmethode nutzen, wenngleich es sich hier nur um ein wirklich einfaches Beispiel handelt, um die Implementierung zu demonstrieren.

Android: Implementierung In-App-Käufe

Der Google Play Store stellt Android-Nutzer rund 700.000 Anwendungen zum download bereit. Die Anwendungen können dort kostenfrei oder gegen ein kleines Entgeld gekauft und installiert werden. Damit eine kostenfreie Anwendung für den Entwickler gewinnbringend ist, wird oft mit der Hilfe von Google-Adsense Werbung in die Applikation eingefügt. Durch die vom Google Play Store zur Verfügung gestellten In-App-Käufe kann der Entwickler dem Benutzer ermöglichen die oft lästige Werbung durch den Erwerb einer Premium-Version zu entfernen. Des Weiteren können die Anwendungen durch In-App-Käufe (z.B. bei einem Spiel zusätzlich Leben zu erwerben) dem Entwickler einen zusätzlichen Gewinn bringen.

Die Implementierung von In-App-Käufen ist in erster Linie recht simpel, wenn bestimmte Punkte eingehalten werden. Hält man sich nicht an diese Punkte und berücksichtigt bestimmte Tatsache nicht, kann die Implementierung von In-App-Käufen gerne sehr viel Zeit kosten. Ich selbst habe dies im Rahmen eines Kundenprojekts erfahren und möchte an dieser Stelle mit diesem Blogbeitrag andere Entwickler davor bewahren in die gleichen Fallen zu tappen.

Anforderungen

  • Google Developer Konto
  • Google Play Konto (Darf nicht identisch mit dem Google Developer Konto sein).

Bibliothek in das Projekt einbinden

Um den Billing-Service von Google in einem Projekt verfügbar zu machen, muss die Biliothek zunächst mit Hilfe des Android-SDK-Managers herrunter geladen werden. Das Paket nennt sich Google Play Billing Library und befindet sich unter dem Punkt „Extras“.

Die Datei IInAppBillingService.aidl befindet sich nach dem Download in folgendem Verzeichnis:

 <sdk>/extras/google/play_billing/

Die Datei „IInAppBillingService.aidl“ muss jetzt dem Projekt hinzugefügt werden, wobei die Datei unter Eclipse in dem Projekt-Pfad /src in das Package com.android.vending.billing kopiert wird. Ist das Package nicht vorhanden, muss dieses erstellt werden. Unter einem Nicht-Eclipse-Projekt wird die Datei in das Verzeichnis /src/com/android/vending/billing kopiert. Auch hier müssen die Ordner ggf. erstellt werden. Nachdem die Datei kopiert wurde, muss der Build-Vorgang des Projekts einmal druchgeführt werden. Als Nächstes muss dem Projekt die Berechtigung zur Verwendung der IInAppBillingService.aidl zugeteilt werden. Dazu wird in der AndroidManifest.xml folgende Zeile eingefügt:

 <uses-permission android:name="com.android.vending.BILLING" />

Das Projekt ist jetzt bereit einen Request an den Google Play Store zu senden und eine Antwort zu erhalten.

Hilfsklassen

Das Beispiel-Projekt der Google Play Billing Library verfügt über Hilfsklassen, welche uns die Implementierung der In-App-Käufe erleichtern. Die 9 Klassen, welche unter folgendem Pfad zu finden sind, kopieren wir in unser Projekt in ein dafür angelegtes Package.

 PFAD_SDK/extras/google/play_billing/samples/TrivialDrive/src/com/xample/android/trivialdrivesample/util

Anlegen der Produkte

Damit die Produkte gekauft werden können, müssen diese zunächst der Anwendung in der Google Developer Konsole hinzugefügt werden. Dazu muss die Anwendung in dem Developer Konto erstellt werden (falls noch nicht geschehen). Nach der Auswahl der Anwendung können unter dem Punkt In-App-Produkte neue Produkte für die Anwendung erstellt werden. Bei der Vergabe der Produkt-ID ist darauf zu achten, das diese zu einem späteren Zeitpunkt nicht mehr geändert werden kann. In der API Version 3 stellt Google nur noch ein verwaltetes Produkt und ein Abo zur Verfügung. Bei einem Abo handelt es sich um eine automatisch wiederkehrende Zahlung, welche für normale In-App-Produkte wie z.B. eine Premium-Version oder zusätzliche Leben nicht in Frage kommt. Die API Version 3 unterscheidet bei verwaltet Produkten in verbrauche und nicht verbrauchbare Produkte. Nicht verbrauchbare Produkte können nur einmalig gekauft werden, wobei dessen Erwerb im Google Play Store Konto gespeichert wird. Verbrauchbare Produkte verhalten sich im ersten Moment wie ein Nicht-Verbrauchbares-Produkt bis zu dem Zeitpunkt wo sie verbraucht werden. Nachdem sie verbraucht wurden können die verbrauchbaren Produkte erneut gekauft werden. Wird ein Produkt verbraucht, kann der Erwerb dieses Produkts jedoch nicht mehr nachgewiesen werden. Über diesen Fakt sollte man sich an dieser Stelle im Klaren sein und seine Anwendung dementsprechend anpassen, da Produkte, welche der Benutzer gekauft und nicht verbraucht hat, nach einer Neuinstallation oder Installation auf einem anderen Device wieder zur Verfügung gestellt werden müssen.

Testzugriff auf das Developer Konto

Um über ein Google-Play-Konto den Erwerb der erstellten Produkte zu testen, muss diesem Google-Play-Konto Testzugriff auf das Developer Konto gewährt werden. Die Email-Adresse des für den Testzugang kann im Google Developer Konto unter EinstellungenGmail-Konten mit Testzugriff eingegeben werden.

Billing Service Klasse erstellen

Zurück in der Anwendung erstellen wir eine Klasse (Im folgenden Beispiel MyBillingService, welche die Schnittstelle zum Google Play Store darstellt.

Wichtig<br> Produkt (SKU) Informationen können nur von bereits gekauften Produkte angefragt werden. Da bis dato keine Produkte gekauft wurden erhält man die Information:<br><br> Querying SKU details.<br> queryPrices: nothing to do because there are no SKUs.<br><br> Produkt-Informationen können erst nach dem Erwerb gelistet werden.

In der folgenden Klasse sind notwendige Anpassungen durch Kommentare gekennzeichnet.

public class MyBillingService {
  // Debug-Tag for logging   public static final String TAG = MyBillingService.class.getSimpleName();   //Konstante, um festzulegen ob die gekauften Produkte des Benutzers beim ersten
  //Start der Anwendung abgefragt wurden   public static final String KEY_CHECK_ITEMS = "check_items";
  //Konstante zum Lesen und Speichern via SharedPreferences   public static final String KEY_LIFE_COUNT = "life_count";   private static MyBillingService instance = null;
  private Activity activity;
  // SKUs der Produkte im App-Store definieren (ID)   public static final String SKU_ONE = "ID Produkt 1";   public static final String SKU_TWO = "ID Produkt 2";

  //(arbitrary) Request Code
  static final int RC_REQUEST = 10001;
  //Status der Produkte   //Jedes Produkt besitzt den Status gekauft oder nicht gekauft   private boolean skuOne;   private boolean skuTwo;
  private HashMap<String, Boolean> status;

  // Atrribut, welches durch In-App-Produkte beeinlusst wird.
  // Zum Beispiel Anzahl der Leben in einem Spiel   int maxLife;
  // Helper Objekt
  private IabHelper mHelper;

  public static MyBillingService getInstance(Activity activity) {
    if(instance == null) {
      instance = new MyBillingService(activity);
    }
    return instance;
  }

  private MyBillingService(Activity activity) {

    //Status setzen     this.activity = activity;
    status = new HashMap<>();
    status.put(SKU_ONE, false);
    status.put(SKU_TWO, false);

    //Muss auf das jeweilige Projekt angepasst werden.
    maxLife = Integer.valueOf(MyApplication.getInstance().readFromPreferences(KEY_LIFE_COUNT, "2"));

    //Befindet sich im Developer Konto nach der Auswahl der Anwendung unter Dienste & APIs
    String base64EncodedPublicKey = "API KEY HIER EINFÜGEN";

    // Erstellen des Helpers
    Log.d(TAG, "Creating IAB helper.");
    mHelper = new IabHelper(this.activity, base64EncodedPublicKey);

    //Schaltet das Debuggen ein. In einer Release-Version sollte hier false übergeben werden.
    mHelper.enableDebugLogging(true);

    Log.d(TAG, "Starting setup.");
    mHelper.startSetup(new IabHelper.OnIabSetupFinishedListener() {
      public void onIabSetupFinished(IabResult result) {
        Log.d(TAG, "Setup finished.");

        if (!result.isSuccess()) {
          // Problemfall
          complain("Problem setting up in-app billing: " + result);
          return;
        }

        // Wurde das Helper-Objekt bereits zerstört, beenden
        if (mHelper == null) return;

        // IAB wurde initialisiert. Anfragen welche Produkte der Benutzer besitzt.
        Log.d(TAG, "Setup successful. Querying inventory.");
        mHelper.queryInventoryAsync(mGotInventoryListener);
      }
    });
  }


  // Listener, welcher ausgerufen wird, wenn die Liste mit gekauften Produkten erfolgreich angefragt wurde
  private IabHelper.QueryInventoryFinishedListener mGotInventoryListener = new IabHelper.QueryInventoryFinishedListener() {
    public void onQueryInventoryFinished(IabResult result, Inventory inventory) {
      Log.d(TAG, "Query inventory finished.");

      if (mHelper == null) return;

      // Meldung im Fehlerfall
      if (result.isFailure()) {
        complain("Failed to query inventory: " + result);
        return;
      }

      Log.d(TAG, "Query inventory was successful.");

      // Welche Produkte hat der Benutzer bereits gekauft?

      Purchase purchase;
      for(Map.Entry<String, Boolean> product: status.entrySet()) {
        purchase = inventory.getPurchase(product.getKey());
        status.put(product.getKey(), (purchase != null && verifyDeveloperPayload(purchase)));
        Log.d(TAG, "User has " + product.getKey() + (product.getValue() ? " bought" : " not bought"));
      }

      //Muss angepasst werden: Beim ersten Start der Anwendung       //müssen alle gekauften Artikel mit ihren Auswirkungen berücksichtigt werden
      if(!Boolean.valueOf(MyApplication.getInstance().readFromPreferences(KEY_CHECK_ITEMS, "false")))       {       for(Map.Entry product: status.entrySet()) {         switch (product.getKey()) {           case SKU_ONE:             if(product.getValue()) {               //TODO             }         }       }       saveData();       Log.i(TAG, "Initialzie in-app-purchase");       MyApplication.getInstance().saveToPreferences(KEY_CHECK_ITEMS, String.valueOf(true));       }

      Log.d(TAG, "Initial inventory query finished; enabling main UI.");
    }
  };

  public HashMap getStatus() {
    return status;
  }

  public void buyProdukt(String id) {
    Log.d(TAG, "Buy Produkt " + id);

    if (status.get(id)) {
      complain("Product already bought!");
      return;
    }

    Log.d(TAG, "Launching purchase flow for: " + id);

    /* TODO: for security, generate your payload here for verification. See the comments on
    * verifyDeveloperPayload() for more info. Since this is a SAMPLE, we just use
    * an empty string, but on a production app you should carefully generate this. */
    String payload = "";

    mHelper.launchPurchaseFlow(activity, id, RC_REQUEST,
    mPurchaseFinishedListener, payload);
  }

  //Funktion muss in der Activity aufgerufen werden in der die Klasse zuerst aufgerufen wird.
  public boolean onActivityResult(int requestCode, int resultCode, Intent data) {
    Log.d(TAG, "onActivityResult(" + requestCode + "," + resultCode + "," + data);
    if (mHelper == null) return false;

    if (!mHelper.handleActivityResult(requestCode, resultCode, data)) {
      return false;
    }
    else {
      Log.d(TAG, "onActivityResult handled by IABUtil.");
      return true;
    }
  }

  /** Verifizieren das es sich um einen gültigen Kauf handelt */
  boolean verifyDeveloperPayload(Purchase p) {
    String payload = p.getDeveloperPayload();

    /*
    * TODO: An dieser Stelle muss verifiziert werden, dass es sich um einen gültigen Kauf handelt.     * Der String (getDeveloperPayload) ist der gleiche, welcher beim Kauf übergeben wird.     *     * Einen zufälligen String zur Laufzeit zu generieren ist hierbei wenig zielführen,     * da sichergestellt werden muss, dass der Kauf von einem Gerät durchgeführt werden     * kann und durch ein andere verifiziert werden kann. Zu diesem Punkt wird bald ein     * weiterer Blogbeitrag folgen. Für Testzwecke reicht hier die Rückgabe von true aus     *     * Vor der Veröffentlichung im App-Store sollte diese Funktion auf jeden Fall implementiert werden     */

    return true;
  }
  // Listener, welcher aufgerufen wird, wenn der Kauf erfolgreich war
  private IabHelper.OnIabPurchaseFinishedListener mPurchaseFinishedListener = new IabHelper.OnIabPurchaseFinishedListener() {
    public void onIabPurchaseFinished(IabResult result, Purchase purchase) {
      Log.d(TAG, "Purchase finished: " + result + ", purchase: " + purchase);
      // if we were disposed of in the meantime, quit.
      if (mHelper == null) return;

      if (result.isFailure()) {
        //complain("Error purchasing: " + result);
        return;
      }
      if (!verifyDeveloperPayload(purchase)) {
        complain("Error purchasing. Authenticity verification failed.");
        return;
      }

      //Was soll passieren, wenn der Benutzer ein Produkt erworben hat?
      switch (purchase.getSku()) {       case SKU_ONE:         //TODO         saveData();         break;       }

      saveData();
      Log.d(TAG, "Purchase successful. Bought " + purchase.getSku());

      //Handelt es sich bei SKU ONE um ein verbrauchbaren Produkt?       //Wenn ja, dann dies an dieser Stelle verbraucht werden damit der       //Benutzer das Produkt erneut kaufen kann
      if (purchase.getSku().equals(SKU_ONE)) {
        Log.d(TAG, "Purchase is sku one. Starting sku one consumption.");
        mHelper.consumeAsync(purchase, mConsumeFinishedListener);
      }
    }
  };

  // Wird aufgerufen, sobald der Verbrauch eines Produkts erfolgreich war.
  private IabHelper.OnConsumeFinishedListener mConsumeFinishedListener = new IabHelper.OnConsumeFinishedListener() {
    public void onConsumeFinished(Purchase purchase, IabResult result) {
      Log.d(TAG, "Consumption finished. Purchase: " + purchase + ", result: " + result);

      if (mHelper == null) return;

      // An dieser Stelle wissen wir, dass es sich um SKU_ONE handelt
      // da nur dieses Produkt verbraucht werden kann.
      // Sind mehere verbrauchbare Produkte vorhanden, muss dies an dieser Stelle überprüft werden
      if (result.isSuccess()) {
        // Was soll gesehen, wenn der Benutzer SKU_ONE verbraucht hat?
        Log.d(TAG, "Consumption successful. Provisioning.");
        //TODO
        saveData();
      }
      else {
        complain("Error while consuming: " + result);
      }
      Log.d(TAG, "End consumption flow.");
    }
  };

  // Methode muss in der StartActivity aufgerufen werden   public void destroy() {
    // Sehr wichtig:     Log.d(TAG, "Destroying helper.");     if (mHelper != null) {       mHelper.dispose();       mHelper = null;     }   }

  void complain(String message) {
    Log.e(TAG, "**** TrivialDrive Error: " + message);
    alert("Fehler: " + message);
  }

  void alert(String message) {
    AlertDialog.Builder bld = new AlertDialog.Builder(activity);
    bld.setMessage(message);
    bld.setNeutralButton("OK", null);
    Log.d(TAG, "Showing alert dialog: " + message);
    bld.create().show();
  }

  void saveData() {

    /*     * Speichern der Daten in der Applikation     * Eine einfache Möglcihkeit ist die Verwendung von SharedPreferences.     * Bei einer veröffentlichten App sollten diese Daten verschlüsselt werden,     * da sonst unbefugter Zugriff möglich ist.     * Mehr dazu in den Anmerkungen.     */
  }
}

Mit der Anwendung verknüpfen

Die zuvor erstellte Klasse muss in der MainActivity der Anwendung initialisiert werden. Dazu wird die folgende Codezeile im Konstruktor verwendet:

  billing = MyBillingService.getInstance(this);

Die Methoden unserer Billing Klasse onDestroy() und onActivityResult() müssen jetzt mit der MainActivity verknüpft werden. Dazu wird folgender Code in die MainActivity des Projekts eingefügt.

  @Override
  protected void onActivityResult(int requestCode, int resultCode, Intent data) {     Log.d(TAG, "onActivityResult(" + requestCode + "," + resultCode + "," + data);     if(!billing.onActivityResult(requestCode, resultCode, data)) {       super.onActivityResult(requestCode, resultCode, data);     }     else {       Log.d(TAG, "onActivityResult handled by IABUtil.");     }   }
  @Overridee   protected void onDestroy() {     billing.destroy();     super.onDestroy();   }

Unsere Billing Klasse ist nun mit der Anwendung verknüpft und kann in jedem Fragment aufgerufen werden.

Produkte kaufen

Produkte können nun in einem dafür zuständigen Fragment gekauft werden. Beispielweise kann der Erwerb des ersten Produkts durch folgende Codezeile ausgelöst werden.

  MyBillingService.getInstance(getActivity()).buyProdukt(MyBillingService.SKU_ONE);

Testen wir diesen Vorgang nun in unserer lokalen Testumgebung wird die Anfrage der Produkte fehlschlagen und wir erhalten folgende Meldung.

Dieser Fehler ist darauf zurück zu führen das es sich bei der APK, welche den App-Store nach Podukten anfragt, nicht von uns signiert wurde.

APK signieren

Die Signierung einer APK erfolgt unter Android Studio problemlos unter dem Menüpunkt Build und Generate signed APK. Die Generierung einer signierten APK ist auf der Google Developer Seite ausführlich beschrieben.

Hochladen der APK

Die generierte APK kann jetzt im Google Developer Konto in die Alpha- oder Beta-Phase hochgeladen werden. Wichtig dabei ist, das diese veröffentlicht und nicht nur als Entwurf gespeichert wird. Nach dem Hochladen der APK dauert es in der Regel 2-3 Stunden bis diese den Testern zur Verfügung gestellt wird. Auf dem aktuellen Stand haben wir jedoch noch keinen Alpha- Oder Beta-Test eingerichtet.

Alpha- oder Beta-Test einrichten

Damit unsere APK an Testnutzer ausgeliefert werden kann muss ein Alpha- oder Beta-Test eingerichtet werden. Die Email-Adresse unseres Google Play Kontos muss in die Liste der Tester eingetragen werden. Testbenutzer müssen die Einladung zum Alpha- oder Beta-Test über den unten stehenden Link annehmen. Direkt nach dem Hochladen der APK ist dieser Link ungütlig und erhält nach 2-3 Stunden Gültigkeit. Weitere Informationen zum Testen von APKs befinden sich auf der Google Developer Seite.

Wichtig<br> Es muss bereits eine aktive Version der Anwendung im Release sein, da die In-App-Produkte sonst nicht aktiviert werden und diese nicht gekauft werden können. Daher empfiehlt es sich zunächst eine Version ohne In-App-Käufe zu veröffentlichen und die In-App-Käufe dann in der nächsten Version zu implementierten.

Ist bereits eine Version der Anwendung im Release wird nach 2-3 Stunden ein Update an die Benutzer, welche sich in der Liste des Alpha- oder Beta-Tests befinden, ausgeliefert. Die APK der Alpha oder Beta-Phase muss eine höhere Versionsnummer als die Release Version haben, da immer die Version mit der höchsten Versions-Nummer ausgeliefert wird.


Testen der In-App-Käufe

Nachdem die neue Version auf den Devices des Testbenutzers aufgespielt wurde, kann der In-App-Kauf getestet werden. Da wir unser Testkonto zuvor mit Testzugriff versehen haben, wird uns für den Erwerb der Produkte kein Geld in Rechnung gestellt.

Anmerkungen

  • Produkt-Informationen können erst nach dem Erwerb gelistet werden
  • Kauf der Produkte ist nur über eine signierte APK möglich
  • Werden die Informationen über In-App-Käufe via Applikation-Preferences in der Anwendung gespeichert sollten diese verschlüsselt werden. Mehr dazu.
  • API-Key sollte aus Sicherheitsgründen zur Laufzeit zusammengesetzt werden
  • Produkte werden erst nach dem Release der Anwendung aktiviert

Abschluss

Wie zu sehen ist, ist die Implementierung von In-App-Käufen kein Hexenwerk, solange bestimmte Punkte beachtet werden. Bei Fehlern, wie zum Beispiel das Anfragen von Produktinformationen von nicht erworbenen Produkten, hält man sich unter Umständen sehr lange auf, wenn man über bestimmte Fakten nicht Bescheid weiß.

Quellen

Google Analytics datenschutzkonform nutzen

Immer mal wieder in den Medien oder auch gerne einmal abgemahnt: Der falsche Einsatz der Webtracking-Lösung Analytics von Google. Dabei ist es eigentlich ganz einfach:

  1. Vertrag zur Auftragsdatenverarbeitung (ADV) mit Google (Forderung aus § 11 BDSG) schließen
  2. Anonymisierung der IP-Adressen sicherstellen
  3. Widerspruchsrecht der Betroffenen einräumen
  4. Angepasste Datenschutzerklärung bereitstellen
  5. Löschung von Altdaten, die ggf. bereits gesammelt wurden

1. Vertrag zur Auftragsdatenverarbeitung schließen

Da personenbezogene Daten (hauptsächlich IP-Adresse) im Auftrag von Google verarbeitet werden, ist gemäß § 11 BDSG hierzu ein Vertrag nötig. Google hat hier mit einigen Datenschützern einen Mustervertrag zur Auftragsdatenverabeitung entwickelt, diesen können Sie unter folgender URL herunterladen:

http://static.googleusercontent.com/media/www.google.com/de//analytics/terms/de.pdf

Diesen Vertrag ausfüllen und mit vorfrankiertem Rückumschlag an Google senden und man hat zumeist binnen 7 bis 14 Tagen einen ADV mit Google mit dem alle Seiten (Sie, Google und die Datenschützer stellvertretend für das BDSG) leben können.

2. IP-Adressen anonymisieren

Um die Anonymisierung der IP-Adressen zu gewährleisten, hat Google eine Erweiterung des Google Analytics Codes zur Verfügung gestellt. Durch Nutzung der Code-Erweiterung„“anonymizeIp“ werden die letzten 8 Bit der IP-Adressen gelöscht und somit anonymisiert. Dadurch ist zwar weiterhin eine grobe Lokalisierung möglich, dies ist jedoch von den deutschen Datenschutzbehörden toleriert.

Die aktuell Universal Analytics-Lösung des Tracking-Codes erfordert die Anpassung wie folgt:

<script>

(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');

ga('create', 'UA-XXXXXXX-X', 'website.de');
ga('set', 'anonymizeIp', true);
ga('send', 'pageview');

</script>

Mit der eingefügten zusatzzeile ga('set', 'anonymizeIp', true); wird die Annonymisierung umgesetzt und bei Google entsprechend gespeichert.

3. Widerspruchsrecht Einräumen

Es muss dem Betroffenen (dem Besucher der Webseite) die Möglichkeit des Widerspruchs gegen die Erfassung von Nutzungsdaten eingeräumt werde.

Es müssen zwei Arten der Möglichkeit des Widerspruchs implementiert werden:

1. Link zum Deaktivierungs-Add-on

Das Deaktivierungs-Add-on verhindert, dass Google Analytics ausgeführt wird. Webseitenbetreiber wird von den entsprechenden Stellen empfohlen, auf diese Software zu verlinken, damit der Betroffene auch wirksam widersprechen kann.

Zu finden ist dieses Tool unter folgender URL: tools.google.com/dlpage/gaoptout

2. Möglichkeit zum setzen eines Opt-Out-Cookies

Da das Browser-Add-on nur Desktop-Nutzern die Möglichkeit des Widerspruchs gibt (was von den Aufsichtsbehörden beanstandet wurde), hat Google eingelenkt und eine alternative Möglichkeit mit dem Opt-Out-Cookie geschaffen. Der Betroffene hat die Möglichkeit, durch Klicken eines Links ein Opt-Out-Cookie zu setzen, der dann dazu führt, dass Tracking für das Endgerät des Betroffenen verhindert. Für das Opt-Out-Cookie muss folgendes Script immer vor dem eigentlichen Google Analytics-Script im Quelltext eingefügt werden.

<script>

var gaProperty = 'UA-XXXXXXX-X';
var disableStr = 'ga-disable-' + gaProperty;
if (document.cookie.indexOf(disableStr + '=true') > -1) {
window[disableStr] = true;
}

function gaOptout() {
document.cookie = disableStr + '=true; expires=Thu, 31 Dec 2099 23:59:59 UTC; path=/';
window[disableStr] = true;
}

</script>

4. Angepasster Datenschutzerklärung

Die Nutzung von Google Analytics sollte gegenüber dem Betroffenen in der Datenschutzerklärung offengelegt werden.

Bisher hatte Google in den Google Analytics Bedingungen eine Formulierung für die Datenschutzerklärung bereitgehalten. Dieser wird nun nicht mehr angeboten, da dieser auf den jeweiligen Zweck des Einsatzes angepasst werden hätte müssen.

Wenn man davon ausgeht, dass die bisherigen Datenschutzhinweise von Google die Datenverarbeitungen bei Google Analytics zutreffend beschreiben und dass diese auch nicht wesentlich geändert wurden, kann man die bisherige Textvorlage von Google Analytics weiterhin verwenden und mit der folgend vorgeschlagenen Ergänzungen versehen:

Diese Website benutzt Google Analytics, einen Webanalysedienst der Google Inc. („Google“). Google Analytics verwendet sog. „Cookies“, Textdateien, die auf Ihrem Computer gespeichert werden und die eine Analyse der Benutzung der Website durch Sie ermöglichen. Die durch das Cookie erzeugten Informationen über Ihre Benutzung dieser Website werden in der Regel an einen Server von Google in den USA übertragen und dort gespeichert. Im Falle der Aktivierung der IP-Anonymisierung auf dieser Website, wird Ihre IP-Adresse von Google jedoch innerhalb von Mitgliedstaaten der Europäischen Union oder in anderen Vertragsstaaten des Abkommens über den Europäischen Wirtschaftsraum zuvor gekürzt. Nur in Ausnahmefällen wird die volle IP-Adresse an einen Server von Google in den USA übertragen und dort gekürzt. Im Auftrag des Betreibers dieser Website wird Google diese Informationen benutzen, um Ihre Nutzung der Website auszuwerten, um Reports über die Websiteaktivitäten zusammenzustellen und um weitere mit der Websitenutzung und der Internetnutzung verbundene Dienstleistungen gegenüber dem Websitebetreiber zu erbringen. Die im Rahmen von Google Analytics von Ihrem Browser übermittelte IP-Adresse wird nicht mit anderen Daten von Google zusammengeführt. Sie können die Speicherung der Cookies durch eine entsprechende Einstellung Ihrer Browser-Software verhindern; wir weisen Sie jedoch darauf hin, dass Sie in diesem Fall gegebenenfalls nicht sämtliche Funktionen dieser Website vollumfänglich werden nutzen können. Sie können darüber hinaus die Erfassung der durch das Cookie erzeugten und auf Ihre Nutzung der Website bezogenen Daten (inkl. Ihrer IP-Adresse) an Google sowie die Verarbeitung dieser Daten durch Google verhindern, indem Sie das unter dem folgenden Link (http://tools.google.com/dlpage/gaoptout?hl=de) verfügbare Browser-Plugin herunterladen und installieren. Sie können die Erfassung durch Google Analytics verhindern, indem Sie auf folgenden Link klicken. Es wird ein Opt-Out-Cookie gesetzt, das die zukünftige Erfassung Ihrer Daten beim Besuch dieser Website verhindert: Google Analytics deaktivieren Nähere Informationen zu Nutzungsbedingungen und Datenschutz finden Sie unter http://www.google.com/analytics/terms/de.html bzw. unter https://www.google.de/intl/de/policies/. Wir weisen Sie darauf hin, dass auf dieser Website Google Analytics um den Code „gat._anonymizeIp();“ erweitert wurde, um eine anonymisierte Erfassung von IP-Adressen (sog. IP-Masking) zu gewährleisten. (Quelle der Formulierung: www.datenschutzbeauftragter-info.de).

5. Löschung von Altdaten

Es ist darauf zu achten, dass die Anpassungen nur neue Google Analytics Profile erfassen. Zuvor erhobene Properties (ehemals Profile) sind nach Ansicht der Aufsichtsbehörden unzulässig erhoben worden und so zu löschen.

Fazit

Mit der Einigung von Google und den Aufsichtsbehörden sieht man, dass auch Google daran interessiert ist, Rechtsunsicherheiten auszuräumen und einen datenschutzkonformen Betrieb zu ermöglichen. Mit den Auswirkungen der sog. Cookie-Richtlinie aus dem EU-Vorgaben, ist damit zu rechnen, das weitere Anpassungen erforderlich werden. Sollten Sie hierzu Fragen haben oder Hilfe benötigen, so helfen wir natürlich gerne!

Tutorial: Einführung in die Template-Engine Smarty

Schon wieder hat der Designer mit seinen Grundkenntnissen in HTML an Ihrem funktionalen PHP-Quellcode Änderungen vorgenommen, weil er das Design nach seinen Vorgaben anpassen wollte? Oft entstehen durch keine klaren Abgrenzungen von Funktion und Design ärgerliche Missgeschicke, welche häufig eine Menge unnötige Arbeit verursachen. Das so genannte Templating Framework Smarty schafft hier Abhilfe.

Die Template-Engine Smarty sorgt dafür, dass bei der Entwicklung einer Webanwendung die Applikations-Logik klar von dem Design/Ausgabe getrennt ist. Die Darstellung des Designs erfolgt bei Smarty mit der Hilfe von Templates. In diesen Templates wird mit einer Kombination von HTML, CSS und Smarty-Template-Tags festgelegt, wie die Informationen dargestellt werden sollen. Durch diese Trennung kann das Design jederzeit auf ein zeitgemäßes Layout angepasst werden, ohne dabei eine Änderung der kompletten Applikation vorzunehmen. Dieses Tutorial wird Ihnen anhand eines Beispielprojekts die Basics von Smarty näher bringen.

Anforderungen

  • Webserver mit PHP ≥ 4.0.6

Vorbereitung/Installation

Vorbereitung und Installation der Smarty-Template-Engine

Zu Beginn erstellen wir ein neues Projekt und nennen dies einfachheitshalber „smarty_tutorial“. In diesem Projekt werden als nächstes die Datei „index.php“ und der Ordner „lib“ und erzeugt. Im Ordner „lib“ werden die Datei „smtemplate.php“ sowie der Ordner „views“ erstellt. Jetzt können wir das Framework von der offiziellen Smarty Seite herunterladen und entpacken. Der Ordner des Archives „demo“ enthält Demo-Dateien, welche wir für unser Beispielprojekt nicht benötigen. Wichtig für uns ist der Ordner „libs“, welchen wir in „smarty“ umbenennen in dann in unser zuvor erstelltes Projekt „smarty_tutorial“ in den Ordner „lib“ einfügen. Um zu einem späteren Zeitpunkt den Update-Vorgang zu vereinfachen, sollten die Smarty-Dateien im Ordner „smarty“ nicht verändert werden. Des Weiteren benötigt Smarty die Ordner „templates_c“, „cache“ und „config“, welche wir im Ordner „smarty“ anlegen. Smarty benötigt Schreibrechte (755) auf den Inhalt des Ordners „smarty“. Die Struktur unseres Projekts sollte nun wie folgt aussehen:

Konfiguration

Als nächstes erstellen wir eine Datei „smtemplate.php“ im Ordner „lib“, welche eine Klasse für unsere Smarty Template Engine darstellt. In dem Konstruktor dieser Klasse müssen die Pfade zu den Unterverzeichnissen im „smarty“-Ordner konfiguriert werden. Für die Realisierung dieser Konfiguration gibt es zwei Möglichkeiten: zum einen die Angabe der Pfade als Klassenkonstanten direkt in der „smtemplate.php“-Datei oder zum anderen die Definition der Pfade in einer Konfig-Datei. Wir definieren unsere Einstellungen über eine Konfig-Datei, weil sich auf diese Weise alle Smarty-Konfigurationen über diese Datei regeln lassen. Also erstellen wir zunächst eine Datei „smtemplate_config.php“ und speichern die Pfade in einem Array.

$smtemplate_config =
 array(
      'template_dir' => 'views/',
      'compile_dir' => 'lib/smarty/templates_c/',
      'cache_dir' => 'lib/smarty/cache/',
      'config_dir' => 'lib/smarty/config/',
);

Jetzt können wir unsere Konfig-Datei in die Datei „smtemplate-php“ einbinden und die Konfiguration wie im Folgenden zu sehen ist vornehmen.

/* Smarty-Dateien laden */
require_once('smarty/Smarty.class.php');
require_once('smtemplate_config.php');

/* An dieser Stelle können Applikations spezifische Bibliotheken geladen werden * Bsp: require(bib.lib.php); * */
class SMTemplate extends Smarty { /**   * Konstruktor.   * Erzeugt eine neue Smarty-Instanz und konfiguriert die Smarty-Pfade   */
  function __construct(){   parent::__construct();   global $smtemplate_config;   $this->template_dir = $smtemplate_config['template_dir'];   $this->compile_dir = $smtemplate_config['compile_dir'];   $this->cache_dir = $smtemplate_config['cache_dir'];   $this->config_dir = $smtemplate_config['config_dir'];   } }

Erstes Template erstellen und darstellen

Um ein Template darstellen zu können, benötigt die SMTemplate-Klasse eine Funktion „render“, welche dies übernimmt.

function render($template){
     $this->display($template . '.tpl');
}

Als nächsten Schritt erstellen wir im Ordner „views“ eine Template mit dem Namen „hello.tpl“ und folgendem Inhalt.

<html>
    <head>
         <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
         <title>Home</title>
    </head>
  <body>   <p>Hello, World!</p>   </body>
</html>

Zur Darstellung des Templates wird in der Datei “index.php” die Funktion “render” unserer Smarty-Klasse aufgerufen.

require_once('lib/smtemplate.php');
$tpl = new SMTemplate(); $tpl->render('hello');

Der Aufruf der Seite „index.php“ sollte „Hello, World!“ ausgeben.


Übergabe von Variablen

Bei dem jetzigen Stand der Beispielanwendung können wir noch keine dynamischen Inhalte darstellen. Für diesen Fall verwendet Smarty Variablen, welche in einem Template benutzt werden können. Eine Übergabe dieser Variablen erfolgt durch die Übergabe eines Daten-Arrays an die Render-Funktion oder durch die Übergabe von Objekten, welche anschließend zu einem Array umgewandelt werden. In einem Template können die Variablen durch {$variablenname} aufgerufen werden. Um unsere Beispielapplikation etwas mit Leben zu füllen, muss zunächst die Render-Funktion angepasst werden, sodass diese das übergebene Daten-Array mit Hilfe der Funktion „assign“ an Smarty weitergibt.

function render($template, $data = array()){
     foreach($data as $key => $value) {
         $this->assign($key, $value);
     }
     $this->display($template . '.tpl');
}

In der Datei „index.php“ wird ein Array mit Daten gefüllt und an die Render-Funktion weitergeleitet.

$data = array(
     'user' => 'Coding Pioneers',
     'date' => time()
);

$tpl = new SMTemplate(); $tpl->render('hello', $data);

Auf die Variablen “user” und “date” können wir jetzt in dem Template “hello.tpl” zugreifen.

<body>
    <p>Hello {$user}! Date: {$date}</p>
</body>

Der Aufruf der Seite „index.php” gibt nun „Hello Coding Pioneers! Date: …“ aus. Zu sehen ist, dass das Datum noch unformatiert ist. Auch dazu bietet Smarty eine Lösung: Formatierung von Variablen.

Formatierung von Variablen

Smarty ermöglicht neben der einfachen Übergabe auch die Formatierung der übergebenen Variablen durch sogenannte Modifier, welche durch eine Pipe (|) angegeben werden. Zum Beispiel verwenden wir an unserer Variablen {$user} den Modifier „upper“, welcher den Inhalt in Großbuchstaben darstellt. Auch das Datum lässt sich mit Hilfe eines Modifiers formatieren. Ändern wir also unser Template „hello.tpl“ und verwenden die zuvor beschriebenen Modifier.

<body>
    <p>Hello {$user|upper}! Date: {$date|date_format:"%d %B"}</p>
</body>

Es ist zu sehen, dass der User-Name in Großbuchstaben dargestellt wird und das Datum formatiert ist. Neben dem Aufruf eines einzelnen Modifiers lassen sich diese durch {$var|mod1|mod2|mod3} kombinieren. Eine Liste aller in Smarty verfügbaren Modifier befindet sich in der ausführlichen Doku ab Seite 40. Reichen die rund 20 Modifier nicht aus, ermöglicht Smarty das Erstellen eigener Modifier.


Eigene Variablenmodifier

Für einen eigenen Variablenmodifier muss lediglich eine PHP-Funktion geschrieben werden. Dazu erstellen wir in dem Ordner „smarty/plugins“ die Datei „smarty_modifier_minimize“. Die Funktion minimiert HTML-Code, indem unnötige Leerzeichen, Umbrüche und Tabstopps entfernt werden.

function smarty_modifier_minimize($input) {
  // Input Kürzen   // s+ - mehrere Whitespaces aufeinanderfolgend   // \n - Zeilenumbruch   // \r - Zeilenumbruch   // \t - Tab
  $input = preg_replace('/^\s+|\n|\r|\t$/m', '', $input);
  /* Kommentare entfernen */   $input = preg_replace('//sU', '', $input);
  return $input; }

Auf dem aktuellen Standpunkt unserer Beispielanwendung können wir diesen Modifier noch nicht sinnvoll verwenden, da wir uns bisher nur mit dem Übergeben von Variablen mit einfachen Werten beschäftigt haben. Den Minimize-Modifier werden wir verwenden, nachdem wir uns die Layouts in Smarty näher angeschaut haben.

Layouts

Smarty verfügt über die Möglichkeit Templates zu laden, diese als Text zurückzugeben und in einer Variablen zu speichern. Diese Methode ermöglicht es, die Struktur der Seite in einem Template zu speichern und den dynamischen Inhalt an die gewünschte Position im Layout zu laden. Als erstes erstellen wir im Hauptverzeichnis des Projekts einen Ordner „layouts“ und in diesem Ordner ein neues Template mit dem Namen „page.tpl“ und den im folgenden Code dargestellten Inhalt. Die Smarty-Variable, welche ein Template als Text beinhalten soll, erhält zur Unterscheidung von normalen Variablen zwei Unterstriche. An dieser Stelle lässt sich der zuvor erstellte Variablenmodifier „minimize“ verwenden um die Inhaltsseite zu minieren.

<html>
     <head>
         <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
         <title>Home</title>
     </head>
  <body>   <header>   <h2>Header Content</h2>   <hr />   </header>
  <div>   {$__content|minimize}   </div>
  <footer>   <hr />   <h2>Footer Content</h2>   </footer>   </body> </html>

Im Ordner „views“ erstellen wir zwei neue Templates „start“ und „about“, welche die dynamischen Inhalte repräsentieren, mit folgendem Inhalt:

start.tpl

<h1>Start<h1/> <p> Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean aliquet dignissim diam at vulputate. Aenean nec ligula ac dolor fringilla pharetra. Cras in augue ac tellus dictum pellentesque. Integer elementum tempus lectus, non rutrum sem viverra a. Sed tincidunt sollicitudin dolor, ut blandit magna auctor non. </p>
about.tpl
<h1>Start<h1/> <p> Maecenas sed nibh felis. Donec dictum porta ante at faucibus. Morbi massa tellus, pulvinar id porta id, imperdiet vel nibh. Donec lectus nulla, porttitor a tempor id, cursus vitae leo. </p>

Im nächsten Schritt muss der Pfad zu dem Layout-Ordner in der Smarty-Engine konfiguriert werden. Dazu fügen wir zunächst ein neues Element in das smtemplate_config-Array.

$smtemplate_config =
 array(
      'layouts_dir' => 'layouts',
      'template_dir' => 'views/',
      'compile_dir' => 'lib/smarty/templates_c/',
      'cache_dir' => 'lib/smarty/cache/',
      'config_dir' => 'lib/smarty/config/',
);

In der SMTemplate-Klasse müssen wir den Layout-Ordner als weiteren Template-Ordner angeben, wobei zu beachten ist, dass Views und Layouts nicht den gleichen Namen besitzen dürfen, da Smarty nicht zwischen View- und Layout-Templates unterscheiden kann. Als Lösung des Problems können die Präfixe „view_“ und „layout_“ verwendet werden. Als nächstes muss die Render-Funktion angepasst werden, sodass sie eine View als Text lädt und diese in dem Layout „page“ darstellt. Um mit Smarty ein Template zu laden und dieses als Text zurückzugeben anstatt darzustellen, wird die Funktion „fetch“ verwendet und mit der bekannten Funktion „assign“ an die Smarty-Variable „__content“ übergeben. Das Layout „page“ wird wie zuvor mit der Funktion „display“ dargestellt.

function __construct(){
     parent::__construct();
         
     global $smtemplate_config;
     $this->template_dir = $smtemplate_config['template_dir'];
     $this->addTemplateDir($smtemplate_config['layouts_dir']);
     $this->compile_dir = $smtemplate_config['compile_dir'];
     $this->cache_dir = $smtemplate_config['cache_dir'];
     $this->config_dir = $smtemplate_config['config_dir'];
}
function render($template, $data = array(), $layout = 'page') {   foreach($data as $key => $value){   $this->assign($key, $value);   }
  $content = $this->fetch($template . '.tpl');   $this->assign('__content', $content);   $this->display($layout . '.tpl'); }

Um den Seiteninhalt in dem Layout darzustellen, wird in der „index.php“ wie gewohnt die Render-Funktion mit dem Inhaltstemplate „start“ aufgerufen.

$tpl = new SMTemplate();
$tpl->render('start');
Einfache Startseite mit Smarty
Einfache About-Seite mit Smarty

Der Aufrufe von “index.php” liefert folgende Ausgabe, indem unser „start“-Template in dem Layout „page“ dargestellt wird:



Wird das Template in „about“ geändert, ist zu erkennen, dass sich der Inhalt bei gleichbleibendem Layout geändert hat.



Header und Footer

Auf einer Webseite sind Header und Footer meistens auf jeder Seite identisch. Bei einem Layout für einen Blogbeitrag und das Impressum ändert sich oft an Header und Footer nichts außer dem Titel der Seite (<head><title>Titel</title></head>). Aus diesem Grund ist es sinnvoll, Header und Footer in Templates auszulagern. In Smarty können Templates durch den Befehl „include“ in andere Templates eingebunden werden. Des Weiteren besteht die Möglichkeit, dem Template, welches mit Hilfe von „include“ eingebunden wird, eine Variable als Attribut zu übergeben, wodurch sich z.B. ein sich ändernder Seitentitel realisieren lässt. Der Befehl {include file=“header.tpl“ title=“Start“} entspricht dem Einbinden eines Headertemplates und der Übergabe des Wertes „Start“ an die Variable {$title}. Um das beschriebene Vorgehen in der Praxis umzusetzen, erstellen wir die Templates „header.tpl“ und „footer.tpl“ im Ordner „layouts“.

header.tpl
<html>   <head>   <meta http-equiv="Content-type" content="text/html; charset=utf-8" />   <title>{$title|default:"Title"} | Smarty Tutorial</title>   </head>
  <body>   <header>   <h2>Header Content</h2>   <hr />   </header>
  <div>   footer.tpl   </div>
  <footer>   <hr />   <h2>Footer Content</h2>   </footer>
  </body>
</html>

Als nächstes erstellen wir ein neues Seitenlayout „page-head-foot“, binden die zwei Templates ein und übergeben dem Header die Variable $title.

{include file="../layouts/header.tpl" title="$title"}
     {$__content|minimize}
{include file="../layouts/footer.tpl"}

Danach muss die Funktion “render” angepasst werden, sodass sie den Seitentitel an die Smarty-Engine übergibt.

function render($template, $data = array(), $layout = 'page', $pagetitle = '') {
     foreach($data as $key => $value){
         $this->assign($key, $value);
     }
  $content = $this->fetch($template . '.tpl');   $this->assign('__content', $content);   $this->assign('title', $pagetitle);   $this->display($layout . '.tpl'); }

In der Datei „index.php“ muss der Aufruf des „start“-Templates durch den Seitentitel und das neue Layout ergänzt werden.

$tpl = new SMTemplate();
$tpl->render('start', $data, 'page_head_foot', 'Start');
Einfache Startseite mit Smarty

Der Aufruf der Seite „index.php” stellt den inkludierten Header mit Titel, den Inhalt des Templates „start” sowie den Footer dar.



Wiederkehrende Elemente

Die zuvor beschriebene Funktion kann ebenfalls für wiederkehrende Elemente verwendet werden. Zum Beispiel hat jede Seite einen Seitentitel (<h1>), welcher zwecks einheitlichem Erscheinungsbild der Webseite immer das gleiche Layout aufweisen sollte. Dazu erstellen wir ein Template im Ordner „layouts“ mit dem Namen „pagetitle“ und folgendem Inhalt:

<h1 style="color: red;">{$pagetitle|default:"Pagetitle"}</h1>

Dieses Template inkludieren wir in dem Inhaltstemplate „start”, um den Seitentitel jederzeit individuell, bei gleichbleibendem Layout, anpassen zu können.

{ include file="../layouts/pagetitle.tpl" pagetitle="Startpagetitle" }
<p> Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean aliquet dignissim diam at vulputate.
Aenean nec ligula ac dolor fringilla pharetra. Cras in augue ac tellus dictum pellentesque.
Integer elementum tempus lectus, non rutrum sem viverra a. Sed tincidunt sollicitudin dolor, ut blandit magna auctor non. </p>
Einfache Startseite mit wiederkehrenden Elementen

Ein erneuter Aufruf der Datei “index.php” zeigt, dass sich der Titel in “Startpagetitle” geändert hat und rot dargestellt wird, welches zeigt, dass das „pagetitle“-Template verwendet wird.

War das schon alles?

Wie durch die aufgezeigten Möglichkeiten von Smarty zu sehen ist, lässt sich durch die Verwendung von Smarty das Design einer Webanwendung einfach von dem funktionalen Quellcode trennen. Smarty bietet weitaus mehr Funktionen als die hier dargestellten, welche ich im Rahmen dieses Tutorials nicht weiter ausführen möchte. Der weitere Funktionsumfang umfasst unter anderem Schleifen, IF-Abfragen, weitere Plugins (nicht nur Variablenmodifier) sowie das Caching der geladenen Seiten. Eine ausführliche Dokumentation zu Smarty, in der die zuvor genannten Funktionen näher beschrieben sind, befindet sich auf der offiziellen Smarty-Seite.


Quellen

Share- & Like-Buttons selber hosten und designen

Die Share- und Like-Buttons von Google, Facebook, Xing, Twitter, Linkedin und all den anderen sozialen Netzwerken sind mit kleineren Hürden schnell auf der eigenen Webseite integriert. Was ist das Problem? – Die Buttons laden langsam, sind datenschutzrechtlich bedenklich, sehen alle unterschiedlich aus und passen – egal wie man es dreht und wendet – nicht so recht ins Design. Eine andere Lösung muss her!

Layout / Designprobleme

Zunächst einmal das Offensichtliche: Unstrittig ist, dass die Buttons (ob von Google, Facebook, Xing, Twitter, Linkedin oder wem sonst) nur mit einer begrenzten Anzahl an Layoutentwürfen daherkommen. Daher passen diese zumeist auch nicht oder nur begrenzt in das eigene Webdesign von 2015. Ein weiteres Design-Problem ist, dass die Anbieter sich nur begrenzt auf ähnliche Layouts einigen konnten. Ob Hintergrund mit oder ohne Verlauf, der Border-Radius oder elementare Dinge wie die pixelgenaue Höhe – deprimierend.

Ladehemmungen

Mithilfe der nativen Integration werden die Buttons mit mehreren Aufrufen von den Seiten des Anbieters geladen. Für Twitter, Facebook und Xing mit 19 Requests dauert dies ca. 1-2 Sekunden und bedeutet 19 Ressourcen, die geladen werden müssen. Das sind mehr Elemente als die gesamte Webseite bzw. ein einzelner Blogbeitrag in Summe hat.

Bei mäßigem Surfverhalten und ausgehend von der Tatsache, dass nicht jede Webseite Share-Icons anbietet, müssen zumeist DNS-Anfragen vorab getätigt werden. Das kostet zusätzlich Zeit. Da nicht jeder Dienst zu jeder Tageszeit gleichermaßen performant ist oder hin und wieder auch mal komplett hängt, legt man das Design der Seite – mit der Nutzung eines Share-Icons – in die Hand des jeweiligen Anbieters.

Datenschutzrechtlich bedenklich

Löblich und gesetzeskonform ist, wenn in Ihren Datenschutzbestimmungen die Integration der einzelnen Services erwähnt ist. Aber: Hand aufs Herz. Haben Sie diese Bestimmungen schon einmal gelesen und verstanden? Sind diese Bestimmungen aktuell und nach geltendem Recht?


Wenn Sie diese Frage mit einem klarem „Ja“ beantworten können: Chapeau!
Ansonsten geht es Ihnen ungefähr wie mir: obwohl ich mich mit der Thematik des Datenschutzes ausführlich beschäftige, kann ich nicht mit absoluter Sicherheit sagen, ob dem Landesrecht, dem deutschem Recht oder/und europäischem Recht damit genüge getan wird. Dispute mit den einzelnen Konzernen gibt es zur Genüge und eine besondere Affinität zum Datenschutz hat auch keiner dieser Konzerne, schließlich sind Daten deren Geschäftsgrundlage. De facto holt man sich eine oder mehrere Datenkraken auf die eigene Webseite.

Wir Webmaster legen in diesem Moment die Verantwortung in die Hand unserer Seitennutzer. Diese müssen proaktiv und vorab entsprechende Ressourcen blockieren, ohne zu wissen, dass diese auf unserer Seite sind. Die im Datenschutz geforderte informationelle Selbstbestimmung ist damit (ohne das nun weiter auszuführen) mehr Schein als Sein.

Die Lösung

Damit wir Webmaster auch wieder Herr der Daten werden, ist es erforderlich, dass die Daten (wie die Anzahl der Likes oder Shares) selbst erhoben werden. Hierhingehend stellen die verschiedenen Dienste APIs bereit, mit denen die Daten gesammelt werden können.

Jeder Anbieter hat zumeist einen Link, über den direkt die URL geteilt werden kann. Diese zwei Techniken bringe ich im Folgenden zusammen.

Zunächst haben ich dabei die Butttons mit einheitlichem Syntax erstellt und gestylt:

HTML-Chunk

<div class="share" style="padding-top:15px"> <span class="share_counter" style="display:none">Interaktionen - Vielen Dank!</span><br><br> <a href="https://www.facebook.com/sharer/sharer.php?u={url}" title="Teile den Beitrag {titel} mit Facebook" class="btn btn-default btn-facebook btn-share"><span class="flaticon-facebook"></span> <span class="text">like it</span></a> <a href="https://twitter.com/home?status={url}" title="Teile den Beitrag {titel} mit Twitter" class="margin-left btn btn-default btn-twitter btn-share"><span class="flaticon-twitter"></span>  <span class="text">tweet it</span></a> <a href="https://plus.google.com/share?url={url}" title="Teile den Beitrag {titel} mit Google +" class="margin-left btn btn-default btn-google btn-share"><span class="flaticon-google"></span>   <span class="text">give it +1</span></a> <a href="https://www.xing.com/spi/shares/new?url={url}" title="Teile den Beitrag {titel} mit XING" class="margin-left btn btn-default btn-xing btn-share"><span class="flaticon-xing"></span> <span class="text">share it</span></a> <a href="mailto:?&subject=Linkempfehlung&body;={url}" title="Teile den Beitrag {title} per E-Mail" class="margin-left btn btn-default btn-mail"><span class="glyphicon glyphicon-envelope"></span>   Weiterleiten</a></div>

CSS-Code

Da wir Bootstrap und Favicons von flaticon.com einsetzen, ist der Aufwand nur marginal

.btn-default {
    font-family: 'League Gothic', sans-serif;
    background: none;
    font-size: 24px;
    color: #333;
    border-color: #666;
    transition: all 0.3s ease;
}
.btn-default:hover {background: #333; color: #fff;}
.btn-default:focus {outline:0;}
.btn-xing:hover { color: #338383; border-color: #338383; background: none; }
.btn-xing { background: #338383; border-color: #338383; color: #fff; }
.btn-google:hover { color: #db4a39; border-color: #db4a39; background: none; }
.btn-google { background: #db4a39; border-color: #db4a39; color: #fff; }
.btn-twitter:hover { color: #00acee; border-color: #00acee; background: none; }
.btn-twitter { background: #00acee; border-color: #00acee; color: #fff; }
.btn-facebook:hover { color: #3b5998; border-color: #3b5998; background: none; }
.btn-facebook { background: #3b5998; border-color: #3b5998; color: #fff; }
.btn-mail:hover { color: #999; border-color: #999; background: none; }
.btn-mail { background: #999; border-color: #999; color: #fff; }

Javascript

Weiter wird ein wenig JavaScript benötigt, der zunächst die Button-Klicks in Popups aufruft:

    $('a.btn-share').on('click', function(event) {
        // Popup öffnen und Fokus setzen
         fenster = window.open($(this).attr('href'), "Beitrag Teilen", "width=650,height=450,resizable=yes");
         fenster.focus();
         // Normale Link-Click-Aktion unterbinden
        event.preventDefault();
    })

Likes und Shares Laden

Nun können die Likes und Shares asynchron geladen werden. Dies geschieht aus dem Grund, damit keine Probleme auftauchen, falls hier mal etwas hakt oder hängt. Das Script kann im Hintergrund arbeiten, ohne die Funktion im Frontend zu beeinträchtigen.

$.ajax({
data: "url="+window.location,
data : "url="+ encodeURIComponent('http://www.coding-pioneers.com/'), type : 'POST', dataType : 'json', success : function(data) { // Alle Interaktionen setzen und Sichtbarkeit ändern. if (data.all == 0) { $('.share .share_counter').text('Bisher gibt es keine Interaktionen mit dem Beitrag - Teile ihn doch!'); } else { $('.share .share_counter').prepend(data.all + ' '); }
$('.share .share_counter').fadeIn(); // Facebook Interaktionen if (data.facebook != 0) { $('.share .btn-facebook .text').text(data.facebook + (data.facebook == 1 ? ' like' : ' likes')); }
// Twitter Interaktionen if (data.twitter != 0) { $('.share .btn-twitter .text').text(data.twitter + (data.twitter == 1 ? ' tweet' : ' tweets')); }
// Google Interaktionen if (data.google != 0) { $('.share .btn-google .text').text(data.google); } // Xing Interaktionen if (data.xing != 0) { $('.share .btn-xing .text').text(data.xing + (data.xing == 1 ? ' share': ' shares')); } }, error : function(error, msg) { // TODO ggf. Error-Reporting } });

Im Hintergrund übergibt eine JSON-Datei Daten wie die folgenden:

{
  "all": 6,
  "facebook": 1,
  "google":4,
  "twitter": 1,
  "xing": 0,
  "url": "http://www.coding-pioneers.com/"
}


Im Hintergrund sitzt der PHP-Contoller für dieses JSON-Datei:

$url = "http://www.coding-pioneers.com/";
if(isset($_POST['url'])) 
    $url = $_POST['url'];
function twitter($url='') { $json_string = file_get_contents('http://urls.api.twitter.com/1/urls/count.json?url='.$url); $json=json_decode($json_string,true); if($json['count']) return intval($json['count']); return 0; }
function google($url='') { // CURL initiieren & Parameter Setzen $ch=curl_init(); curl_setopt($ch,CURLOPT_URL,'https://clients6.google.com/rpc'); curl_setopt($ch,CURLOPT_POST,true); curl_setopt($ch,CURLOPT_SSL_VERIFYPEER,false); curl_setopt($ch,CURLOPT_POSTFIELDS, '[{"method":"pos.plusones.get","id":"p","params":{"nolog":true,"id":"'.rawurldecode($url).'","source":"widget","userId":"@viewer","groupId":"@self"},"jsonrpc":"2.0","key":"p","apiVersion":"v1"}]');
curl_setopt($ch,CURLOPT_RETURNTRANSFER,true); curl_setopt($ch,CURLOPT_HTTPHEADER,array('Content-type: application/json')); // CURL Resultate in String schreiben und schließen $curl_results = curl_exec($ch); curl_close($ch);
// JSON Parsen $json = json_decode($curl_results,true);
// Rückgabe vorbereiten if($json[0]['result']['metadata']['globalCounts']['count']) return intval($json[0]['result']['metadata']['globalCounts']['count']);
return 0; }
function facebook($url='') { // Auslesen über Facebook Graph $source='http://graph.facebook.com/?id='.$url; $result=json_decode(file_get_contents($source));
// Rückgabe vorbereiten if($result->shares) return intval($result->shares);
return 0; }

function xing($url='') { // CURL initiieren & Parameter Setzen $ch = curl_init(); curl_setopt($ch,CURLOPT_URL, 'https://www.xing-share.com/spi/shares/statistics'); curl_setopt($ch,CURLOPT_POST, 1); curl_setopt($ch,CURLOPT_POSTFIELDS, 'url='.$url); curl_setopt($ch,CURLOPT_RETURNTRANSFER, true);
// CURL Resultate in String schreiben und schließen $curl_results = curl_exec($ch); curl_close($ch);
// JSON Parsen $json = json_decode($curl_results,true);
// Rückgabe vorbereiten if($json['share_counter']) return intval($json['share_counter']);
return 0; }
// Einzelne Interaktionen $interaktionen['twitter'] = twitter($url); $interaktionen['google'] = google($url); $interaktionen['facebook'] = facebook($url); $interaktionen['xing'] = xing($url);
// Summe der Interaktionen $interaktionen['all'] = array_sum($interaktionen);

Fazit

Die Share-Buttons werden deutlich schneller geladen, haben eine deutlich angenehmere Handhabung, passen nun auch ins Bild der Webseite und die Werte der Seite, die durch das Script gewonnen werden, können so auch intern gespeichert werden und sind schneller und vor allem einfacher auswertbar.

Links

Demo der Buttons

Projekt auf Github

Website-Tuning Stage 3: Eine Zusammenfassung

Wie in dem ersten und zweiten Teil der Blogreihe erläutert, kommt es auf optimale, komprimierte und nötige Inhalte an. Viele Ressourcen werden daher nun asynchron geladen, sind komprimiert und zu größeren Teilen minifiziert. Um nun die letzten Prozente herauszukitzeln, haben wir Folgendes getan:

  • HTML minifizieren
  • CSS minifizieren
  • Bilder optimieren
  • SVG komprimieren
  • Google Maps entfernen
  • Webfonts und andere Bibliotheken selber hosten

HTML-Komprimieren und optimieren

Um das HTML zu minifizieren, werden im weitesten Sinne Whitespaces entfernt. Also Zeichen, die während der Entwicklung für die Struktur nötig sind bzw. ungemein helfen. Dem Browser ist es hinterher jedoch weitestgehend egal, wie viele Zeilenumbrüche im Quelltext sind oder wie strukturiert der Quelltext durch die IDE der Wahl eingerückt wurde (durch Tabstops oder mehrere Leerzeichen). Aufgrund dessen werden diese Whitespaces mithilfe eines kleinen Snippets im MODX vor der Ausgabe entfernt:

Des Weiteren wird bei uns zumeist der Quelltext mittels Kommentaren beschrieben, um diesen zu späteren Zeitpunkten auch noch gut verstehen zu können (Dokumentation). Diese Dokumentation ist für den live-Betrieb der Seite nicht erforderlich und wurde ebenso aus dem Quelltext entfernt.

So konnten noch einmal 6 von 19 KB und somit 31% gespart werden.

Weitere Ressurcen optimieren

Das Logo wird als SVG-Grafik eingebunden. Diese ist im weitesten Sinne eine XML-Datei, welche ebenso minifizierbar und komprimierbar ist. Somit wurden die Daten mit dem MIME-Typ „svg+xml/text“ zur nginx-Konfiguration hinzugefügt und die Datei minifiziert.

Google Maps wurde aus der Startseite entfernt. Grund hierfür sind die ca. 45 Requests des Browsers, bis die Seite tatsächlich geladen ist. Das führt dazu, dass einige wichtige Ressourcen parallel mit der Karte geladen werden und sich die Bandbreite teilen müssen und daher langsamer geladen werden. Die Karte braucht durchschnittlich 3-5 Sekunden bei einer guten Internetverbindung, bis keine weiteren Ressourcen mehr nachgeladen werden. Subjektiv betrachtet reagiert in der Zeit der Browser marginal träger, was vermutlich mit der Leistungsfähigkeit des Endgerätes einhergeht. Aufgrund des späten Ladens des CSS gibt es zudem noch ein Darstellungsproblem beim erstmaligen Laden der Seite mit Browsern mit der Webkit-Engine (nur eine halbe Karte wird angezeigt). Netter Nebeneffekt: Die Datenkrake Google bleibt den Besuchern zunächst erspart.

Bilder optimieren

Auf der Startseite von Coding-Pioneers wird ein größeres Bild (Keyvisual) genutzt. Dieses war in der ursprünglichen Größe knapp 1,6 MB groß. In einer kleineren Größe (Breite: 1900 Pixel) hatte es noch 450 KB und macht damit noch rund 50% der Seitengröße (Gesamt: 695 KB) aus. Der Webdienst compressor.io verspricht, Bilder um bis zu 90% zu verkleinern und das ohne Qualitätsverluste. Zu verlieren gab es also nicht viel, wenngleich 90% Verkleinerung nicht erwartet wurden. So konnten am Ende annähernd 100 KB gespart werden (446 KB reduziert auf 345 KB also eine Ersparnis von rund 23%), was also zu einer Gesamtersparnis von etwas über 10% führte.

Was wurde denn nun gespart?

Angefangen haben wir mit einem Prototypen, der in Summe 2,2 MB also grob 2200 KB groß war. Es wurden 70 Ressourcen geladen (zum größeren Teil asynchron durch Google Maps) und das Ereignis „ready“ des DOM wurde nach durchschnittlich 1650 ms ausgelöst.

Durch die Komprimierung müssen von den 930 KB nur 695 KB übertragen werden, das entspricht einer Einsparung von 25%.


Durch das Minifizieren von:

  • HTML: 19 auf 13 KB (31% Ersparnis), keine Ressourceneinsparung
  • CSS: 196 auf 88 KB (55% Ersparnis), durch das Zusammenfassen wurden zwei Dateien eingespart, die weniger geladen werden müssen. Hier konnte auch relativ viel eingespart werden, da die Bibliothek des Bootstrap vom Umfang reduziert werden konnte.
  • JavaScript: 100 KB auf 140 KB, da noch weitere Skripte gebraucht wurden, um andere Möglichkeiten zu eröffnen, dafür wurden zwei Requests gespart. Die Minifizierungsrate fällt hier leider nicht so hoch aus, da die eingebundenen Bibliotheken wie jQuery bereits maximal minifiziert sind.

  • Bilderkomprimierung: Mithilfe von compressor.io konnten 140 KB gespart werden also einer Gesamtersparnis von etwas über 15%.

In Summe haben wir so von den ursprünglichen 2100 KB gute 1400 KB in der Übertragung sparen können. Durch die Optimierung werden nicht mehr 68 Ressourcen geladen, sondern nur noch 20. Das DOM-Ereignis „Content Loaded“ wird nach 240-400ms vom Browser ausgelöst. Ab dem Moment werden die Daten beim Client gerendert und dargestellt. Darauf folgt dann das Ereignis „Ready“ nach durchschnittlich 1,5 Sekunden. Nun werden Funktionen aufgerufen, wie die jQuery-Funktion „ready“ und damit die letzten Operationen ausgeführt.

Fazit: Performance matters

Was sollten Sie also tun, damit die Seite so schnell wie möglich geladen wird? Hier eine kommentierte Liste zum Abarbeiten:

  1. Serverperformance: Suchen Sie sich einen guten Hoster und Webspace mit SSDs, so können Webseiten spürbar schneller bereitgestellt werden. Ein Webspace-Paket für 0,49 Euro / Monat und 2000 anderen Webseiten auf dem gleichen Server helfen nur bedingt. Gerade als Unternehmen spart man hier an der falschen Stelle!
  2. Optimieren Sie Ihre Bilder: Hier steckt mit das meiste Potenzial, um die Seitengröße schnell auf ein sinnvolles Maß zu bringen (Nur weil heutzutage jedes Smartphone kaum mehr Bilder von unter 4 MB erzeugt, müssen diese nicht direkt so auf die Webseite)
  3. Reduzieren Sie vor der Ausgabe Ihren CSS-Code: Zeilenumbrüche und Einrückungen strukturieren den Code zwar für Sie optimal und Kommentare noch einmal ungemein, aber dem Client (Browser) ist das zumeist vollkommen egal.
  4. Gleiches gilt für Ihren HTML-Code: So lassen sich schnell mal ein paar KB sparen.
  5. Für JavaScript gilt grundsätzlich das gleiche: Hier sind Optimierungen jedoch nicht ganz so trivial.
  6. Schalten Sie die Komprimierung (gzip/mod_deflate) ein: Ihre Webseite wird ca. 30% schneller geladen!
  7. Reduzieren Sie die Anzahl der Dateien bzw. fassen Sie Dateien zusammen, die geladen werden: Jede Datei, die gespart wird, spart einen HTTP-Header und blockiert damit keine anderen Ressourcen. Besonders CSS- und JavaScript-Dateien lassen sich gut zusammenfassen. Auch Bilder lassen sich in sog. Sprites zusammenfassen.
  8. Vermeiden Sie verschiedene Dateiquellen und laden Sie möglichst alles von einem Host. Auch wenn man Webfonts vom Anbieter A, jQuery vom CDN B und andere Bibliotheken von Anbieter C einbindet, sorgt das dafür, dass im Worst-Case drei DNS-Anfragen vor dem vermeidlich schnelleren Laden getätigt werden. Das blockiert den Browser kurzfristig und erhöht zudem die Anzahl der Fehlerquellen.

Wofür das Ganze? Sind wir schon fertig?

Die Seite wird nun insgesamt spürbar schneller geladen, was vor allem den mobilen Nutzern zugute kommt. Google hat hier für die Analyse das Produkt „PageSpeed Insights“ bereitgestellt. Hier räumen wir mit der aktuellen Seite 100 von 100 möglichen Punkten im Desktop-Bereich ab PageSpeed Insights von coding-pioneers.com

An einigen Ecken hakt es noch und an anderen Ecken liegt noch einiges an Potential. Hier wird es azyklisch immer mal wieder kleine Ergänzungen geben und wir werden diese mit Code-Snippets für Sie zur Verfügung stellen! Sie haben Fragen, Anmerkungen oder Wünsche – Wir freuen uns über Kommentare!

Links

Website-Tuning Stage 2: JS zusammenfassen & komprimieren

Im WWW ist sie kaum noch wegzudenken: Die Skriptsprache JavaScript. Sie dient unter anderem zur Dynamisierung der Inhalte einer Website. Der JS-Quellcode wird im HTML zwischen den script-Tags eingebunden oder dort mittels Attribut aus einer externen Datei geladen:

<script src="script.js"></script>

Wie bereits im letzten Teil der Serie erklärt wurde, ist es wichtig, dass so wenig Dateien wie möglich eingeladen werden. Das gilt auch für den eingebundenen JavaScript-Code. Die Ziele sind folgende:

  • Skriptdateien zusammenfassen
  • Quellcode minifizieren

Skriptdateien zusammenfassen

Auf einer Website können mehrere Skript-Dateien eingebunden werden. Wird eine Website in einem Web-Browser aufgerufen, läd dieser in der Regel 6 Dateien gleichzeitig. Dabei werden neben dem Skript auch Bilder, CSS-Dateien und weitere Elemente der Seite geladen. Deswegen ist es sinnvoll, die Anzahl der eingebundenen Dateien möglichst gering zu halten. Dies kann durch das Zusammenfassen aller erforderlichen Skript-Dateien zu einer Datei umgesetzt werden.

Quellcode minifizieren

Das weitere Vorgehen besteht in der Komprimierung der zusammengefassten Skript-Datei. Quellcode enthält oft für den Interpreter unwichtige Zeichen wie Zeilenumbrüche, Tabstopps und Kommentare. Diese Zeichen dienen nur dem Verständnis sowie der Förderung der Lesbarkeit und können daher restlos aus dem Quellcode entfernt werden. Durch diese Operation lässt sich die Größe der Skript-Datei um ein Vielfaches verkleinern, wodurch die Ladezeit der Datei verringert wird.

Folgende Elemente können restlos entfernt werden:

  • Zeilenumbrüche
  • Tabstops
  • Kommentare
  • Zeilenumbrüche
  • Aufeinander folgende Leerzeichen

MODX Snippet

Durch das folgende MODX-Snippet „minimize-js“ kann die Performance beim Laden das JavaScripts mit den zuvor beschriebenen Operation simpel verbessert werden. Der Aufruf des Snippets erfolgt durch [[minimize_js? ]]. Durch das Snippet werden die übergebenen Dateien zusammen gefasst und komprimiert. Da es sich um eine simple Komprimierung handelt, führen einzeilige If-Abfragen, welche einen Zeilenumbruch enthalten, zu einer fehlerhaften Ausgabe.

Parameter

NameBeschreibung
srcFilesPfade zu den JavaScript-Dateien, welche zusammengefasst und komprimiert werden sollen. Pfade werden durch ein Komma getrennt.

Quellcode des MODX-Snippets

<?php<br>
<br>
$srcFiles = $modx->getOption('srcFiles', $scriptProperties);<br><br>

/* Leerzeichen entfernen */<br>
$srcFiles = str_replace(" ", "", $srcFiles);<br><br>

/* Parameter aufsplitten */<br>
$params = explode(",",$srcFiles);<br><br>

$output = "";<br><br>

/* Source-Dateien optimieren */<br>
foreach($params as $file)<br>
{<br><br>

  /*Dateiinhalt auslesen */<br>
  $string = file_get_contents($file);<br><br>

  /* Einzeile Kommentare entfernen */<br>
  $string = preg_replace("/[^:](\/\/.*)/", "\n", $string);<br><br>

  /* Mehrzeilige Kommentare entfernen */<br>
  $string = preg_replace("/(\/\*.*\*\/)/s", "\n", $string);<br><br>

  /* carriage returns durch Zeilenumbruch ersetzen */<br>
  $string = str_replace("\r", "\n", $string);<br><br>

  /* tabs durch Leerzeichen ersetzen */<br>
  $string = str_replace("\t", "\n", $string);<br><br>

  /* Mehrere Zeilenumbrueche durch einen ersetzen */<br>
  $string = preg_replace("/\n+/", "\n", $string);<br><br>

  /* Zeilenumbrueche an schließenden Klammern durch Semikolons ersetzen */<br>
  $string = preg_replace("/[)]\n/", ");", $string);<br><br>

  /* Entfernt das Semikolon an If-Abfragen und Schleifen */<br>
  $string = str_replace(");{", "){", $string);<br><br>

  /* Entfernt das Semikolon am Punkt-Operator */ <br>
  $string = str_replace(");.", ").", $string);<br><br>

  /* Entfernt das Semikolon an zwei aufeinander folgenden runden Klammern*/<br>
  $string =preg_replace("/[);]\s+[);]/", ")", $string);<br><br>

  /* Restliche Zeilenumbrüche entfernen*/<br>
  $string = preg_replace("/\n/", "", $string);<br><br>

  /* Mehrere Leerzeichen durch ein Leerzeichen ersetzen */<br>
  $string = preg_replace("/\s+/", " ", $string);<br><br>


  $output = $output.$string;<br>
}<br>
<br>
return $output;<br>
?>

Verwendung

Nachdem mit dem zuvor beschriebenen Quellcode das MODX-Snippet erstellt wurde, muss dieses in die Website eingebunden werden. Dazu wird eine neue Ressource vom Content-Type „javascript“ erzeugt und das Snippet mit

[[minimize_js? &srcFiles=`js/script1.js,js/script2.js,js/script3.js`]]

eingebunden. Zu beachten ist, dass der Aufruf des Snippets ohne ein Ausrufungszeichen nach der zweiten öffnenden eckigen Klammer erfolgt, damit das Snippet gecacht wird. Durch das Cachen des Snippets wird zusätzliche Ladezeit gespart, da die Datei nicht neu geladen werden muss.

Die zuvor erstellte Ressource wird im Footer durch

<script src="ressourcen-name.js"></script>

Fazit

Durch die Verwendung des MODX-Snippet konnten wir die Größe unserer JS-Datei um 60% reduzieren. Hier bewegen wir uns ebenfalls im KB-Bereich. Dank moderner Datenübertragungstechniken spielt das Einsparen von KBs in der heutigen Zeit kaum noch eine Rolle. Aber auch hier gilt: „Serverentlastung bei Lastzeiten“ und „VDSL, Glasfaser und LTE sind nicht immer verfügbar“.

Bei dieser Methode der Komprimierung handelt es sich um ein simples Verfahren, welches eine Alternative zu externen Programmen, wie zum Beispiel dem Closure Compiler von Google, liefert. Durch das MODX-Snippet bleibt die gesamte Kontrolle bei dem Programmierer der Website und der Quellcode wird nicht an eine externe Schnittstelle gesendet.

Website-Tuning Stage 1: Code validieren und optimieren

Das WWW – wie wir es heute kennen – hat eine gewisse Entwicklung hinter sich. Durch diese ist letztlich auch HTML bzw. HTML5 entstanden, da es vorher keine Standards für die strukturierte Übertragung von digitalen Informationen zwischen mehreren Personen gab.

Zur Historie von HTML: Die Hypertext Markup Language gehört zur Klasse der sog. Auszeichnungssprachen, die einen Text semantisch strukturiert und es erstmals Ende 1992 ermöglichte Informationen spezifiziert über Ländergrenzen hinweg zu übertragen. Heutzutage wird HTML vom W3C und der WHATWG weiterentwickelt. Die derzeitige aktuellste Spezifikation ist HTML5, die bereits von den meisten Webbrowsern unterstützt wird.

Wenn nun entsprechend dieser Spezifikation der Quellcode geschrieben wird, so besteht auch eine größere Chance, dass dieser dann auf allen Endgeräten gleichartig interpretiert und angezeigt wird.

HTML5 validieren

Damit der Quellcode einer Webseite entsprechend der Spezifikation des W3C geschrieben wird, bzw. das Ergebnis sich prüfen lässt, gibt es vom W3C einen sog. Validatior. Werden hier Fehler oder Warnungen ausgegeben, so können diese korrigiert werden. Ein bekannter Validator ist unter folgender Domain zu finden:

html5.validator.nu

CSS validieren

Weiter verhält es sich mit dem CSS genauso wie mit dem HTML, hier wird auch ein Validator von W3C bereitgestellt:

jigsaw.w3.org/css-validator

Es fällt auf, dass hier etliche Fehler gefunden werden und der Quellcode angeblich alles aber eben nicht valide ist. Das liegt daran, dass CSS3 noch nicht spezifiziert ist (und damit kein Standard, der sich prüfen ließe) und bisher nur ein Entwurf ist, der permanent erweitert wird und dann durch die Browser-Engines implementiert wird. Dadurch gibt es auch die verschiedenen Browser-Präfixe (wie bspw. „-webkit“).

Nichtsdestotrotz lassen sich so einige Fehler im Stylesheet erkennen und bereinigen.

HTML5 und CSS optimieren

Damit das CSS und HTML so schnell wie möglich angezeigt werden, ist es wichtig, dass dieser Quellcode so schnell wie möglich geladen wird. Daraus folgt, dass zum einen der HTML und CSS-Code immer so klein wie möglich sein sollten und zum zweiten, dass auch nur das übertragen wird, was auch wirklich gebraucht wird.


Folgendes ist empfehlenswert:

  • Kommentare Entfernen
  • Kommentare aus Scripts und Styles entfernen
  • Entfernen von CDATA-Sektionen von Skripten und Styles
  • Überflüssige Leerzeichen und Zeilenumbrüche entfernen
  • Entfernen von leeren Attributen (class=„“)
  • Entfernen von leeren Elementen
  • URLs relativ angeben (URLs minifizieren)

Texte per HTTP-Kompression verkleinern

HTTP-Kompression, die von Servern und Webbrowsern unterstützt wird, dient der Verkürzung der Übertragungsgeschwindigkeit und der Optimierung der Bandbreitenausnutzung.

Komprimierbare Ressourcen (wie Textdateien also vor allem HTML, CSS und Javascript) auf einer Webseite, die mittels HTTP-Komprimierung bereitgestellt werden, sorgen dafür, dass sich bis zu 90% des benötigten Datenvolumens bei der Übertragung einsparen lassen. Viele Webserver können Dateien vor dem Senden mittels gzip komprimieren. Dies geschieht durch Aufrufen eines Drittanbietermoduls oder mithilfe integrierter Routinen. Dadurch können für das Rendern Ihrer Website erforderliche Ressourcen signifikant schneller heruntergeladen werden.

Bei anderen Ressourcen wie z.B. JPEG-Bildern lohnt diese Vorgehensweise nicht, da das Bild selber schon komprimiert ist.

HTTP-Kompression (gzip) im nginx-Webserver aktivieren

Aus allgemeinen Performancegründen arbeiten wir mit dem Webserver nginx. Dieser liefert die statischen Dateien deutlich schneller als ein Apache Webserver aus, sodass dieser hierhingehend konfiguriert werden muss. Die folgende Konfiguration beinhaltet die gesamte nginx-Konfiguration für das MODX-CMS:


location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {<br>
    expires 30d;<br>
    add_header Pragma public;<br>
    add_header Cache-Control "public, must-revalidate, proxy-revalidate";<br>
    try_files $uri = @fallback;<br>
<br>
    if (!-e $request_filename) {<br>
        rewrite ^/(.*)$ /index.php?q=$1 last;<br>
    }<br>
}<br>
<br>
gzip on;<br>
gzip_proxied any;<br>
gzip_types  application/javascript application/json application/xml application/xml+rss application/x-javascript text/css text/javascript text/plain text/xml;<br>
gzip_vary on;

Fazit

Alleine durch diese kleinen Optimierungen konnte der Textanteil der übertragenen Daten von ca. 250 kB auf rund 70 kB, also um über 70% gesenkt werden. Diese läppischen 180 kB sind zwar in Zeiten von VDSL, Glasfaser und LTE kaum der Rede wert, entlasten aber dennoch gerade in Lastzeiten den Server enorm und auch einen geneigten Interessenten, der eben doch nur über EDGE oder GPRS mit dem Internet verbunden ist.