OXID 6 | Modul erstellen mit Namespaces und Composer

Durch den Einzug von Composer bei OXID 6.x, hat sich nicht nur am Core einiges geändert. In diesem Beitrag versuche ich so detailliert wie möglich die Erstellung eines Moduls für OXID 6.x zu erklären. Grundkenntnis von Programmierung und OXID setze ich aber für dieses Tutorial voraus.

In meinem Beispiel erweitere ich die Artikel von OXID um weitere Felder in einem zusätzlichen Tab. Zur Verdeutlichung hier eine kurze Übersicht:

  • Artikel bekommen neuen Tab im Backend
  • Neue Felder für technische Details anlegen
  • Datenbankfelder werden beim aktivieren des Moduls automatisch angelegt
  • Die neuen Inhalte werden als zusätzlichen Tab im Shop bei der Detailansicht eines Produkts angezeigt
  • Installation über Composer

Genug der Einführung, starten wir mit der Erstellung des Moduls.

1. Ordnerstruktur anlegen

Wer öfters ein OXID 6 Modul erstellen muss, dem empfehle ich sich die Ordnerstruktur dafür einmal anzulegen und zu sichern. Im Verzeichnis „modules“ erstellen wir also die Struktur für unser Modul:

..\source\modules
OXID 6 Modul Ordnerstruktur
OXID 6 Modul Ordnerstruktur

2. Funktionen erstellen

Im angegebenen Pfad kopieren wir uns die ArticleMain.php:

..\vendor\oxid-esales\oxideshop-ce\source\Application\Controller\Admin

und fügen diese in unser Modul unter folgenden Pfad ein:

..\source\modules\protipps\technical_details\Application\Controller\Admin

Die Datei benennen wir um in TechnicalDetails.php. Der Name der Datei muss der class im Code entsprechen. Die meisten Funktionen aus der Original-Datei benötigen wir für unsere Zwecke allerdings nicht. Deswegen löschen wir alle Zeilen die wir nicht benötigen einfach raus und passen den Namespace für unsere Zwecke an. Der Namespace ist später wichtig für unsere composer.json. Das Ergebnis sieht wie folgt aus:

namespace ProTipps\TechnicalDetails\Application\Controller\Admin;

class TechnicalDetails extends \OxidEsales\Eshop\Application\Controller\Admin\AdminDetailsController {
    public function render() {
        parent::render();
        $this->_aViewData['edit'] = $article = oxNew(\OxidEsales\Eshop\Application\Model\Article::class);
        $oxId = $this->getEditObjectId();
        if (isset($oxId) && $oxId != "-1") {
            $article->loadInLang($this->_iEditLang, $oxId);
        }
        return "article_technical_details.tpl";
    }

    public function save() {
        $soxId = $this->getEditObjectId();
        $aParams = \OxidEsales\Eshop\Core\Registry::getConfig()->getRequestParameter("editval");

        $oArticle = oxNew(\OxidEsales\Eshop\Application\Model\Article::class);
        $oArticle->loadInLang($this->_iEditLang, $soxId);
        $oArticle->setLanguage(0);
        $oArticle->assign($aParams);
        $oArticle->setLanguage($this->_iEditLang);
        $oArticle = \OxidEsales\Eshop\Core\Registry::getUtilsFile()->processFiles($oArticle);
        $oArticle->save();

        parent::save();
    }
}

3. Admin Template erstellen

Damit wir im OXID Backend unsere Felder angezeigt bekommen, benötigen wir ein Admin Template. Da wir die Ansicht der Artikel erweitern möchten, wechseln wir in das folgende Verzeichnis und kopieren uns die article_main.tpl:

..\source\Application\views\admin\tpl

Das Template kopieren wir in unser Modul in folgendes Verzeichnis:

..\source\modules\protipps\technical_details\Application\views\admin\tpl

Wie in der TechnicalDetails.php in der render Function angegeben, müssen wir das Template noch umbenennen in article_technical_details.tpl. Als nächstes können wir die ganzen Textfelder, die im OXID Backend eigentlich unter dem Reiter Stamm angezeigt werden, entfernen. Dann müssen wir die zwei hidden Felder mit der Klasse article_main an unsere anpassen. In meinem Beispiel wird aus:

<input type="hidden" name="cl" value="article_main">
<input type="hidden" name="cl" value="protippstechnicaldetails">

