Blog: Latest Entries (15):


PHP: XLSX einfach lesen

Bei in Java bei XML ist die bekannteste Lösung manchmal nicht die Lösung, die man gerade braucht. Groß, komplex und kann alles. Dependencies machen dann aber Probleme und wenn man nur eine Datei schnell und einfach lesen möchte, braucht man nicht irgend eine HTML-Lib, die nur in ganz bestimmten Fällen nötig wäre.

Beim Lesen von Excel-Dateien in PHP ist es genau so. HTML-Lib machte Probleme beim Installieren über Composer, aber ich will ga rkeine HTML-Sachen damit machen. Cool das es gehen würde, aber ich will nur schnell und einfach die Daten der Tabelle auslesen. CSV hätte ja gereicht, aber es kommt eben eine Excel-Datei.

Dann fand ich SimpleXLSX.

Super einfach zu benutzen. Hat keine weiteren Dependencies und liefert direkt die Tabelle als ein plain old Array.

bbcode-image


Code (XLSX to JSON):

<?php
include_once __DIR__ . '/vendor/autoload.php';

use Shuchkin\SimpleXLSX;

if ( $xlsx = SimpleXLSX::parse(__DIR__ . '/pets.xlsx') ) {
$json = [];
$head = $xlsx->rows()[0];

foreach ($xlsx->rows() as $idx => $row) {
if($idx > 0) {
$item = [];
foreach ($head as $idxKey => $key) {
if(strlen($key) > 0) {
$item[$key] = $row[$idxKey];
}
}
$json[] = $item;
}
}

echo json_encode($json);
} else {
echo SimpleXLSX::parseError();
}


Ergebnis:

[
{
"Name": "Miki",
"Rasse": "Hund",
"Farbe": "braun"
},
{
"Name": "Teddy",
"Rasse": "Hund",
"Farbe": "wei\\u00df"
},
{
"Name": "Merle",
"Rasse": "Katze",
"Farbe": "wei\\u00df|braun|schwarz"
},
{
"Name": "Finja",
"Rasse": "Katze",
"Farbe": "braun|schwarz"
},
{
"Name": "Ronja",
"Rasse": "Katze",
"Farbe": "wei\\u00df|schwarz"
},
{
"Name": "Molly",
"Rasse": "Katze",
"Farbe": "grau"
}
]

Development: FTP-Sync anstelle von Docker

Viele erinnern sich noch an Zeiten, wo man direkt auf einem Webserver seinen HTML-Seiten und Scripts geschrieben und getestet hat. HTML ging meistens schon lokal, aber wenn es um PHP oder anderes ging brauchte man einen Server. Dann schwenkte man auf XAMPP um, wo man einen lokalen Apache nutze. Linux brachte den Apache und PHP direkt mit. Aber man hatte oft kein Linux und half sich mit VirtualPC oder VirtualBox, so man entweder eine shared Speicher hatte oder ganz klassisch per FTP oder später SCP/SSH seine Dateien aus der IDE ins Zielsystem bekam. Dann kam Docker und die Welt wurde gut.. über all gut? Nein, dann erstaunlich viele gerade im Agentur-Bereich arbeiten immer noch mit einem Server und einem FTP-Sync. Gut heute oft mit SFTP oder SCP, aber ohne Docker oder lokalen Webserver.
Während ich klassische vServer mit Apache und ohne Reverse-Proxy und Docker für veraltet halte, sind sie noch öfter Realität als Docker-/K8n-Umgebungen. Selbst shared-Hosting für produktive Umgebungen sind noch öfters anzutreffen.

Nach einem Gespräch, wo noch direkt auf dem Server gearbeitet wurde und nicht mal eine lokale IDE einen Sync in Richtung Server vornahm sondern direkt die Datei vom Server aus geöffnet wurde (da kann man fast direkt mit vi auf dem Server arbeiten...), hier eine einfache kostenlose Lösung, wo man wenigstens die Dateien lokal hat und so auch ohne Probleme mit Git arbeiten kann.

Genutzt wird VisualStudio Code (die Intellij-IDEs bringen so einen Sync direkt von Haus aus mit, kosten aber in den meisten Varianten Geld).

Ein Plugin installieren:
bbcode-image


FTP-Config anlegen (wird geöffnet nach dem ersten Sync-Versuch):
bbcode-image


