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
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!
Kommentare
Hallo,
ditt is ne super Anleitung! Weiter so!
Grüßle
Jogi
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
Hallo Klaus, installiere das Modul einfach mittels ZIP-Archiv. Eine gute Anleitung findest du hier: ZIP-Archiv mit Composer installieren
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.
Hallo Micha, danke dir für den Hinweis! Habe meinen Code aktualisiert.
Weitere Beiträge
Wordpress | Benutzerdefinierte Felder in Sidebar anzeigen
Vom Photoshop Design zum fertigen Wordpress Template (Teil 1)
Wordpress | Die Anzeige von Inhalten mit Conditional Tags selbst steuern.
PHP 7 | Daten aus HTML-Formular in eine XML-Datei speichern