Nicht zu vergessen ist, dass diese Anpassungen in beiden Formularen in dem Template zu machen sind. Danach können wir unsere eigenen Felder hinzufügen. Wer mehrsprachige Anzeigen benötigt, dem empfehle ich bei den Labels mit Sprachmarkern zu arbeiten.

Folgende Datenbankfelder werden wir für die Anzeige benötigen:

  • ptmaterial
  • ptpower
  • ptpowersupply
  • ptbattery
  • ptbatteryquantity
  • ptbatterytype

Dazu erstellen wir im Anschluss die nötigen Event Funktion. Wir müssen somit nicht manuell in die Datenbank eingreifen. Jetzt aber noch das komplette Admin Template im Überblick.

[{include file="headitem.tpl" title="GENERAL_ADMIN_TITLE"|oxmultilangassign}]
<script type="text/javascript">
    <!--
    function editThis( sID )
    {
        var oTransfer = top.basefrm.edit.document.getElementById( "transfer" );
        oTransfer.oxid.value = sID;
        oTransfer.cl.value = top.basefrm.list.sDefClass;

        //forcing edit frame to reload after submit
        top.forceReloadingEditFrame();

        var oSearch = top.basefrm.list.document.getElementById( "search" );
        oSearch.oxid.value = sID;
        oSearch.actedit.value = 0;
        oSearch.submit();
    }
    [{if !$oxparentid}]
    window.onload = function ()
    {
        [{if $updatelist == 1}]
        top.oxid.admin.updateList('[{$oxid}]');
        [{/if}]
        var oField = top.oxid.admin.getLockTarget();
        oField.onchange = oField.onkeyup = oField.onmouseout = top.oxid.admin.unlockSave;
    }
        [{/if}]
    //-->
</script>

[{if $readonly}]
    [{assign var="readonly" value="readonly disabled"}]
[{else}]
    [{assign var="readonly" value=""}]
[{/if}]

<form name="transfer" id="transfer" action="[{$oViewConf->getSelfLink()}]" method="post">
    [{$oViewConf->getHiddenSid()}]
    <input type="hidden" name="oxid" value="[{$oxid}]">
    <input type="hidden" name="oxidCopy" value="[{$oxid}]">
    <input type="hidden" name="cl" value="protippstechnicaldetails">
    <input type="hidden" name="editlanguage" value="[{$editlanguage}]">
</form>

<form name="myedit" id="myedit" action="[{$oViewConf->getSelfLink()}]" method="post" onSubmit="return chkInsert()" style="padding: 0px;margin: 0px;height:0px;">
    [{$oViewConf->getHiddenSid()}]
    <input type="hidden" name="cl" value="protippstechnicaldetails">
    <input type="hidden" name="fnc" value="">
    <input type="hidden" name="oxid" value="[{$oxid}]">
    <input type="hidden" name="voxid" value="[{$oxid}]">
    <input type="hidden" name="oxparentid" value="[{$oxparentid}]">
    <input type="hidden" name="editval[oxarticles__oxid]" value="[{$oxid}]">

    <table class="technische-details" cellspacing="0" cellpadding="0" border="0" style="width:98%;">
        <tr>
            <td valign="top" width="48%">
                <table cellspacing="0" cellpadding="0" border="0">
                    <tr>
                        <td class="edittext">
                            <label>[{oxmultilang ident="PROTIPPS_TECHNICAL_DETAILS_MATERIAL"}]</label>
                            <input type="text" class="editinput" size="32" name="editval[oxarticles__ptmaterial]" value="[{$edit->oxarticles__ptmaterial->value}]">
                        </td>
                    </tr>
                    <tr>
                        <td class="edittext">
                            <label>[{oxmultilang ident="PROTIPPS_TECHNICAL_DETAILS_POWER"}]</label>
                            <input type="text" class="editinput" size="32" name="editval[oxarticles__ptpower]" value="[{$edit->oxarticles__ptpower->value}]">
                        </td>
                    </tr>
                    <tr>
                        <td class="edittext">
                            <label>[{oxmultilang ident="PROTIPPS_TECHNICAL_DETAILS_POWER_SUPPLY"}]</label>
                            <input type="text" class="editinput" size="32" name="editval[oxarticles__ptpowersupply]" value="[{$edit->oxarticles__ptpowersupply->value}]">
                        </td>
                    </tr>
                </table>
            </td>
            <td valign="top" width="48%">
                <table cellspacing="0" cellpadding="0" border="0">
                    <tr>
                        <td class="edittext">
                            <label>[{oxmultilang ident="PROTIPPS_TECHNICAL_DETAILS_BATTERY_OPERATION"}]</label>
                            <input type="text" class="editinput" size="32" name="editval[oxarticles__ptbattery]" value="[{$edit->oxarticles__ptbattery->value}]">
                        </td>
                    </tr>
                    <tr>
                        <td class="edittext">
                            <label>[{oxmultilang ident="PROTIPPS_TECHNICAL_DETAILS_BATTERY_QUANTITY"}]</label>
                            <input type="text" class="editinput" size="32" name="editval[oxarticles__ptbatteryquantity]" value="[{$edit->oxarticles__ptbatteryquantity->value}]">
                        </td>
                    </tr>
                    <tr>
                        <td class="edittext">
                            <label>[{oxmultilang ident="PROTIPPS_TECHNICAL_DETAILS_BATTERY_TYPE"}]</label>
                            <input type="text" class="editinput" size="32" name="editval[oxarticles__ptbatterytype]" value="[{$edit->oxarticles__ptbatterytype->value}]">
                        </td>
                    </tr>
                </table>
            </td>
        </tr>
        <tr>
            <td class="edittext" colspan="2"><br><br>
                <input type="submit" class="edittext" id="oLockButton" name="saveArticle" value="[{oxmultilang ident="ARTICLE_MAIN_SAVE"}]" onClick="Javascript:document.myedit.fnc.value='save'" [{if !$edit->oxarticles__oxtitle->value && !$oxparentid}]disabled[{/if}] [{$readonly}]>
            </td>
        </tr>
    </table>
