Blog: Latest Entries (15):



cJSv2: Services und Service-Injection

Ich hab cJSv2 jetzt soweit erweitert, dass man nun sich einfach in verschiedene Services in seine Controller injecten lassen kann (und ist dann unter seinem Namen oder einem Alias dort direkt aufrufbar). Jeder Service ist Singleton, so dass jeder Controller auf die selbe Instance des Services zugreift.
An sich wird damit die cjsGetController Methode, mit der sich Controller untereinander mit ihren Namen konnten, überflüssig und man kann einfach asynchrone/reactive Lösungen basteln.

Hier erst einmal ein kleines synchrones Event-System:


function EventBus() {
this.events = [];

this.register=function(event, callback){
if(!this.events[event]) {
this.events[event] = [];
}
this.events[event].push(callback);
}

this.fire=function(event, payload){
if(this.events[event]) {
this.events[event].forEach(subscriber => {
subscriber(payload);
})
}
}
}

function Receiver(){
this.value="";

this.init=function(){
this.EventBus.register("update", (payload) => {
this.value = payload.value;
});
this.cjsPushBindings();
};
}

function Sender(){
this.values=[
'Blubb',
'Test',
'Brumm',
'Event',
'Something'
];

this.send=function(event){
this.EventBus.fire("update", {value: this.values[Math.floor(Math.random() * this.values.length)]})
};
}



<div cjs-controller="sender:Sender" cjs-services="EventBus">
<input type="button" value="fire event" cjs-binding-event="click:send"/>
</div>
<div cjs-controller="receiver:Receiver:init" cjs-services="EventBus">
<input type="text" value="" cjs-binding-value="value"/>
</div>



Werte und Qualität - Eine generische Betrachung

Irgendwann kommt es in jeder Firma zu dem Zeitpunkt an dem über Werte, Qualität gesprochen wird und wie man diese selbst leben kann. Bei allen die direkten Kundenkontakt haben ist es immer einfach: Nett sein, schnell helfen und auf die Bedürfnisse der Kunden eingehen. Bei einem Entwickler der vielleicht sogar nur Backend-Services für Firmen internen Software schreibt, ist es weniger einfach zu definieren, wie man die Werte leben soll und wie Qualität erzeugt werden kann.

Als erstes: Was ist ein Software Entwickler eigentlich? Dabei ist es glaube ich falsch sich auf die Tätigkeit zu beschränken. Viel mehr geht es um den Zweck. Software hat keinen Selbstzweck. Software ist nicht da, weil sie Software ist. Jede Software hat einen Zweck und dieser Zweck ist gerade im Firmenumfeld ein Problem zu lösen. Wenn alles schon perfekt wäre bräuchte man keine neue Software und keine Entwickler mehr. Der beste Entwickler ist der, der sich durch seine Software selbst überflüssig macht.
Software ist dabei auch nicht die Lösung, sondern das Werkzeug um zur Lösung zu kommen. Ein Software-Entwickler entwickelt Lösungen, die mit Hilfe von Software herbei geführt werden.

Wenn wir das Einhalten der Werte und der Qualität (was Qualität ist nicht zu diskutieren, sondern in den ISO-Normen festgelegt!) als die großen Probleme der Firma sehen, ist die Software natürlich nicht die Lösung, weil keine Firma 100% automatisiert ist. Wir müssen hier in Schichten denken. Für die Mitarbeiter, die Kundenkontakt haben oder die Produkte für die Kunden verwalten ist unsere Lösung deren Werkzeug. Wir lösen das Problem, das besteht, dass die ein größeres und abstrakteres Problem lösen können. Das macht die Software nicht weniger bedeutend, denn die Basis muss stimmen, damit die großen Probleme überhaupt bewältigt werden können. Der Software-Entwickler ermöglicht es dem anderen Mitarbeiter erst effektiv arbeiten zu können.

bbcode-image


Jetzt mal konkreter, damit man sich nicht anfängt dabei m Kreis zu drehen. Welche Werte sollte ein Entwickler bei seiner Software anwenden, damit z.B. ein Mitarbeiter im Kundensupport seinen Job gut erledigen kann und der Kunde am Ende zufrieden und sicher verstanden fühlt?

