Property-System
===============

Properties sind die Eigenschaften, die einem Artikel als Metainfos, einem
Benutzer als Attribut oder einem Produkt in varisale als Attribut gegeben werden
können. Sie enthalten die schematische Beschreibung der Eigenschaft, wie den
Titel, den internen Namen, den :doc:`Datentyp <datatypes>` und dessen Parameter
sowie weitere, AddOn-spezifische Angaben.

Beginnend mit Sally 0.4 werden Properties nicht mehr in der Datenbank, sondern
in YAML-Dateien definiert. Damit können sich die meisten AddOns die
umfangreichen Quellcodes für das Aufbauen und Parsen der Formulare sparen.
Immerhin werden Properties vom Integrator festgelegt und ändern sich im
Live-Betrieb nicht mehr (im Gegensatz zu ihren **Werten**, die sich natürlich im
Backend ändern lassen sollten).

AddOns, die Properties benutzen, bringen immer auch eine eigene API zum Zugriff
auf diese mit. Die folgenden Beschreibungen sind daher für Autoren von AddOns
interessant, nicht für Entwickler, die sie nutzen.

Properties definieren
---------------------

Für gewöhnlich wird ein AddOn seine Properties in der :file:`globals.yml`
speichern. Die dort definierten Inhalte werden in den Root der Konfiguration
gemerged und stehen somit über ``sly_Core::config()->get(...)`` zur Verfügung.

Der mitgelieferte, abstrakte ``WV_Service_Property`` sollte zum Laden verwendet
werden. Er wird automatisch Änderungen an der Struktur erkennen und die in ihm
definierten, abstrakten Hooks aufrufen. Ein AddOn sollte also einen Service von
der Basis ableiten und diese Methoden definieren:

.. sourcecode:: php

  <?
  class MyPropService extends WV_Service_Property {
    protected function onNew(WV_Property $property) {
      // Code, der beim Auftreten einer neuen Property ausgeführt wird.
    }

    protected function onDeleted(WV_Property $property) {
      // Code, der beim Löschen einer Property ausgeführt wird.
    }

    protected function onChangedDatatype(WV_Property $oldVersion, WV_Property $newVersion) {
      // Code, der bei Änderung des Datentyps ausgeführt wird
    }

    protected function onChangedParameters(WV_Property $oldVersion, WV_Property $newVersion) {
      // Code, der bei Änderung der Parameter ausgeführt wird
    }

    protected function onChanged(WV_Property $oldVersion, WV_Property $newVersion) {
      // Code, der bei sonstigen Änderungen ausgeführt wird
    }

    protected function createProperty($name, array $property) {
      return new MyPropertyClass($name, $property);
    }
  }

In den Methoden können dann die eigentlichen Daten aktuell gehalten werden.
``WV_Property`` ist eine Basisimplementierung, die bereits die wichtigsten
Eigenschaften kapselt und als Basis für eine eigene Property-Klasse
(``MyPropertyClass`` im obigen Beispiel) benutzt werden sollte.

Beispiel
^^^^^^^^

Wir nehmen folgende globals.yml im AddOn "test" an:

.. sourcecode:: yaml

  testaddon:
     foo:
        title: Meine Property
        helptext: Hilfetext, der unter dem Eingabeelement erscheint.
        datatype: stringline
     bar:
        title: Noch eine Eigenschaft
        datatype: stringtext

Die Properties können wie folgt ausgelesen werden:

.. sourcecode:: php

  <?
  $service    = new MyPropService();
  $data       = sly_Core::config()->get('testaddon');
  $properties = $service->loadFromArray($data);

  /*
  $properties = Array(
    'foo' => WV_Property{
       title:protected => 'Meine Property',
       ...
     },
    'bar' => ...
  )
  */

Komplexe Hilfetexte
-------------------

Seit Changeset 27b729b953fc_ unterstützen die Developer Utils neben den reinen
Strings für Hilfetexte (siehe obiges Beispiel) auch eine erweiterte Variante.
Dabei kommt ein Callback zum Einsatz, dessen Aufgabe es ist, den Hilfetext
direkt an dem jeweiligen Formularelement zu setzen.

.. _27b729b953fc: https://bitbucket.org/webvariants/developer-utils/changeset/27b729b953fc

.. warning::

  Es handelt sich hierbei **nicht** um ein Feature von Sally. Die normale
  Formular-API von Sally (``sly_Form_ElementBase->setHelpText()``) hat von der
  ganzen Geschichte keine Kenntnis.

Integration
^^^^^^^^^^^

Die Verarbeitung der erweiterten ``helptext``-Angabe ist von Datentypen
abhängig. Nur Datentypen, die das Format explizit unterstützen, können Callbacks
auswerten.

``WV_Datatype`` steht die statische Methode ``setHelpTextOnElement()`` zur
Verfügung, die die ganze Logik der Callbacks implementiert. Üblicherweise wird
man diese Methode in der ``renderForm()`` des Datentyps aufrufen wollen, nachdem
man das entsprechende Formularelement erzeugt hat. Im ``boolean``-Datentyp sieht
das beispielsweise wie folgt aus:

.. sourcecode:: php

  <?
  class _WV_Datatype_Boolean extends _WV_Datatype_Base implements WV_IDatatype {
    public function renderForm(sly_Form $form, WV_IProperty $property, $value) {
      // (some stuff)

      $checkbox = new sly_Form_Input_Checkbox('property_'.$property->getID().'_value', $property->getTitle(), '1', $description);
      $checkbox->setMultilingual($property->isMultilingual());

      // here is where the magic happens
      WV_Datatype::setHelpTextOnElement($form, $checkbox, $property);

      $form->add($checkbox);
      return $form;
    }

    // ...
  }

Konfiguration
^^^^^^^^^^^^^

Eine minimale Konfiguration sieht wie folgt aus:

.. sourcecode:: yaml

  testaddon:
     foo:
        title: 'Meine Property'
        datatype: 'stringline'
        helptext:
           callback: ['MyClass', 'myStaticMethod']  # typisches Format für Callbacks
           params: {}

Obige Konfiguration definiert, dass für den Hilfetext die Methode
``MyClass::myStaticMethod()`` ausgeführt wird. Ihm werden abgesehen von den
Standard-Parametern (siehe unten) keine weiteren Parameter mitgegeben.

In vielen Fällen wird man noch weitere Daten übergeben wollen. Dies könnte wie
folgt aussehen (die gesamte Struktur der ``params``-Angabe im YAML ist vom
Callback abhängig und kann frei gestaltet werden).

.. sourcecode:: yaml

  testaddon:
     foo:
        title: 'Meine Property'
        datatype: 'stringline'
        helptext:
           callback: ['MyClass', 'myStaticMethod']  # typisches Format für Callbacks
           params:
              myparam: 'wert'
              alist: [1,2,3]
              evenmore:
                 - foo
                 - bar

Alle definierten Parameter werden 1:1 dem Callback übergeben.

Der Callback
^^^^^^^^^^^^

Ein Callback ist (wie der Name schon sagt) ein beliebiges PHP-Callable, also
Klassenmethode oder globale Funktion. Ihm werden immer die folgenden vier
Parameter übergeben:

``$property``
  Das zu rendernde Property selbst.

``$form``
  Das ``sly_Form``-Objekt, indem das Formularelement gerendert werden soll.

``$element``
  Das Formularelement, das vom Property gerendert wird. Die grundlegenden
  Methoden von ``sly_ElementBase`` sind immer verfügbar.

``$params``
  Die zusätzlich definierten Parameter aus dem YAML.

Ein Callback, der obige Konfiguration auswertet, könnte wie folgt aussehen:

.. sourcecode:: php

  <?
  class MyClass {
     public static function myStaticMethod(WV_Property $prop, sly_Form_ElementBase $element, sly_Form $form, array $params) {
        print_r($params);
        /*
        Array
        (
          'myparam' => 'wert',
          'alist' => Array(1,2,3),
          ...
        )
        */

        // (here some magic happens and you decide what the help text should be)

        $helptext = doMagic();

        // Der Hilfetext muss nicht zurückgegeben, sondern direkt am Element
        // gesetzt werden.

        $element->setHelpText($helptext);

        // Auf Wunsch kann auch HTML erlaubt sein (dann wird $helptext raw ausgegeben)
        // $element->setHelpTextIsHTML(true);
     }
  }

Helper-Implementierung
----------------------

Die Technik mit den Callbacks wurde eingeführt, um Hilfetexte dynamischer zu
gestalten. Primär, um bei den Eingabemöglichkeiten, bei denen Platzhalter
möglich sind, zu assistieren.