Wenn uploadOnSave aktiviert ist am Besten die IDE noch mal neustarten.

Geht auf jeden Fall besser als WinSCP parallel zum Sync laufen zu lassen.

Würde ich so entwickeln wollen? Nein. Besonders wenn mehr als ein Entwickler an einem Projekt arbeiten, geht nichts über Docker. Für Shopware habe ich gute Images oder man nimmt Dockware, was gerade für Entwickler an sich vollkommen reicht.

Shopware 6: Administration-Suche erweitern

Wie man eine eigene Entity in die Suche integriert hatte ich schon erklärt. Was aber wenn man eine vorhandene Entity um weitere durchsuchbare Felder erweitern will? Das geht auch relativ einfach.

Basiert auf diesem Forums-Post: https://forum.shopware.com/t/admin-such-optimierung-default-search-configuration-js/94397/3


const module = Module.getModuleByEntityName('customer');

if (module?.manifest?.defaultSearchConfiguration) {
module.manifest.defaultSearchConfiguration = {
...module.manifest.defaultSearchConfiguration,
extensions: {
// In case some other plugin has already done this trick; we do not want to remove theirs.
...(module.manifest.defaultSearchConfiguration.extensions ?? {}),
// Add our extension fields to the omnisearch
customFields: {
customer_debitor_set_number: {
_searchable: true,
_score: searchRankingPoint.HIGH_SEARCH_RANKING,
},
}
},
};
}


Auch wenn dort die Felder hierarchisch angegeben werden, sind diese bei den Snippets flach strukturiert.


{
"sw-profile": {
"tabSearchPreferences": {
"modules": {
"customer": {
"customer_debitor_set_number": "Debitor-Nummer"
}
}
}
}
}


Jetzt kann man die Darstellung der Suchergebnis-Items verbessern, damit man weiß, was man wo gefunden hat. Aber das ist an sich erstmal optional.

bbcode-image

Shopware 6 Tipp: Cart-Validator und Cart-Service

In einem Cart-Validator sollte man vermeiden vom Cat-Service die getCart()-Methode zu verwenden. Ich habe einen Service der mir für den Validator benötigte Daten lieferte und dabei auch einen Wert aus dem Cart generierte. getCart() triggert aber wieder den Validator.. Endlossschleife! Also am besten wirklich nur das in die Validator-Methode rein gereichte Cart-Object verwenden.

Shopware 6: Readonly und Disabled CustomFields

Es gibt manchmal CustomFields in denen man Daten wie externe Ids, ein Import- oder Export-Datum oder ein einfaches Bool-Flag speichern möchte. Der normale Admin-Benutzer darf diese Daten gerne sehen sollte sie aber nicht ändern, weil er oder sie nicht das nötige Wissen über die internen Abläufe des Plugins hat, um genau zu wissen, welche Auswirkungen so eine Änderung hat.

Deswegen ist es gut so ein CustomField readonly zu machen und am Besten komplett zu disablen. Das ist über die config des CustomFields sehr einfach möglich. In der Manifest einer App kann dass leider schon wieder ganz anders sein, weil dort sowas nicht vorgesehen ist.

bbcode-image

Shopware 6: Eigene Entitäten in der globalen Admin-Suche

Oft ist es sehr viel einfacher direkt etwas in die Suche der Administration einzugeben, als umständlich eine Seite zu öffnen und etwas aus der Liste per Hand oder Browser-Suche heraus zu suchen.
Eigene oder fehlende Entitäten dort zu integrieren ist an sich recht einfach und logisch. Es gibt hier eine Anleitung die aber leider so für mich nicht funktioniert hat, weil ein wichtiger Teil fehlte.

https://developer.shopware.com/docs/guides/plugins/plugins/administration/search-custom-data.html

Routennamen sind hier erstmal nur Beispiel haft vergeben.

Step 1
Ich gehe davon aus das ein Plugin existiert mit einem JS-Module, das mindestens eine Route hat und dessen Name nach dem Schema {vendor}-{name} aufgebaut ist. Zum Module müssen wir wie beschrieben
einige wenige Dinge ergänzen:


{
...

entity: 'ce_my_entity',
...
}


hier kommt später noch was dazu!

Step 2