</form>

[{include file="bottomnaviitem.tpl"}]
[{include file="bottomitem.tpl"}]

4. Core Event erstellen

Die Event Funktion benutzen wir um die Datenbankfelder automatisch anzulegen. Dadurch werden die Felder beim aktivieren des Moduls erstellt und die Views auch gleich aktualisiert. Wir erstellen in folgenden Pfad die Datei TechnicalDetailEvent.php

..\source\modules\protipps\technical_details\Core\Events

Der Inhalt der Datei ist folgender:

<?php
 
namespace ProTipps\TechnicalDetails\Core\Events;

use \OxidEsales\Eshop\Core\DbMetaDataHandler

class TechnicalDetailEvent {

    public static function onActivate() {
        $oMetaData = oxNew("oxDbMetaDataHandler");
        $dbQueries = [];

        if(!$oMetaData->fieldExists("ptmaterial", "oxarticles")) {
            $dbQueries[] = "ALTER TABLE `oxarticles` ADD COLUMN `ptmaterial` VARCHAR(40) NOT NULL AFTER `OXSHOWCUSTOMAGREEMENT`";
        }
        if(!$oMetaData->fieldExists("ptpower", "oxarticles")) {
            $dbQueries[] = "ALTER TABLE `oxarticles` ADD COLUMN `ptpower` VARCHAR(40) NOT NULL AFTER `ptmaterial`";
        }
        if(!$oMetaData->fieldExists("ptpowersupply", "oxarticles")) {
            $dbQueries[] = "ALTER TABLE `oxarticles` ADD COLUMN `ptpowersupply` VARCHAR(40) NOT NULL AFTER `ptpower`";
        }
        if(!$oMetaData->fieldExists("ptbattery", "oxarticles")) {
            $dbQueries[] = "ALTER TABLE `oxarticles` ADD COLUMN `ptbattery` VARCHAR(40) NOT NULL AFTER `ptpowersupply`";
        }
        if(!$oMetaData->fieldExists("ptbatteryquantity", "oxarticles")) {
            $dbQueries[] = "ALTER TABLE `oxarticles` ADD COLUMN `ptbatteryquantity` VARCHAR(40) NOT NULL AFTER `ptbattery`";
        }
        if(!$oMetaData->fieldExists("ptbatterytype", "oxarticles")) {
            $dbQueries[] = "ALTER TABLE `oxarticles` ADD COLUMN `ptbatterytype` VARCHAR(40) NOT NULL AFTER `ptbatteryquantity`";
        }
        if (count($dbQueries) === 0) return true;

        $oMetaData->executeSql($dbQueries);
        $oMetaData->updateViews();
    }