Der Mitarbeiter sollte schnell alle Kundendaten sehen und bei Abfragen, sollte das System nicht einfach warten, sondern eine Meldung ausgeben, die der Mitarbeiter dem Kunden auch mitteilen kann, damit Wartezeiten erklärbar werden. Aber an sich sollte dieses nie nötig sein, weil Antwortzeiten immer verlässlich sind. Nennen wir diesen Wert der Software "responsive".

Fehler sind menschlich und unter Stress unvermeidbar und häufig. Wenn ein Kunde berechtigt oder unberechtigt Druck macht und etwas schnell gehen soll, sollte bei Fehlern immer eine brauchbare Fehlermeldung kommen oder auch versucht werden möglichst viel automatisch zu korrigieren. Niemals sollte wegen einer formal ungültigen ein Formular abstürzen, nicht funktionieren oder es erzwingen Daten erneut eingeben und damit erneut vom Kunden erfragen zu müssen. Wenn man zum dritten mal nach seiner Telefonnummer gefragt wird, wird das Benutzererlebnis echt schlecht. Nenne wir diesen Wert der Software "widerstandsfähig" oder "resilient".

Wenn Fehler in Systemen auftreten, kommen meistens viele Kundenanfragen auf einmal. Keiner will hören, dass es heute etwas länger alles dauert, weil das System durch die vielen Anfragen langsam ist. Gerade bei Sonderangeboten und überlasteten Systemen kann es sehr negativ auffallen, wenn ein Kunde etwas nicht bekommt, weil das System plötzlich nicht mehr reagiert. Systeme sollten sich schnell anpassen.
Wenn nun plötzlich 100 Kunden anstelle von 10 Kunden bedient werden müssen.. dann sollte alles noch genau so schnell und stabil sein wie sonst. Nennen wir diesen Wert der Software "elastisch", weil es flexibel Resourcen nutzen kann, wenn nötig.

Diese drei Werte kann man sehr gut auf die Meta-Begriff: Schnell, Kompetent und Zuverlässig übertragen.

Bestimmt kommen diese drei "Werte" den meisten schon aus dem Reactive Manifesto bekannt vor. Ich finde dieses orientiert sich so sehr an der alltäglichen Realität, dass es sich die Ansätze mehr als einfach nur ein Idee für gute Software-Systeme sind. An sich lässt sich damit alles beschreiben, wo Kommunikation stattfindet.

Dann gibt es noch so Werte, die an sich keine Werte sondern Selbstverständiglichkeiten oder auch gesetzliche Vorgaben sind. Z.B. Freundlichkeit. Egal ob eine andere Abteilung, ein Kollege oder ein Kunde eine Frage oder ein Problem hat, ist man freundlich. Niemand will nerven oder versteht Erklärungen aus Vorsatz nicht. Man muss natürlich auch bestimmend sein, wenn man z.B. an wichtigen Dingen sitzt und die Anfrage von der Priorität niedrig ist. Erklären und eine feste Zeit oder Einordnung geben. Man kümmert sich und der andere fühlt sich verstanden. Das ist auch Freundlichkeit.. und kompetentes Auftreten.

Und dann gibt es Datenschutz. Ist das ein Wert? Es ist gesetzlich vorgeschrieben. Ich glaube es sollte doch als Wert geführt werden, weil man sollte es leben und nicht nur ausführen, weil man es muss. Es macht alles einfacher, wenn Datenschutz gleich mit einfließt und nicht von einer anderen Abteilung später erzwungen wird. Don't be evil ist immer ein guter Ansatz!

Ich hoffe damit hab ich ein paar Ansätze und Denkanstöße geben können, womit ein Entwickler beim nächsten mal, wenn er nach Werten und Qualität gefragt, ein kleines Set an Werkzeugen an der Hand hat, um die Frage schneller beantworten zu können.


PHP API Platform: Speed run

Die Zeiten wo man eine API mit:

echo json_encode($result);
die();

in einem einfachen Controller implementierte und eher ein Beiwerk waren sind schon länger vorbei. Heute baut große Clients mit Vue.js oder React und PHP liefert keinen HTML-Code mehr aus sondern implementiert nur noch eine reine REST-API.