Den Type (der Entität) hinzufügen. Der Name muss nicht dem Namen der Entität entsprechen, ist aber nicht verkehrt es so zu machen.


Application.addServiceProviderDecorator('searchTypeService', searchTypeService => {
searchTypeService.upsertType('ce_my_entity', {
entityName: 'ce_my_entity',
placeholderSnippet: 'search.general.placeholderSearchBar',
listingRoute: 'hpr.searchexample.index',
});
return searchTypeService;
});


Damit kennt die Suche nun den neuen Type und dann theoretisch schon danach suchen.

Step 3

Jetzt müssen wir festlegen wie unsere Entität bei den Ergebnissen dargestellt werden soll. Dafür erweitern wir ein Template und machen es der Suche bekannt.

Twig:

{% block sw_search_bar_item_cms_page %}
{% parent %}

<router-link v-else-if="type === 'ce_my_entity'"
v-bind:to="{ name: 'hpr.searchexample.detail', params: { id: item.id } }"
ref="routerLink"
class="sw-search-bar-item__link">
{% block sw_search_bar_item_ceme_label %}
<span class="sw-search-bar-item__label">
<sw-highlight-text v-bind:searchTerm="searchTerm"
v-bind:text="item.name">
</sw-highlight-text>
</span>
{% endblock %}
</router-link>
{% endblock %}


JavaScript:

Shopware.Component.override('sw-search-bar-item', {
template: templateItem
});


Step 4

Jetzt muss die Suche nur noch wissen wie unsere Entität bezeichnet werden soll, was wir bei den Snippets des Modules mit hinterlegen.


{
"global": {
"entities": {
"ce_my_entity": "Meine Entität"
}
}
}


Was noch fehlt
Soweit ist alles gut und nach Anleitung. Aber es funktionierte einfach nicht. Es wurde nach allen möglichen Entitäten gesucht nur nicht nach der eigenen. Nach viel Gesuche kam ich dann darauf, dass bei den Preferences, die für die Liste der Entities genutzt wird, meine eigene garnicht aufgelistet wurde. Warum? Weil ich natürlich keine Preferences dafür hinterlegt hatte, weil es nirgendwo angegeben war.

Das JS-Module muss also so aussehen:

{
...
entity: 'ce_my_entity',
defaultSearchConfiguration: {
_searchable: true,
name: {
_searchable: true,
_score: searchRankingPoint.HIGH_SEARCH_RANKING,
},
description: {
_searchable: true,
_score: searchRankingPoint.HIGH_SEARCH_RANKING,
}
}
...

}


Diese Preferences findet man im Profile seine Admin-Users und kann es dort alles noch genauer anpassen, wie die Suche suchen soll. Hier werden nun die Felder name und description angeboten und auch direkt aktiviert.


Damit funktionierte die Suche dann auch sofort wie gewünscht.

Shopware 6: Rules in Twig prüfen

Wenn man sich in einer Shopware 6 SaaS Umgebung und Apps bewegt, hat man nicht mehr die Möglichkeit Rules im PageLoader zu prüfen und ein bool-Value rein zu reichen, weil man nun alles via Twig machen muss. Entweder im Template oder in den App Scripts, die die PageLoader-Events ersetzt haben.

Geht zum Glück an ganz einfach auch wenn es kein array_intersect gibt.


{% set hideBuyButton = false %}
{% set checkRuleIds = config('MyApp.config.checkRules') %}
{% set intersect = checkRuleIds|filter((rule) => rule in context.context.ruleIds) %}
{% if intersect|length > 0 %}
{% set hideBuyButton = true %}
{% endif %}

Shopware 6: HTML-Sanitizer deaktivieren

Während man in 6.4 noch beliebigen eigenen HTML-Code in z.B. CMS-Elementen oder Snippets eingeben konnte, filtert 6.5 Teile dieses Codes nun heraus. Er gilt als möglicherweise unsicher. Wenn man nun von 6.4 auf 6.5 migriert und z.B. style-Tags entfernt werden, wäre es sehr aufwendig alles nun in SCSS und dem Theme unterzubringen. Einfacher ist es den Sanitizer zu deaktivieren und das selbe Verhalten wie bei 6.4 wieder zu haben.

bbcode-image


In der config/packages/shopware.yaml kann den Sanitizer einfach deaktiveren.