    public static function onDeactivate() {

    }

}

Kurze Erklärung

  • Der Namespace sollte klar sein. Der Abschnitt „ProTipps\TechnicalDetails“ ist wichtig für die composer.json. Der Rest des Pfads gibt an wo die Datei liegt.
  • Die Klasse muss auch wieder analog zum Dateinamen sein.
  • Die Funktion onActivate() erstellt beim aktivieren des Moduls die nötigen Datenbankfelder in oxarticles.
  • Bei der Funktion onDeactivate() können, wenn gewünscht, Funktionen programmiert werden die beim deaktivieren des Moduls ausgeführt werden sollen.
  • Die Funktion updateViews() aktualisiert die Datenbank und erstellt die dazugehörigen Views.

5. Frontend Ausgabe

Um den Inhalt unserer Felder auch im Shop angezeigt zu bekommen, benötigen wir noch das Template dazu. Ich möchte die Texte in einem neuen Tab in der Detailansicht eines Produkts angezeigt bekommen. Wir wechseln somit in folgenden Pfad unseres Moduls:

..\source\modules\protipps\technical_details\out\blocks\page\details\inc

Hier erstellen wir eine Template mit dem Dateinamen tabs_technical_details.tpl. Wir erweitern dadurch die Original Datei tabs.tpl aus dem Flow bzw. Wave Theme. Die Datei liegt unter:

\source\Application\views\flow\tpl\page\details\inc

Der Inhalt unseres Templates ist wie folgt:

[{$smarty.block.parent}]

[{capture append="tabs"}]<a href="#technische-details" data-toggle="tab">[{oxmultilang ident="PROTIPPS_TECHNICAL_DETAILS"}]</a>[{/capture}]
[{capture append="tabsContent"}]
    <div id="technische-details" class="tab-pane[{if $blFirstTab}] active[{/if}]">
        <h2>[{oxmultilang ident="PROTIPPS_TECHNICAL_DETAILS"}]:</h2>

        <dl>
            <dt>[{oxmultilang ident="PROTIPPS_TECHNICAL_DETAILS_MATERIAL"}]</dt>
            <dd>[{$oDetailsProduct->oxarticles__ptmaterial->value}]</dd>

            <dt>[{oxmultilang ident="PROTIPPS_TECHNICAL_DETAILS_POWER"}]</dt>
            <dd>[{$oDetailsProduct->oxarticles__ptpower->value}]</dd>

            <dt>[{oxmultilang ident="PROTIPPS_TECHNICAL_DETAILS_POWER_SUPPLY"}]</dt>
            <dd>[{$oDetailsProduct->oxarticles__ptpowersupply->value}]</dd>

            <dt>[{oxmultilang ident="PROTIPPS_TECHNICAL_DETAILS_BATTERY_OPERATION"}]</dt>
            <dd>[{$oDetailsProduct->oxarticles__ptbattery->value}]</dd>

            <dt>[{oxmultilang ident="PROTIPPS_TECHNICAL_DETAILS_BATTERY_QUANTITY"}]</dt>
            <dd>[{$oDetailsProduct->oxarticles__ptbatteryquantity->value}]</dd>

            <dt>[{oxmultilang ident="PROTIPPS_TECHNICAL_DETAILS_BATTERY_TYPE"}]</dt>
            <dd>[{$oDetailsProduct->oxarticles__ptbatterytype->value}]</dd>
        </dl>

    </div>
    [{assign var="blFirstTab" value=false}]
[{/capture}]

Kurze Erklärung:

  • smarty.block.parent bedeutet dass alle anderen Tabs auch weiterhin angezeigt werden sollen.
  • Mit $oDetailsProduct->ocarticles__xxxxxxx->value wird der Inhalt des jeweiligen Datenbankfeld angezeigt.

6. metadata.php

Der Inhalt der metadata.php ist in unserem Fall folgender:

<?php

/**
 * Metadata version
 */
$sMetadataVersion = '2.0';

/**
 * Module information
 */