Bei REST-APIs mit klassischen Resources und GET, PUT, POST, PATCH, DELETE kann man alles sehr gut verallgemeinern, so dass ein Framework kaum noch Anpassungen benötigen, um eine lauffähige API bereit stellen zu können. Eines dieser Frameworks ist das in PHP implementierte API Platform. Zusammen mit Symfony 4 kommt noch schneller zu einer REST-API als mit
Spring-Boot oder Meecrowave.

Ich skizziere hier einen kleinen Speed run, der aufzeigt wie man sehr schnell eine recht komplexe REST-API implementieren kann.
Nicht alles wird für alle nötig sein, aber es gibt einen guten Überblick für eigene Projekte. Ich geh von einem vorhandenen Symfony 4
Projekt aus, in das API Platform mit integriert wird.

1. Installation:

composer require api


2. Die Resource:
Im Grunde nimmt man eine Doctrine-Entity und erweitert die um 2 Annotationen (ApiResource und ApiProperty für die Id).


namespace hp\examples\api\entity;

use ApiPlatform\Core\Annotation\ApiProperty;
use ApiPlatform\Core\Annotation\ApiResource;
use Doctrine\ORM\Mapping as ORM;

/**
* Example-Resource
*
* @ORM\Entity
* @ORM\Table(name="blubb")
*
* @ApiResource(
* )
*/
class Blubb {
/**
* @var int
*
* @ORM\Column(name="id", type="integer", nullable=false)
* @ORM\Id
* @ORM\GeneratedValue(strategy="IDENTITY")
*
* @ApiProperty(
* identifier=true
* )
*/
private $id;

/**
* @var string
*/
private $value = ''

....
....
....
}


Den Rest der Entity wie einen argumentlosen Constructor und die Getter/Setter wird sicher jeder selbst schnellst erzeugen können.

Eine Migration oder die Table per Doctrine zu erzeugen kann sicher auch jeder selber und braucht hier nicht weiter ausgeführt werden.

3. Pfad zu den Resourcen:

Hier passen wir das Paths Field in config/packages/api_platform.yaml an:

api_platform:
mapping:
paths: ['%kernel.project_dir%/src/examples/api/entity']
patch_formats:
json: ['application/merge-patch+json']
swagger:
versions: [3]


Nun weiß API Platform, wo es nach den Resources suchen soll.

4. URL-Prefix anpassen:
In der config/routes/api_platform.yaml kann noch schnell ein Prefix für die API bzw den API-Controller gesetzt werden.


api_platform:
resource: .
type: api_platform
prefix: /api/examples/


5. Starten und Testen:

Nun kann die Anwendung auch schon gestartet werden, wenn sie nicht schon ist. Also in meinem Fall den Docker-Container starten, der einen Apache auf dem Port 8080 öffnet.

Ob die Routen vorhanden sind prüfen wir mit:

php bin/console debug:router


Wenn die Routen angezeigt werden kann http://localhost:8080/api/examples/docs.json aufgerufen werden und hier sollten alle Pfade zu der Resource aufgelistet sein. Sollte die Route nicht gefunden werden hilft ein php bin/console cache:clear. Um die Resource wirklich auch testen zu können, findet man eine Swagger-Umgebung unter http://localhost:8080/api/examples/blubbs.html. Wie man sieht hängt ein 's' am dem Namen der Klasse und der Name ist auch nicht in Camelcase geschrieben. Wenn man die JSON-Ausgabe sehen möchte muss man nur ../blubbs.json anstelle der HTML-Variante aufrufen.

6. DTOs und Delegations
Nicht immer will man die Entity so raus reichen, wie sie ist. Auch will man vielleicht Objekte durch einfache Strings ersetzen. Auch beim Anlegen will man vielleicht Business-Keys und keine echten Ids nutzen, die aber in der Ausgabe dann doch zusätzlich mit angezeigt werden sollen.
Dafür kann man bei API-Platform DataTransformer und Stellvertreter-Objekte definieren. Die DataTransformer muss man nur anlegen und werden durch das autowire von Symfony direkt auch schon verwendet.

Ein einfaches Beispiel:

BlubbIn

namespace hp\examples\api\entity;

class BlubbIn {
public $value;
public $logComment;
}


BlubbOut

namespace hp\examples\api\entity;