shopware:
html_sanitizer:
enabled: false

Shopware-Dev Docker-Setup

Update meiner Shopware Docker Umgebung. Funktioniert mit 6.4. An 6.5 arbeite ich noch. Es ist Imagick installiert, um z.B. automatisch beim Upload von PDFs die erste Seite als JPG zu speichern und in einem CustomField als Vorschau zu verlinken.

docker-compose.yml

version: '3.3'

services:
sw_mysql:
image: bitnami/mysql:5.7
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: sw
MYSQL_USER: sw
MYSQL_PASSWORD: sw
volumes:
- ./db_vol/:/bitnami/mysql/data

sw_http:
build: ./containerData/apache
volumes:
- ./containerData/apache/my_vhost.conf:/etc/apache2/sites-enabled/000-default.conf:rw
- ./sw:/var/www/html:rw,delegated
ports:
- 80:80
- 443:443
- 8080:8080
- 8090:8090

mail:
image: mailhog/mailhog
ports:
- 8025:8025

adminer:
image: adminer
ports:
- 8086:8080
depends_on:
- sw_mysql


containerData/apache/Dockerfile

FROM php:8.2-apache

RUN apt-get update &&\
apt-get install --no-install-recommends --assume-yes --quiet ca-certificates \
curl \
git \
libxml2-dev \
libxslt-dev \
libfreetype6-dev \
libjpeg62-turbo-dev \
libpng-dev \
libcurl4-gnutls-dev \
zlib1g-dev \
libzip-dev \
nano \
unzip \
ghostscript \
libmagickwand-dev \
&& docker-php-ext-install -j$(nproc) iconv \
&& docker-php-ext-configure gd --with-freetype=/usr/include/ --with-jpeg=/usr/include/ \
&& docker-php-ext-install -j$(nproc) gd

RUN apt-get install -y libc-client-dev libkrb5-dev \
&& docker-php-ext-configure imap --with-kerberos --with-imap-ssl \
&& docker-php-ext-install imap

RUN docker-php-ext-install dom \
&& docker-php-ext-install pdo \
&& docker-php-ext-install pdo_mysql \
&& docker-php-ext-install curl \
&& docker-php-ext-install zip \
&& docker-php-ext-install intl \
&& docker-php-ext-install xml \
&& docker-php-ext-install xsl \
&& docker-php-ext-install fileinfo

RUN mkdir -p /usr/src/php/ext/imagick
RUN curl -fsSL https://github.com/Imagick/imagick/archive/06116aa24b76edaf6b1693198f79e6c295eda8a9.tar.gz | tar xvz -C "/usr/src/php/ext/imagick" --strip 1
RUN docker-php-ext-install imagick


RUN curl -sL https://deb.nodesource.com/setup_16.x -o nodesource_setup.sh \
&& bash nodesource_setup.sh \
&& apt-get install -y nodejs