$aModule = [
    'id'          => 'technical_details',
    'title'       => [
        'de' => '.PROTIPPS | Technische Details',
        'en' => '.PROTIPPS | Technical details',
    ],
    'description' => [
        'de' => 'Artikel um technische Details erweitern',
        'en' => 'Expand articles with technical details',
    ],
    'thumbnail'     => 'out/admin/src/img/pt-logo.svg',
    'version'       => '1.0.0',
    'author'        => 'Programmier Tipps',
    'url'           => 'https://www.programmier-tipps.de/',
    'email'         => 'kontakt@programmier-tipps.de',
    'extend'        => array(),
    'controllers'         => array(
        'protippstechnicaldetails' => \ProTipps\TechnicalDetails\Application\Controller\Admin\TechnicalDetails::class
    ),
    'blocks' => array(
        array(
            'template'  => 'page/details/inc/tabs.tpl',
            'block'     => 'details_tabs_longdescription',
            'file'      => 'out/blocks/page/details/inc/tabs_technical_details.tpl'
        ),
    ),
    'templates'     => array(
        'article_technical_details.tpl' => 'protipps/technical_details/Application/views/admin/tpl/article_technical_details.tpl'
    ),
    'events'        => array(
        'onActivate' => '\ProTipps\TechnicalDetails\Core\Events\TechnicalDetailEvent::onActivate',
        'onDeactivate' => '\ProTipps\TechnicalDetails\Core\Events\TechnicalDetailEvent::onDeactivate'
    ),
    'settings' => array(),
];

Kurze Erklärung

  • MetadataVersion 2.0 wird für Composer benötigt
  • Unter „controllers“ wird die Klasse hinterlegt, die wir anfangs in unserem Admin Template definiert haben „protippstechnicaldetails“ und der Namespace zur Datei ohne Dateiendung (php). Anstatt der Dateiendung benutzen wir ::class.
  • Mittels „blocks“ können wir die Frontend-Datei tabs.tpl überschreiben.
    • template -> ist der Pfad zur Datei vom Flow bzw. Wave Theme
    • block -> ist der Name des Blocks den wir überschreiben wollen
    • file -> ist der Pfad zu unserem Template mit welchem der Block überschrieben/erweitert wird
  • „templates“ bestimmt wo unser Admin Template zu finden ist
  • Und mit „events“ sagen wir OXID wo unsere Datei für das automatische erstellen unserer Datenbankfelder liegt. Wichtig! Hier verwenden wir wieder Namespaces.

7. menu.xml

Damit unser Tab auch unter Artikels im OXID Backend erscheint und funktioniert brauchen wir eine menu.xml mit diesem Inhalt:

<?xml version="1.0" encoding="UTF-8"?>
<OX>
    <OXMENU id="NAVIGATION_ESHOPADMIN">
        <MAINMENU id="mxmanageprod">
            <SUBMENU id="mxarticles" cl="article" list="article_list">
                <TAB id="PROTIPPS_TECHNICAL_DETAILS" cl="protippstechnicaldetails" />
            </SUBMENU>
        </MAINMENU>
    </OXMENU>
</OX>

Kurze Erklärung:

  • MAINMENU mit „mxmanageprod“ weiß OXID das wir den Menüpunkt „Artikel verwalten“ erweitern möchten.
  • Unser Tab soll im SUBMENU von Artikel verwalten „mxarticles“ (Artikel) erscheinen.
  • TAB -> id wird für die Übersetzung verwendet
  • TAB -> cl ist wieder unsere Klasse unseres Controllers aus dem Formular und der metadata.php

8. composer.json befüllen

Der Aufbau unserer composer.json sieht nun wie folgt aus:

{
  "name": "protipps/technical_details",
  "description": "Expand articles with technical details",
  "type": "oxideshop-module",
  "keywords": ["oxid", "modules", "eShop"],
  "homepage": "https://www.programmier-tipps.de/",
  "version": "1.0.0",
  "license": [
    "GPL-3.0-only"
  ],
  "extra": {
    "oxideshop": {
      "target-directory": "protipps/technical_details"
    }
  },
  "autoload": {
    "psr-4": {
      "ProTipps\\TechnicalDetails\\": "../../../source/modules/protipps/technical_details"
    }
  }
}

Kurze Erklärung:

  • Der Aufbau einer composer.json für OXID wird hier sehr gut erklärt.
  • Wichtig ist dass unter autoload unser Namespace hinterlegt wird, so wie wir in überall definiert haben.

9. Sprachdateien

Der Vollständigkeit halber hier noch der Inhalt der Sprachdateien.