Ein gutes Beispiel dafür liefert das UserWorkflows-AddOn. Es definiert eine
ganze Reihe von Global Settings, die Angaben zu eMail-Empfängern, -Inhalten
usw. steuern. Innerhalb dieser Settings sind Platzhalter der Form
``#PLATZHALTER#`` erlaubt, mit denen Benutzer-Attribute in den Text eingefügt
werden können (eine Anrede könnte ``Hallo #FIRSTNAME# #LASTNAME#!`` lauten).

Um nun im Backend alle verfügbaren Platzhalter anzuzeigen, müsste das AddOn
bereits wissen, welche Attribute im Projekt definiert werden. Das kann es nicht.
Alternativ könnte der Integrator die Hilfetexte der vielen Settings
überschreiben -- zeitaufwendig.

Um dem zu begegnen wurde ein kleiner Helper implementiert:
``WV_Form_PropertyHelper``. Dieser erlaubt es, über ein paar Hilfsmethoden
die Listen von Benutzerattributen (und anderen!) abzurufen und als Hilfetext
darzustellen.

PHP-Seite
^^^^^^^^^

Um zu verstehen, wie er konfiguriert wird, sehen wir uns zuerst an, wie man ihn
von Hand in eigenen Formularen (die nicht auf dem Property-Datentyp-System
basieren) benutzen kann. Wir wollen dabei einem Input-Feld die Beschriftung
geben, dass Benutzerattribute als Platzhalter verwendet werden können.

.. sourcecode:: php

  <?
  // nichts besonderes hier
  $input = new sly_Form_Input_Text('myfield', 'Mein Text', '');

  // wir holen uns einen helper
  $helper = new WV_Form_PropertyHelper();

  // wir können den Text, der VOR der Liste der Attribute steht, selber bestimmen
  $helper->setLabel('Verwenden Sie die folgenden Attribute: ');

  // jetzt fügen wir dem Helper nach und nach Properties hinzu
  $helper->addProperty('NAME', 'Name der Person');
  $helper->addProperty('FIRSTNAME', 'Vorname der Person');
  // ...

  // und zuletzt lassen wir den generierten Hilfetext setzen
  $helper->prepareFormElement($input);

  // Hilfetext wird jetzt in etwa wie folgt aussehen:
  // 'Verwenden Sie ...: <strong>#NAME#</strong> (Name der Person), <strong>#.....#</strong> (...)'

Das ist nett, aber bisher haben wir noch nichts gewonnen. Die eigentliche
Hilfsfunktionalität sind die anderen ``add*``-Methoden:

.. sourcecode:: php

  <?
  // nichts besonderes hier
  $input = new sly_Form_Input_Text('myfield', 'Mein Text', '');

  // wir holen uns einen helper
  $helper = new WV_Form_PropertyHelper();

  // füge alle kategorie-metainfos hinzu
  $helper->addCategoryMetainfos();

  // füge alle frontenduser-attribute für benutzertyp 'default' hinzu
  $helper->addFrontendUserAttributes('default');

  // und noch die restlichen für den typ 'manager'
  $helper->addFrontendUserAttributes('manager');

  // und zuletzt lassen wir den generierten Hilfetext setzen
  $helper->prepareFormElement($input);

Jetzt haben wir deutlich Code eingespart. Und gleichzeitig ist unser Formular
dynamisch und passt sich den definierten Metainfos oder Benutzerattributen
automatisch an.

.. note::

  Die verfügbaren Helper sind hartcoded. Es gibt keine Möglichkeit, weitere
  Methoden in der Klasse zu ergänzen. Wer die Technik auch für eigene Attribute
  nutzen möchte, kann die ganze Klasse ableiten und erweitern oder die
  primitive ``add(name, description)``-Methode verwenden.

Es stehen die folgenden Helper bereit:

* Metainfo

  * ``addArticleMetainfos($articleType = null)``
  * ``addCategoryMetainfos()``
  * ``addMediumMetainfos()``
  * ``addUserMetainfos()``

* FrontendUser

  * ``addFrontendUserAttributes($userType = null)``

* varisale

  * ``addVarisaleAttributes($productType = null, $fixed = true, $variable = true)``

YAML-Konfiguration
^^^^^^^^^^^^^^^^^^

Der beschriebene Helper steht in Form des Callbacks
``WV_Datatype::placeholders()`` zur Verfügung. Dieser Callback dient einzig
dazu, die Nutzung von ``WV_Form_PropertyHelper`` konfigurierbar zu machen.

Dazu erwartet der Callback zwei Parameter:

``groups`` (array)
  ``groups`` ist ein Array von AddOns. Die Keys sind jeweils die Namen
  (``metainfo``, ``frontenduser``, ``varisale``) der AddOns und die Werte
  entsprechen weiterer Konfiguration. Das untenstehende Beispiel zeigt, wie es
  geht.

``label`` (string, optional)
  Das Label, das vor die Liste gesetzt werden soll (das ist **nicht** das Label
  des ganzen Formularelements!)

Wir wollen nun folgendes konfigurieren:

* Verwende den Callback in ``WV_Datatype``.
* Setze vor die Liste den Text ``Platzhalter``.
* Zeige alle FrontendUser-Attribute an.
* Zeige zusätzlich alle Benutzer-Metainfos an.
* Zeige zusätzlich (zu Demo-Zwecken) noch alle Artikel-Metainfos des Artikeltyps
  ``standard`` an.

.. sourcecode:: yaml

  testaddon:
     foo:
        title: 'Meine Property'
        datatype: 'stringline'
        helptext:
           callback: ['WV_Datatype', 'placeholders']
           params:
              # Das Label. Unspannend.
              label: 'Platzhalter: '

              # Die Gruppen (= AddOns)
              groups:
                 # FrontendUser: keine weitere Angabe (null für den Benutzertyp)
                 # -> *alle* Attribute anzeigen
                 frontenduser: ~

                 # Metainfo
                 metainfo:
                    # *alle* Benutzermetainfos
                    users: ~
                    # nur die Artikelmetainfos für den standard-Artikeltyp
                    articles: 'standard'

.. note::

  Die varisale-Attribute können bisher noch nicht über YAML eingeschränkt
  werden.

And you're done.