class BlubbOut {
public $id;
public $value;
public $md5sum;
}


Nun brauchen wir auch zwei DataTransformer:

Incoming

namespace hp\examples\api\entity\transformer;

use ApiPlatform\Core\DataTransformer\DataTransformerInterface;

class BlubbInTransformer implements DataTransformerInterface {
public function transform($object, string $to, array $context = []) {
$entity = new Blubb();
$entity->setValue($object->value);

\hp\Logger::log($object->logComment);
return $entity;
}

public function supportsTransformation($data, string $to, array $context = []): bool
{
return $to === Blubb::class && null !== ($context['input']['class'] ?? null);
}
}


outgoing

namespace hp\examples\api\entity\transformer;

use ApiPlatform\Core\DataTransformer\DataTransformerInterface;

class BlubbOutTransformer implements DataTransformerInterface {
public function transform($object, string $to, array $context = []) {
$dto = new BlubbOut();
$dto->id = $object->getId();
$dto->value = $object->getValue();
$dto->md5sum = md5($object->getValue() . $object->getId());
return $dto;
}

public function supportsTransformation($data, string $to, array $context = []): bool
{
return $to === BlubbOut::class && $data instanceof Blubb;
}
}


Die beiden DTOs müssen noch an der Resource referenziert werden:

...

/**
* Example-Resource
*
* @ORM\Entity
* @ORM\Table(name="blubb")
*
* @ApiResource(
* input=hp\examples\api\entity\BlubbIn::class,
* output=hp\examples\api\entity\BlubbOut::class
* )
*/

...


Nun nutzt man das BlubbIn-Format um etwas an die API zu senden, es wird als die Entity in de DB gespeichert und als Rückgabe sieht die Daten im Format von BlubbOut. Dieses ist etwas, was ich bei vielen Frameworks immer vermisst habe... Object-Replacements wie beim Serialisieren unter Java mit writeReplace. Gerade bei REST-APIs ist so etwas extrem praktisch und man kann so auch aus einer Entity verschiedene Domain-abhängige (Bounded-Context) DTOs für verschiedene API-Endpoints erzeugen.

7. Authorization und Security
Klar sind einfache API-Keys die man im Request mitgibt nicht die beste oder finale Lösung, aber sie sind so schön einfach. In API Platform lässt sich so etwas relativ einfach implementieren. Dafür braucht man Symfony Voters, die mich etwas an die Realms aus Tomcat erinnern. Man reicht eine Role rein und die Logik liefert einfach true oder false zurück. Das Beispiel ist nicht sehr fein-granular, aber sollte reichen, um die Grundidee zu vermitteln.

Die Resource schützen:

...

/**
* Example-Resource
*
* @ORM\Entity
* @ORM\Table(name="blubb")
*
* @ApiResource(
* attributes={"security"="is_granted('EXAMPLE_API_ACTION')"},
* input=hp\examples\api\entity\BlubbIn::class,
* output=hp\examples\api\entity\BlubbOut::class
* )
*/

...


Eine ausführlichere Doku mit der Verwendung des Object findet man hier bei API Platform.

Den Voter zu implementieren ist an sich genau so einfach, wie einen DataTransformer zu implementieren und funktioniert genau nach der selben Logik, die ich auf die Art auch immer gerne implementiere.


namespace namespace hp\examples\api\security;

use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;

class ExampleAPIVoter extends Voter {

...
...
...

protected function supports($attribute, $subject) {
return $attribute == 'EXAMPLE_API_ACTION';
}

protected function voteOnAttribute($attribute, $subject, TokenInterface $token) {
return $this->apiKey == $this->getUserRequestApiKey();
}
}


Mit autowire muss man auch nicht groß etwas in die services.yaml eintragen, wenn man nichts aus der env-config oder
so braucht.

Feature-Flags: Neue Features, neue Versionen und alles beim Alten

Gerade bei den Shopware Plugins war es wichtig, dass sich die Funktionsweise eines Plugins nicht einfach ändert. Ich hab erlebt was passieren kann, wenn sich bei einen Newsletter-Plugin in einer Minor-Version plötzlich das Verhalten änderte. Es wurden plötzlich alle Kunden mit Gruppe übertragen und nicht mehr nur die einer bestimmten Gruppe und weil niemand sich die Änderungen groß angesehen hat, bekamen plötzlich alle den Newsletter. Das war doof.