Deutsch

$sLangName  = "Deutsch";

$aLang = [

    'charset'                                           => 'UTF-8',

    'PROTIPPS_TECHNICAL_DETAILS'                        => 'Technische Details',
    'PROTIPPS_TECHNICAL_DETAILS_MATERIAL'               => 'Material',
    'PROTIPPS_TECHNICAL_DETAILS_POWER'                  => 'Stromaufnahme',
    'PROTIPPS_TECHNICAL_DETAILS_POWER_SUPPLY'           => 'Spannungsversorgung',
    'PROTIPPS_TECHNICAL_DETAILS_BATTERY_OPERATION'      => 'Batteriebetrieb',
    'PROTIPPS_TECHNICAL_DETAILS_BATTERY_QUANTITY'       => 'Batteriemenge',
    'PROTIPPS_TECHNICAL_DETAILS_BATTERY_TYPE'           => 'Batterietyp',

];

Englisch

$sLangName  = "English";

$aLang = [

    'charset'                                           => 'UTF-8',

    'PROTIPPS_TECHNICAL_DETAILS'                        => 'Technical details',
    'PROTIPPS_TECHNICAL_DETAILS_MATERIAL'               => 'Material',
    'PROTIPPS_TECHNICAL_DETAILS_POWER'                  => 'Power',
    'PROTIPPS_TECHNICAL_DETAILS_POWER_SUPPLY'           => 'Power supply',
    'PROTIPPS_TECHNICAL_DETAILS_BATTERY_OPERATION'      => 'Battery operation',
    'PROTIPPS_TECHNICAL_DETAILS_BATTERY_QUANTITY'       => 'Battery quantity',
    'PROTIPPS_TECHNICAL_DETAILS_BATTERY_TYPE'           => 'Battery type',

];

Um das Modul nun mit Composer zu installieren, ist nur noch folgender Befehl nötig.

composer config repositories.programmiertipps composer https://github.com/programmiertipps
composer require protipps/technical_details

Du kannst das Modul auch über ein ZIP-Archiv installieren. Eine sehr ausführliche Anleitung findest du im OXID Forge.


Ich hoffe das Tutorial ist für jeden verständlich und nachvollziehbar. Ich freue mich auf konstruktive Kritik und Lob in den Kommentaren. Viel Erfolg bei deinem Projekt!

* Werbung/Affiliate Link
BISON Kryptowährungen

Kommentare

  • Joachim Schabowski
    21.04.2020 - 12:33 Uhr

    Hallo,
    ditt is ne super Anleitung! Weiter so!
    Grüßle
    Jogi

    Antworten
  • Klaus Kowolik
    23.04.2020 - 15:47 Uhr

    Guten Tag,

    kann man den Vorgang mit „composer require protipps/technical_details“ bitte auch detaillierter beschreiben?

    Ich habe Oxid vor Jahren installiert und damals konnte ich ohne Zusatzprogramme wie Composer Module installieren. Jetzt muss man das können. Ich habe es installiert und den o.g. Befehl ausgeführt.
    Die Zeile auf meinem lokalen Webserver auf Win 10 sieht so aus:

    C:\xampp\htdocs\oxid\source\modules>composer require protipps/technical_details

    Es kommt diese Fehlermeldung:

    „[InvalidArgumentException]
    Could not find a matching version of package protipps/technical_details. Check the package spelling, your version c
    onstraint and that the package is available in a stability which matches your minimum-stability (stable).“

    Hat jemand eine Idee, was ich falsch gemacht habe? Ich habe ganz genau die Anleitung verfolgt.

    LG
    Klaus

    Antworten
  • Micha
    20.06.2021 - 12:53 Uhr

    Dein onActivate Event prüft nicht mittels oDbMetaDataHandler::fieldExists() ob das datenbankfeld schon existiert somit wird das sql statement immer geschossen. Außerdem werden zwar die views aktualisiert, aber der FieldCache nicht. Somit ist dein datenbankfeld erst nach leeren des tmp Ordners erreichbar.

    Antworten
    • Markus
      21.06.2021 - 12:16 Uhr

      Hallo Micha, danke dir für den Hinweis! Habe meinen Code aktualisiert.

      Antworten

Schreibe einen Kommentar

Erforderliche Felder sind entsprechend markiert.

Wird nicht veröffentlicht.