RUN rm -rf /var/lib/apt/lists/*

#RUN pecl install xdebug-2.8.0 && docker-php-ext-enable xdebug
#RUN echo 'zend_extension="/usr/local/lib/php/extensions/no-debug-non-zts-20151012/xdebug.so"' >> /usr/local/etc/php/php.ini
#RUN echo 'xdebug.remote_port=9000' >> /usr/local/etc/php/php.ini
#RUN echo 'xdebug.remote_enable=1' >> /usr/local/etc/php/php.ini
#RUN echo 'xdebug.remote_host=host.docker.internal' >> /usr/local/etc/php/php.ini

RUN echo 'memory_limit = 512M' >> /usr/local/etc/php/php.ini

RUN a2enmod rewrite

RUN php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
RUN php composer-setup.php --2.2 #there are problem
RUN mv composer.phar /usr/local/bin/composer

# copy conf-file to /etc/apache2/sites-enabled/000-default.conf
RUN mkdir /files;
COPY ./setup.sh /files/setup.sh
ENTRYPOINT ["sh", "/files/setup.sh"]


containerData/apache/my_vhost.conf

<VirtualHost *:80>
ServerName localhost
ServerAlias www.localhost

DocumentRoot "/var/www/html/public"

RewriteEngine On
RewriteMap lc int:tolower

<Directory "/var/www/html/public">
Options Indexes FollowSymLinks
AllowOverride All
Require all granted
DirectoryIndex index.php
</Directory>
</VirtualHost>


containerData/apache/setup.sh

/usr/sbin/apache2ctl -D FOREGROUND


Die Shopware Installation muss im sw/ Verzeichnis abgelegt werden.

Die 6.5 Version wird eine aktuelle MySQL-Version enthalten und hoffentlich dann ein komplettes Shopware-Setup über Composer.

Craft CMS 4 mit Docker installieren (ohne DDEV)

Wenn man weiß man tun muss ist es an sich recht einfach.

Wir brauchen Verzeichnis mit ./db_data und ./app. Zusätzlich noch eine leere .env Datei.

Um nichts mit DDEV zu tun haben zu müssen gehen wir zu GitHub und laden uns das letzte Release als Zip herunter. Die entpacken wir dann ins app-Verzeichnis.

Nun kommt die docker-compose.yml:


version: "3.6"
services:
console:
image: craftcms/cli:8.0-dev
env_file: .env
environment:
XDEBUG_CONFIG: client_host=host.docker.internal
SECURITY_KEY: dah873zhekdzhc3fai8zfdufu
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
volumes:
- ./app:/app
command: php craft queue/listen

web:
image: craftcms/nginx:8.0-dev
ports:
- 8080:8080
env_file: .env
environment:
XDEBUG_CONFIG: client_host=host.docker.internal
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
volumes:
- ./app:/app

postgres:
image: postgres:13-alpine
ports:
- 5432:5432
environment:
POSTGRES_DB: dev_craftcms
POSTGRES_USER: craftcms
POSTGRES_PASSWORD: SecretPassword
volumes:
- ./db_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD", "pg_isready", "-U", "craftcms", "-d", "dev_craftcms"]
interval: 5s
retries: 3

redis:
image: redis:5-alpine
ports:
- 6379:6379
healthcheck:
test: ["CMD", "redis-cli", "ping"]

von https://github.com/craftcms/docker übernommen.

Nun alles mit docker-compose up -d starten. Sich auf den web-Container per docker exec verbinden. Er hat keine bash sondern nur die sh. Aber egal. Einmal dieses Command ausführen:


php craft setup/security-key

Das generiert uns einen Security-Key für Cookies.

Nun http://localhost:8080/admin/install aufrufen und die Installation kann starten.

Getestet unter Windows mit Docker + WSL2. Sollte also auch ohne Probleme so unter Linux und auf einem Mac funktionieren.

Die EDC Whole/One-DC Shopware Plugins werden OpenSource

Nach einigen Hin und noch mehr Her, hat die Interspark Inc bestätigt, dass sie keine IPs der Interspark GmbH übernommen hat und somit auch nicht die IPs an den beiden Plugins besitzt. War am Ende sehr schnell feste gestellt und ich übernehme die Plugins nun wieder, da es ja sonst niemanden gibt, der Ansprüche erhebt. Die Lizenz wird auf die MIT/BSD Lizenz geändert und die GitLab Projekte werden öffentlich.

Während die Shopware 5 Version schon in mehreren Shops bewiesen hat, das sie funktioniert, steht bei der Shopware 6 Version noch aus, diese als production-ready zu deklarieren.
Sie hat die meisten Test gut gemeistert und sollte nun auch mit Shopware 6.5 funktionieren. Trotzdem steht noch aus diese in einer produktiven Umgebung über einige Tage laufen zulassen.

Builds und Releases wird es ab jetzt hier geben. Jeder der Interesse oder Bedarf hat kann sich die Plugins installieren oder auch gerne mit weiter entwickeln.

Releases sind hier auf Google-Drive zu finden

Eine App-Version des Plugins, um damit auch die Shopware 6 Cloud Version (SaaS) abzudecken ist bis jetzt nicht von mir geplant, außer jemand würde mir dafür 5000 EUR geben, weil der Aufwand wirklich groß ist. Es wäre eine vollständig eigenständige Software, die außerhalb von Shopware läuft und dort wo die DAL verwendet wird dann die Shopware API (mit Multi-Tenant usw) aufrufen würde.

Urlaub Revolutions

Man selbst fährt weit weg und andere übernehmen für die Zeit die Aufgaben, die man sonst selbst ausgeführt hätte. Urlaub. Ein tolles Konzept. Erholung, befreit von den Verpflichtungen die man sonst gegenüber den Kunden hat. Sword Art Online gucken und überlegen wie man Kubernetes Fähigkeiten assimilieren kann ohne wie die Borg-Queen zu erscheinen. Einfach mal AI Bilder von seinem Hund erzeugen. Ok... es hat bis Mittwoch funktioniert. Am Ende muss man proaktiv immer selbst prüfen, ob die Vertretung seine Aufgaben erledigt. Nein... tun sie nicht. Also am Ende ist Urlaub eine Illusion. Es wird trotzdem verlangt, dass man sich um alles kümmert und sich um die Fehler der anderen kümmert und berichtigt.

Am Ende ist das einzige Fazit: Urlaub ist eine Illusion. Es ist eine Matrix mit dem Glauben der freien Entscheidung.

Shopware 6: Probleme mit sehr aktuellen NodeJS-Versionen

Bei einer sehr aktuellen NodeJS-Version kam es bei mir zu dieser Fehlermeldung, als ich versuchte die Administration zu bauen.


error:0308010C:digital envelope routines::unsupported


Das Problem lässt sich beheben in dem man vorher dieses hier aufruft:


export NODE_OPTIONS=--openssl-legacy-provider


Dann sollte es wieder wie gewohnt durchlaufen. Beim Bauen der Storefront gilt das Selbe, wenn man es z. B. in verschiedenen SSH-Session baut.

Klipper: Got EOF when reading from device

Der Fehler


Got EOF when reading from device


besagt, dass Klipper die USB-Verbindung zum Drucker verloren hat. Das kann verschieden Gründe haben: Strom-Ausfall, USB-Kabel hat ein Problem, der Drucker ist in einen Fail-State gegangen.

Bei mir hatte sich ein Kabel der Z-Steppers in der Justierung des Druckbetts verfangen und das Bett konnte nicht mehr ganz zurück fahren. Also wenn der Fehler immer wieder während des Drucks auftritt, hat der Drucker wohl ein Problem und das ganz unabhängig von Klipper.

Update:
Am Ende musste ich doch das USB-Kabel austauschen. Der USB-A Stecker am Pad rutsch schnell heraus und sitzt nicht wirklich fest beim mitgelieferten Kabel.

3D Druck: Marlin Firmware selber bauen

Wenn man sich die Anleitungen durchliest, wie man Marlin selbst compilieren kann, muss mn immer VSCode mit vielen Plugins und so installieren. Alles sehr aufwendig. Aber es geht auch viel einfacher. Dank https://github.com/frealmyr/marlin-build kann man es einfach per Docker bauen. Man muss nur auf eine Sache achten: USE_TAG angeben und in docker-compose einkommentieren und die Configs für diese Version nutzen.

Die Configs findet man hier https://github.com/MarlinFirmware/Configurations

Meine .env sieht so aus:

# This file is to be used with docker-compose.yml, or sourced before using docker run
BOARD=STM32F103RE_creality
MARLIN_FIRMWARE=./out
MARLIN_CONFIGURATION=./ender3_marlin_config
USE_TAG=2.1.2


meine docker-compose.yml so:

version: "3.5"

services:
build:
container_name: marlin-build
image: frealmyr/marlin-build:latest
user: 1000:1000
stdin_open: true
tty: true
environment:
- BOARD
# - USE_LATEST=true # Use latest git tag
# - USE_REPO=https://github.com/frealmyr/Marlin # USe a different git repo
- USE_TAG
# - USE_BRANCH=bugfix-2.0.x # Use a branch instead of latest tag
# - FW_EXTENSION=hex
volumes:
- $MARLIN_FIRMWARE:/home/platformio/build
- $MARLIN_CONFIGURATION:/home/platformio/CustomConfiguration
# - ./build-marlin.sh:/home/platformio/build-marlin.sh # Use build script in repo instead of image



wie man sieht ist das Github-Projekt auszuchecken optional, die beiden Dateien reichen an sich.

Um nun Marlin 2.1.2 für den Ender 3 mit Creality Board 4.2.7 zu bauen muss man nur noch eines tun:


docker-compose up


bbcode-image

Older posts:

Möchtest Du AdSense-Werbung erlauben und mir damit helfen die laufenden Kosten des Blogs tragen zu können?