Kaum jemand liest den Changelog oder nur selektiv nach bestimmten Fixes. Wenn ich als Shopware-Betreiber einen Fix für ein Plugin brauche, habe ich meistens keine Zeit jedes Verhalten und jede Funktion des Plugins nochmal durch zu testen. Ich will nur mein Problem beheben und keine neuen bekommen. Jedes neue Feature muss also in den default Einstellungen erst einmal deaktiviert sein. Nur Fixes dürfen ungefragt greifen.

Funktionen im Beta-Stadium dürfen auch nicht aktivierbar sein, wenn sie nicht stable sind. Unfertiger-Code darf mit ausgeliefert werden, wenn es nicht anders geht! ABER er muss auch hart im Code deaktiviert sein. Oder auch im Build-Prozess aus der Config-XML auskommentiert werden. Da muss dann aber sichergestellt sein, dass geprüft wird, ob das Config-Value überhaupt gesetzt ist und wenn nicht, dass es als deaktiviert gilt.
Aber wenn es nicht zwingend nötig ist, sollte man unfertige Features nie mit ausliefern und per Feature-Flag deaktivieren. Am Ende wird es sicher noch mal überarbeitet und es funktioniert dann anders. Weglassen minimiert das Risiko auf Fehler und dass irgendwer es doch aktiviert und nutzt.

Fazit: Feature-Flags sind wichtig, aber nicht dafür da um unfertige Features mit ausliefern zu können (ein Feature pro Feature-Branch!!!!)


i don't like Release-Branches

Ich halte Release-Branches für sehr problematisch. Manchmal können sie nützlich sein, aber in 99% aller Fälle sind sie überflüssig und bringen mehr Probleme mit sich. Nur wenn wirklich mehrere von einander abhängige Features gleichzeitig entwickelt werden wäre ein Release-Branch denkbar. Ob der dann auch wirklich einen Vorteil bringt, muss jeder dann für sich entscheiden.

Das Problem bei Release-Branches ist, dass alles was release-bereit ist auch da rein kommt. Schnell hat man das Problem, dass man seinen kleinen Fix da rein merged und feststellt, dass 3 weitere Features da drin liegen. Sie sind ja release-bereit, aber ein Release war noch nicht wirklich nötig. Entweder ändern die was, wo man die Benutzer noch instruieren muss oder sie sind nicht so wichtig man hat den Release auf den Ende des Sprints geschoben. Jetzt steht man da und muss erst mal von allen anderen die Info einholen, ob man die Features mit seinem Fix mit releasen kann oder man jetzt doch einen eigenen
Release-Branch aufmachen sollte.


Bloß weil etwas release-fertig ist, heißt es nicht dass ein Release möglich ist!


Die meisten Features und Fixes stehen für sich selbst und sie in einen gemeinsamen Release-Branch zu bringen blockiert schnelle Releases einzelner Features. Für jeden Feature-Branch einen eigenen Release-Branch auf zu machen ist aber auch nicht sinnvoll. Der Feature-Branch sollte meiner Meinung nach auch sein eigener Release-Branch sein... also man braucht nur den Feature-Branch.

FALSCH:
bbcode-image
blocking release-branch.. nicht gut


RICHTIG:
bbcode-image


Dieser Post fußt auf meinen Post Git State Konzept.

Summernote: Image Resizing

Das direkte Einbinden als Base64 Data-URL ist echt praktisch, aber man will ja nicht, dass die Benutzer nun endlos riesige Bilder darüber einbinden. Deswegen ist es eine gute Idee, die Bilder beim Einfügen auch gleich zu verkleinern. Das ist zum Glück mit dem Canvas-Element sehr einfach.

Die Callback-Function:

let onImageUploadFunc = function (elementId) {
return function(image) {
resizeImage(image[0], elementId);
}
};

function resizeImage(file, elementId) {
let image = new Image();
let url = window.URL ? window.URL : window.webkitURL;

image.src = url.createObjectURL(file);
image.onload = function (e) {
let canvas = document.createElement("canvas");
let ctx = canvas.getContext("2d");

let width = 600;
let factor = image.width / width;
let height = image.height / factor;

if(image.height > image.width) {
height = 300;
factor = image.height / height;
width = image.width / factor;
}

canvas.width = width;
canvas.height = height;

ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
$(elementId).summernote('editor.insertImage', canvas.toDataURL('jpeg', 0.7));
};
}


Verwendung:

$('#editSolution').summernote(
{callbacks:{onImageUpload: onImageUploadFunc('#editSolution')}}
);


bbcode-image

Release-unrelevante Änderungen direkt in den Master?

Nur ein kleiner Fix an der README.md oder ein Kommentar in der docker-compose.yml? Braucht es einen feature-Branch? An sich sind solche Änderungen es ja nicht wert die Versionsnummer zu erhöhen, weil sich am releasten Code ja nichts ändert. Aber das einfach so in den Master commiten, obwohl darin nur Abbildungen von release-Tags als Commits zu finden sollten (nach dem ersten Release)?

Das Release als lauffähiges Artefakt eines Dev-Standes würde sagen: Ja, es sollte wie eine Code-Änderung behandelt werden. Eine Version bezieht sich nicht nur auf das fertige Artefakt sondern auf auf dessen Sources aus denen das Artefakt erstellt wurde. Eine falsche Angabe in der README.md ist genau so ein Fehler wie ein Fehler im Code und könnte später zu Missverständnissen und nicht lauffähigen Artefakten führen.

bbcode-image



Also jeder Fix und jede Verbesserung soll einen eigenen Branch und einen Release-Tag erhalten.


Meine Erfahrungen aus Shopware-Plugin Zeiten sagt dann eher, dass man nicht jeden Release auch wirklich releasen muss. Wenn 0.3.2 im Vergleich zu 0.3.1 keine Verbesserung für den Enduser bringt, muss kein öffentlicher/public Release erfolgen sondern es bleibt ein privater Release, der für die Developers gedacht ist.

Docker: Test SSH-Server

Manchmal muss man z.B. das Kopieren von Dateien auf einen Server per SCP testen. Oder auch einfache Deployments auf einem Server. Hier ist ein kleines SSH-Server Image mit Bash und Rsync.


FROM sickp/alpine-sshd:7.5
RUN apk update
RUN apk add bash
RUN apk add rsync


Und in einer docker-compose.yml

version: "3.0"

services:
ssh-server:
build: .
ports:
- "2222:22"


User: root
Password: root

Man kann aber auch authorized-keys hinterlegen, wie auf der Seite des Base-Images erklärt wird.

Ist es ok mit einem dreckigen Master zu starten?

Ist es okay mit einem dreckigen Master-Branch zu starten?


Meiner Meinung nach ist es vollkommen ok. Ein leeres Projekt als stable zu deklarieren macht keinen Sinn und ist in den meisten Fällen auch nicht deploybar, da z.B. eine build.xml oder .gitlab-ci.yml noch fehlen.

bbcode-image
Master mit dirty Commit vor dem ersten Release


Ich vertrete die Meinung, dass der Master erst nach dem ersten Release stable gehalten werden muss. Es folgt der Regel, dass im Master der letzte nutzbare Stand liegt. Wenn kein Release vorhanden ist, ist der letzte Commit der Dev-Version, die nutzbarste Version die man finden kann. In dem Sinne bemühe ich bei der Regel eine Art Fallback auf die Dev-Version, für die Zeit wo kein Release existiert. Nach dem ersten Release ist immer das letzte Release die nutzbarste Version.

Es gibt also immer eine nutzbare Version und nie "Nichts".

Dieser Post fußt auf meinen Post Git State Konzept.

WSL und Vue Probleme

Wenn ich in Ubuntu unter meinem Windows 10 Node.js und NPM installiere macht es nur Probleme. "npm install" beschwert sich dass 'in' unbekannt sei.

Momentan läuft es bei mir so am Besten:


curl -sL https://deb.nodesource.com/setup_12.x | sudo -E bash -
sudo apt-get update && sudo apt-get install -y nodejs


Dann funktioniert auch die Vue-CLI Installation:


npm install -g @vue/cli
npm install -g @vue/cli-service-global


Unter Windows direkt... da war alles noch schlimmer.

Older posts:

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