TailwindCSS und VanillaJS – Step Progress Bar

Ich persönlich finde bei Bestellvorgängen oder Behördenformularen es extrem hilfreich eine sogenannte Prozessverfolgung auf dem Bildschirm zu haben. Damit weiß man stets Bescheid, wo man gerade ist und wie lange das noch in etwa dauern kann.

Ich dachte mir, es ist Samstagabend und die Kinder sind im Bett und die bessere Hälfte macht noch Steuern, dann baue ich mir doch mal eine „Step Progress Bar“ selber. Allein nur mit den Zutaten TailwindCSS und VanillaJS. Viel besser als Katzenvideos auf Youtube oder Netflix.

Meine Vorüberlegung:

– ich setze flexibel mit der Klasseninitialisierung die einzelnen Schritte mit ihren Namen

– die „Step Progress Bar“ Klasse hat eine Funktion mit der ich zwischen den einzelnen Schritten springen kann. Somit kann man sie in anderen Projekten mit einer Callback Funktion aufrufen

– die „Step Progress Bar“ Klasse rendert nach jeder Aktualisierung die View automatisch

Als erstes brauchen wir TailwindCSS. Das besorge ich mir der Einfachheit über CDN. Dann benötige ich ein HTML Gerüst in dem ich die Steps / Schritte anzeigt.

<section><!-- Q: https://tailwindcomponents.com/component/steps-bar -->
    <!-- component -->
    <div class="container mx-auto py-6" >
      <div class="flex" id="ProgressStepper">
      </div>
    </div>
 </section>

Dann gehen wir zum JS Teil über. Ich baue eine Klasse ProgressStepper.

<script>
  // Progress Stepper Class
   class ProgressStepper {
        constructor(paras) {
            this.step_names = paras.step_names;            
            this.reached_step = 1;
            this.paras = paras;
            this.build_progress_bar();
        }

        setPointerToStep(step) {
            if (step > this.step_names.length) {
                console.log("step>length")
                return ;
            }
            this.reached_step = step;
            this.build_progress_bar();
        }

        build_progress_bar() {

            let _html_start_part = function(data, index, reached = 0) {
                return `<!-- progress item -->
                <div class="w-1/4">
                  <div class="relative mb-2">
                    <div class="w-10 h-10 mx-auto bg-green-500 rounded-full text-lg text-white flex items-center">
                      <span class="text-center text-white w-full">
                        ${index} 
                      </span>
                    </div>
                  </div>

                  <div class="text-xs text-center 2xl:text-base">${data}</div>
                </div>
                <!-- /progress item -->`};

            let _html_start_part1 = function(data, index, reached = 0, procentBar = 0, compleated_icon = '') {
                return `<!-- progress item middle -->
                <div class="w-1/4">
                  <div class="relative mb-2">
                    <div class="absolute flex align-center items-center align-middle content-center" style="width: calc(100% - 2.5rem - 1rem); top: 50%; transform: translate(-50%, -50%)">
                      <div class="w-full bg-gray-200 rounded items-center align-middle align-center flex-1">
                        <div class="w-0 bg-green-300 py-1 rounded" style="width: ${procentBar}%;"></div><!-- middle part 100 full & 50 half progressstatus-->
                      </div>
                    </div>

                    <div class="w-10 h-10 mx-auto ${reached == 0 ? 'bg-white border-2 border-gray-200': 'bg-green-500' } rounded-full text-lg text-white flex items-center">
                      <span class="text-center ${reached == 0 ? 'text-gray-600': 'text-white' } w-full">                        
                        ${index}
                      </span>
                    </div>
                  </div>

                  <div class="text-xs text-center 2xl:text-base">${data}</div>
                </div>`;}
            
            var output = '';
            for (let i=0; i<this.step_names.length;i++)
            {                
                if (i==0) 
                {                    
                    output += _html_start_part(this.step_names[i], i+1);
                    continue;
                }
                let reached = this.reached_step >= (i+1) ? 1:0;
                let procentBar, compleated_icon;
                if (this.reached_step >= (i+1)) {
                    procentBar = 100;
                    compleated_icon = this.paras.compleated_icon;
                } else if (this.reached_step > (i+1)) {
                    procentBar = 0;
                } else if(this.reached_step == (i)) {
                    procentBar = 50;
                }
                output += _html_start_part1(this.step_names[i], i+1, reached, procentBar, compleated_icon);
            }

            document.getElementById('ProgressStepper').innerHTML = output;
            return ('build')
        }
   }
</script>

Nun benötige ich noch die einzelnen Schritte. Ziel war es ja ein Shop Checkout zu bauen. Dann nehme ich mal Adressem Zahlungsart, Bestätigen und Completed bzw. Fertig.

<script>
   let paras = {
    step_names: ['Adresse', 'Zahlungsart', 'Bestätigen', 'Fertig'],  
    }

    let p = new ProgressStepper(paras);
</script>

Nun kann man mit der Methode setPointerToStep() auf den Step springen wo man gerade hin möchte. Scotty step me up …

p. setPointerToStep(2);
p. setPointerToStep(0);
p. setPointerToStep(99);
Demo Step Progress Bar

Man kann jetzt hier ordentlich noch was machen:
– Setze einen grünen Hacken sobald der Prozess über den Schritt hinaus ist
– Gebe mit dem Namen auch die Links mit um Sprungmarken zu haben
und noch vieles Mehr …

Tailwind CSS – Ein kurzer Einstieg

Seit Jahren nutze ich Bootstrap. Ich dachte nicht mal im Traum dran, dass es etwas Besseres im Punkto Websitedesign geben wird. Es ist einfach, praktisch und man kann relative schnell ansprechende bzw. aufgeräumtes Designs umsetzen. Allerdings hatte Bootstrap für mich immer einen bitteren Beigeschmack. Ein Gedanke den ich nie zulassen wollte. Und zwar: Bootstrap sieht in 99 Prozent der Fälle aus wie Bootstrap. Es ist auch immer ein größerer Anbach eigene Desigelemente zu implementieren. Bootstrap Formulare sind zwar praktisch aber in den seltensten Fällen schön. Die Navbar wirkt auch immer irgendwie wie bürokratisch / beamtenmäßig. Und am meisten störte mich das man bis Bootstrap 4 Jquery implementieren musste. Ein Javascript Framework was ich immer seltener nutzen mag. Alles im Allen: Bootstrap Webseiten sehen auch immer irgendwie aus wie Backendentwickler (Jeans, Jack Wolfskin Jacke, allways functional Clothes) – langweilig.

In einem aktuellen Projekt musste ich mich mit Tailwind CSS beschäftigen. Zuerst dachte ich alter Wein in neuen Schläuchen und hatte auch nicht wirklich Bock auf ein Weiteres Möchtegern fancy Entwicklungstool. Wie bereits Eingehens geschrieben: „Bootstrap for ever!“

Gamechanger – Der Utility First Ansatz von TailwindCSS

Aber dann kam TailwindCSS. Und ziemlich schnell merkte ich, dass Tailwind das Zeug hat zum Gamechanger. Das Konzept ist ist eigentlich nichts weltbewegendes aber niemand hat es zuvor in diesem Umfang und konsequent durchgesetzt. Das Konzept heißt: Utility First. Statt vordefinierte Klassen und Komponenten nutzt man Toolsets und definiert sein Design selbst. Man arbeitet nur noch mit Klassen im HTML und schreibt kein CSS mehr. Um das Verstehen zu lernen gehen wir umgehend in den praktischen Teil über. In meinem Beispiel möchte ich mit Euch eine einfache Shopdetailseite bauen.

Shop Produkt Übersicht mit TailwindCSS und Alpine.js

Link zur Demo BikeShop Test Seite

Und hier der Link zum Tutorial um dieses Ziel zu erriechen. (coming soon)

Object Orientierte Programmierung in Javascript

Vor 10 Jahren hielt ich immer Abstand von Javascript. Nur wenn es sein musste habe ich mich mit JS beschäftigt. Das waren mir zu viel geschweifte Klammern und Callbacks und es haftete an Javascript ein schlechtes Image, was die Sicherheit anging, an. Um auch ganz ehrlich zu sein, ich hatte Probleme Javascript Code richtig zu lesen. Heute sieht die Welt ganz anders aus. Mittlerweile liebe ich Javascript! Damals nutzte ich für meine Arbeiten dann auch Jquery, da es mir übersichtlicher erschien. Heute nutze ich kein Jquery dafür ausgereifte Javascript Frameworks wie Vue Js oder einfach nur Vanilla JS. Das ich konsequent in meine Projekte umsetze. Ich mache beim Kunden auch kein Hehl aus meiner „Abneigung“ zu Jquery. Es ist zwar nicht wirklich eine Abneigung im klassischen Sinn. Ich nutzte es halt nicht mehr gerne. Unnötiger „Perfomancefresser“.

Das schöne ist, man kann mit JS auch Objekt orientiert entwickeln. Im folgenden Zeige ich Euch ein einfaches Beispiel wie man sauber ein Javascript Objekt erstellt und nutzt.

Ich werde nun ein Warenkorb Objekt erstellen.

Anforderung:

  • wir wollen Produkte zu einem Warenkrob (Cart) hinzufügen (addToCart)
  • wir wollen Produkte aus dem Warenkrob löschen (removeFromCart)
  • der Warenkorb soll im Browser LocalStorage gespeichert werden (save)
  • wir wollen die aktuelle Anzahl der Produkte ermitteln (getCartCount)
  • wir wollen die aktuelle Warenkorb Preis / Summe ermitteln (getCartSum)
class Cart {
        constructor(cart_name) {
            this._cart_name = cart_name;                    
            this._cart;
            this.init();            
        }

        init() 
        {
            let _storage = localStorage.getItem(this.cart_name);
            if (_storage === null) 
            {
                // create new basket Element with an empty array                
                localStorage.setItem(this.cart_name, JSON.stringify([]));
            }  
            this._cart = JSON.parse(localStorage.getItem(this.cart_name));          
            this.refreshViewPoints();           
        }

        get cart_name() {
            return this._cart_name;
        }
        set cart_name(value) {
            this._cart_name = value;
        }

        get cart() {
            return this._cart;
        }

        getCart() {
            return (this.cart);
        }

        deleteWholeCart() {
            this._cart = null;          
            localStorage.removeItem(this.cart_name);
            this.init();
        }

        addItem(id ,q = "1") {
            id = String(id);
            q = String(q);
            console.log("addItems " + id +" with "+ q)
            // check if item_id allready exists and modify cart item or create new cart item
            let is_new = true;
            for (let i=0; i < this._cart.length; i++) {
                if (this._cart[i].product_id === id) {
                    this._cart[i].q = q;
                    is_new = false;
                }
            }   

            if (is_new === true) {              
                this._cart.push( {"product_id":id, "q":q} );   
            }
            
            this.saveCart();
            this.refreshViewPoints();
        }

        saveCart() {
            let _storage = localStorage.getItem(this.cart_name);            

            localStorage.setItem(this.cart_name, JSON.stringify(this._cart));           
            this._cart = JSON.parse(localStorage.getItem(this.cart_name));          
        }

        refreshViewPoints() {            
            document.getElementById("myBasketBadge").innerHTML = this.getCart().length;
            myBasketCount = this.getCart().length;
            document.getElementById("sum").innerHTML = this.calculateCartSum();
            this.renderBasketOverview();
            document.getElementById("json").innerHTML = JSON.stringify(this.getCart());
        }

        calculateCartSum() {   
            if(this.cart.length === 0) {
                return 0;
            }
            
            let sum = this._cart.map((item) => {

                let item_data = pl.filter(function(pl_item) {
                    return pl_item.id == item.product_id
                });

                return (item.q * item_data[0].productPrice);
            });
            console.log(sum);
            return sum.reduce(function(a, b){
                    return a + b;
            }, 0);                      
        }

        deleteItemFromCart(id) {
            this._cart = this._cart.filter(function(item) {
                return item.product_id !== id
            });         
            this.saveCart();
            this.refreshViewPoints();           
        }

        renderBasketOverview() {    
            this.renderBasketContainer();            
        }

        renderBasketContainer() {
            let container = document.getElementById("basketOverviewContainer");                     
            container.innerHTML = '';
            let tbl  = document.createElement('table');
            tbl.style.width  = '100px';

            tbl.style.border = '1px solid black';           
            for(let i = 0; i < this.getCart().length; i++){
                let tr = tbl.insertRow();
                let td = tr.insertCell();
                let str = "id:" +this.getCart()[i].product_id + " q:" + this.getCart()[i].q;
                td.appendChild(document.createTextNode(str));
                td.style.border = '1px solid black';                
            }
            container.appendChild(tbl);                  
        }       
    }

Ihr könnt das Projekt auch aus meinem GitHub klonen.

https://github.com/MikeLowrey/js-shopping-cart-oop

Teufel Rockster Go – Der dicke Stinker

Ich bekam zu Weihnachten ein vermeintlich tolles Geschenk. Und zwar Bluetooths Lautsprecher. Und dann auch noch Teufel Boxen. Wer Teufel nicht kennt. Es ist eine Berliner Lautsprecher Marke. Sie ist bekannt für basslastige mittelpreisige Boxen. Der Ruf von Teufelboxen ist jedenfalls nicht schlecht und spricht Leute an die halt gerne basslastige Mucke hören. Bei anderen Musik Genres hört man spürbar das diese Boxen nicht das NonPlusUltra der Akustikwiedergabe sind. Was jetzt Teufel im allgemeinen nicht angekreidet werden soll, da sie vielleicht genau das so wollen. Um aber „Around The World“ von Daftpunk im Park zu hören ist sie jedenfalls voll empfehlenswert. Und da sind wir auch schon an den Punkten. Wer eine feine Nase hat rate ich vom kauf der Teufel Rockstar Go stark ab. Sie riecht sehr stark nach Chemie. Für mich ist sie olfaktorisch in geschlossenen Räumen nicht zu ertragen ist. Das ungewollte Geruchdesign von dieser Teufel Box wirkt billig und womöglich gesundheitsgefährdend. Ich jedenfalls musste mir jedes mal die Hände waschen um den den chemischen Geruch von den Händen zu kriegen. Das ist wirklich schade, da sie technisch fast ordentlich wirkt. Es gab kaum Probleme mit der Bluetooth Verbindung. Ältere Android Handys können sich zwar verbinden aber sie erkennen die Box nicht zum Audio abspielen oder der Sound klingt sehr schlecht wie eine überlaute Monoboxradio aus den 70igern. Was aber eher ein Problem des Clients (als Handys) ist. Mit neuen IPhones ist der Sound gut. Das beste Soundergebnis lieferten IPhone und USB Verbindungen. Letzteres steht natürlich klar im Widerspruch einer mobilen Box. Ebenso etwas merkwürdig empfinde ich die Haptik der Steuerungstasten an der Box. Sie geben kein wirkliches Feedback beim drücken. Die technisch Reaktion kommt dazu auch noch etwas zeitverzögert. So das man eigentlich das Gefühl hat ein fehlerhaftes Gerät erworben zu haben. Ein weiterer Punkt ist das unpraktische und aus meiner Sicht unschöne Produktdesign. Man kennt die anderen Boxen die Rund sind oder wie Staffelstäbe aussehen. Boxen die man auch gerne in die Hand nimmt, weil sie sich praktisch anfühlen. Die Rockster Go Teufelbox ist klobig und wirkt dadurch befremdlich. Nun kann man annehmen das Teufel seine Herkunft als reiner Boxenhersteller hier widerspiegeln möchte. Aber dann fragt sich der enttäuschte Endkunde, warum bietet ihr dann überhaupt mobile Boxen an?

Alles im allen muss dieses Geschenk zurück gehen. Wäre der beißende Geruch nicht, würde sie aus den anderen Gründen auch an Teufel zurück gehen. Aus anderen Rezessionen im Netz wurde auf den Geruch auch bereits verwiesen und das der Geruch auch nach gründlichen abwaschen immer noch fortbestand.

Trotzdem muss ich für die originale Teufel Rockster eine Lanze brechen. Die große und originale Teufel Rockster ist ein Wahnsinns Teil. Mit einem Freund und Kollegen haben wir uns die auf ein Fahrradanhänger geschnallt und sind damit zum Mauerpark gefahren. Kaum angekommen, war das Handy auch schon mit der Box connected und es ging los. Wir haben an einem Sonntag Abend im Sommer den halben Mauerpark mit einer Box beschallt. Leute kamen und freuten sich zusammen mit uns. Es war einfach ein bassiges schönes Erlebnis. Aus diesem Grund hatte ich auch eine ähnliche Erwartung an die Rockster Go für ca. 140 Euro. Natürlich in einem anderen Maßstab. Soundtechnisch konnte die Box Maßstabsgetreu erfühlen. Aber das drumherum der Box ist ausbaufähig.

Alpine.js – Ein kurzer Einstieg

Nicht alle Webprojekte müssen zwangsläufig darauf hinauslaufen sie mit Javascript Frameworks wie Vue JS, React oder Angular umzusetzen. In den Jahren 2015 bis 2020 war das schon ein spürbarer Trend. Man betrachtet nicht mehr den Overhead von Frameworks bei kleinen Projekten sondern wie schnell man ohne lange Nachzudenken Projekte bauen kann. Allerdings fällt dieser Ansatz spätestens bei neuen Anforderungen einen auf die Füße. Ich meine jetzt nicht, dass es schlecht ist Frameworks zu nutzen. Ich baue sehr gerne Webapplikationen mit Vue JS. Aber kleine Projekte oder auch größere mit geringem dynamischen Frontendelementen bauen ich mittlerweile und mit vorliebe in Vanilla JS. Seit dem ich das mache wächst meine Javascript Passion von Codezeile zu Codezeile.

Heute möchte ich euch die Javascript Bibliothek Alpine.js vorstellen. Alpine ist quasi ein mini Miniframwork. Es bietet dem Nutzer die Reaktivität von größeren Framworks wie Vue oder React aber das zu deutlich weniger Rechenleistung und Code. Eigentlich genau das was man für ein kleines ansprechendes Webprojekt braucht.

Test Shop Seite gebaut mit TailwindCSS und Alpine.js.
Test Shop Seite gebaut mit TailwindCSS und Alpine.js. Zur Demo Seite.

Installation

Alpine.js kann man sich über CDN oder über NPM ins Projekt holen. In unserem Beispiel nutzen wir CDN. Ziel unseres Projekts wird sein, eine Shoping Cart Seite eines Fahrradshops zu bauen mit dynamischen Warenkorbmenü und Produktinformationen in Tab Style.

<script src="https://cdn.jsdelivr.net/gh/alpinejs/alpine@v2.8.0/dist/alpine.min.js" defer></script>

Wie funktioniert Alpine.js

Es ist wirklich ganz einfach und am besten ich erkläre euch das in Schritten.

  1. Deklaration der Variablen im Scope. D.h. man definiert einen Container und deklariere im Start HTML Tag die Variablen die dynamisch sein sollen.
<div x-data="{ isOpen: false }">...</div>

2. In diesem Container hat Alpine.js nun zugriff auf die Variable isOpen. Sie ist mit false initialisiert worden. Im Container befindet sich ein Button der ein bestimmtes Textfeld anzeigen oder ausblenden soll (toogle).

<button @click="isOpen = !isOpen" @keydown.escape="isOpen = false" >klick</button>

Wiederrum im gleichen Container befindet sich auch der Content Block bzw. Textfeld der ein- und ausgeblendet werden soll.

<div x-show="isOpen">Lorem ipsum </div>

Hier kurz das ganze Beispiel:

<div x-data="{ isOpen: false }">
	<button @click="isOpen = !isOpen" @keydown.escape="isOpen = false" >klick</button>
	<div x-show="isOpen">Lorem ipsum</div>
</div>

Ähnlich wie bei Vue Js können wir auch dynamische Klassen hinzufügen. In unserem Beispiel wollen wir die Buttonfarbe zwischen rot und grün wechseln. Grün offen und Rot zu.

<div x-data="{ isOpen: false }">
   <p x-text="isOpen"></p>
   <button class="focus:outline-none" @click="isOpen = !isOpen">
      <span class="px-2 py-1 bg-red-700 text-white" :class="{ 'bg-green-300' : isOpen }">
   klick
      </span>
   </button>
   <div x-show="isOpen">
      Lorem ipsum                
   </div>                    
</div>

Das ist nur eine kleine Auswahl an Möglichkeiten. Alpine.js harmoniert blendend mit TailwindCSS.

Die eben aufgeführten Beispiele reichen aber bereits um das Tutorial Ziel zu erreichen. Hier der

Link zur ShopTest Seite

die ich mit Alpine.js und TailwindCSS umgesetzt habe. Dynamisch sind hier das toogle des Warenkorbes, Profilmenü sowie in der mobilen Ansicht das Burgermenü.

Links
Hier ein Link zu einer Seite die viele Alpine.js Snippets für euch bereit hält.
  • https://www.alpinetoolbox.com/examples/
  • https://github.com/alpinejs/alpine

PHP Das doppelte Fragezeichen

Ich erinnere mich noch an den Tag, wo PHP 7 kam. Da war ich zwischenzeitlich mal Angestellter bei einem Berliner Reise StartUp in Prenzelberg. Bis dahin schrieben wir unsere Anwendung in PHP 5.6 und es war ein wilder Mix aus Prozedural und Objektorientierung. Das war schon eine spannende Sache und man merkte das PHP den Schritt zu einer seriösen Anwendungsprogrammiersprache setzte. Weg vom PHP Scriptkidies hin zu modularen und Objektorientierten. Ich war schon dran komplett zu JAVA Welt zu wechseln, weil mich einfach in PHP viele Dinge störten. Aber es kam anders und ich entwickel heute meine meisten Projekte in PHP oder Javascript.

Heute geht es um den „null coalescing operator“ bzw. den Null-Koaleszenz-Operator. Vielleicht habt ihr den schon mal gesehen. Ein Beispiel:

$currency = $customer_currency ?? 'EUR';

Und ist nichts anderes als:

$currency = isset($customer_currency) ? $customer_currency: 'EUR';

// bzw. 
if ( isset($customer_currency) ) {
	$currency= $customer_currency;
} else {
	$currency = 'EUR';
}

Der „null coalescing operator“ ist seit PHP 7 eingebaut und vereinfacht und verkürzt an manchen Stellen das schreiben von Code. In C und anderen Sprachen gibt es den bereits schon länger. Eine feine Sache.

Einstieg in axios.js

Das tägliche Brot eines Webentwicklers sind XHR / Ajax Anfragen zu generieren. Also man fragt an einer Schnittstelle nach Daten an. Das ist auch ein alter Hut. Dafür gibt es auch nicht einen Weg sondern viele. Und keiner ist richtiger oder falsch. Es ist immer Geschmackssache und vom Fall abhängig. Ich bevorzuge in meiner Javascript Programmierung immer den reinen Weg. Also Vanilla JS. Ich bilde mir ein damit den Clients (also die Webbrowser der Benutzer) zu schonen und somit auch ökologisch Nachhaltig zu entwickeln. Allerdings gibt es Kunden die wollen schnell und günstig eine Lösung und für die gibt es Javascript Frameworks wie zum Beispiel Jquery.

An sich ist Jquery auch eine ganz nette Sache aber ich nutze es wirklich fast nie. In meiner Praxis ist mir zum Thema Jquery aufgefallen, dass ganz gerne und häufig Webdesigner die wenig mit Programmierung und Programmierkonzepte zu tun haben, Jquery benutzen. Bis nach ein zwei Jahren der Kunde von ihnen feststellt, dass ihre Seite ganz schön langsam und auch ganz schön bugy (Mehrzahl von bug) ist läuft. Dann wird ein Softwareentwickler bzw. Frontendentwickler mit der Sache vertraut gemacht. Und dieser sieht nach 2 Tagen Analyse des Spagetti Codes auch nicht mehr so ganz gesund aus. Das sind die Momente wo man erkennt, schnelle und günstige Softwareentwicklung wird am Ende des Tages immer die teurere Variante sein. Lieber gleich zum guten Handwerker gehen statt diesen zu beauftragen wenn es zu spät ist.

Aber es gibt auch Javascript Bibliotheken die man genau für diese Fälle in sein Projekt einbinden kann. Somit muss man nicht gleich ein ganzes JS Framework wie Vue JS oder ReactJS nutzen. Für ein geschmeidiges XHR Handling vertraue ich aber auch Vue JS Axios.

Zum Vergleich stelle ich Euch mal Axios zu Fetch gegenüber.

GET Request Axios vs. Fetch

function get_data_with_fetch() {
	try {
		return fetch('https://jsonplaceholder.typicode.com/todos/1')
		  .then(response => response.json())
	} catch (error) {
		console.error(error)
	}
}
get_data_with_fetch().then(json => {
	console.log("get_data_with_fetch",json)
	let o = document.getElementById("output1");
	o.innerHTML = JSON.stringify(json)
})


function get_data_with_axios() {
	try {
		return axios({
		  url: 'https://jsonplaceholder.typicode.com/todos/2',
		  method: 'get'
		})
	} catch (error) {
		console.error(error)
	}	
}
get_data_with_axios().then(res => {
	console.log("get_data_with_axios",res)
	let o = document.getElementById("output2");
	o.innerHTML = JSON.stringify(res.data)
})

POST Request Axios vs. Fetch


function post_data_with_fetch() {
	try {
		return fetch('https://jsonplaceholder.typicode.com/posts', {
		  method: 'POST',
		  body: JSON.stringify({
		    title: 'foo',
		    body: 'fetch',
		    userId: 1,
		  }),
		  headers: {
		    'Content-type': 'application/json; charset=UTF-8',
		  },
		})
	} catch (error) {
		console.error(error)
	}	
}
post_data_with_fetch().then((response) => response.json())
  .then((json) => {
	console.log("post_data_with_fetch",json)
	let o = document.getElementById("output3");
	o.innerHTML = JSON.stringify(json)
})


function post_data_with_axios() {
	try {
		return axios.post('https://jsonplaceholder.typicode.com/posts', {
		    title: 'foo',
		    body: 'axios',
		    userId: 1,
		})
	} catch (error) {
		console.error(error)
	}	
}
post_data_with_axios().then((json) => {
	console.log("post_data_with_axios",json)
	let o = document.getElementById("output4");
	o.innerHTML = JSON.stringify(json.data)
})

DEMO

Laravel config Datei anlegen

Ich musste mal bei einer bestehenden Laravel Installation eine Paypal Integration bei einem Kunden vornehmen. Nebenbei bemerkt möchte ich das mal loswerden. Paypal hat eine schreckliche Dokumentation. Sie ist überhaupt nicht intuitiv. Aber das ist überhaupt ein anderes Thema.

Ich installierte im Projekt über den Composer die Paypal SDK:

composer require paypal/paypal-checkout-sdk 

Zusätzlich legte ich mir einen neuen Order Services unter app. Dort legte ich eine neue Datei names PaypalClient an. Diese beinhaltete die Logik um mit dem Paypal server in Kontakt zu treten um zum Beispiel eine Bestellung und deren Details sich ausgeben zu lassen. Das kann zum Beispiel und ist in meinem Fall auch so, wichtig um den Bestellvorgang in meiner Datenbank zu hinterlegen. Die PaypallClient Klasse benötigt dazu aber Credentials (Client_id, Secret). Diese kann man natürlich direkt aus der .env laden. Aber eleganter wäre es die aus einer eigens benannten config Datei zu holen. Die folgerichtig auch im config Ordner liegt. Wir legen also eine neue Datei unter config an. Und fügen folgenden Code dort ein:

<?php
/**
 * PayPal Setting & API Credentials
 */
return [
    'client_id' => env('PAYPAL_CLIENT_ID', ''),
    'secret' => env('PAYPAL_SECRET', ''),
];

Und in der env fügen diese zwei zeilen ein:

PAYPAL_CLIENT_ID=xxx
PAYPAL_SECRET=yyy

Und an dem Ort wo wir nun die Credentials benötigen – in meinem Fall nun in der PaypalClient Klasse – holen wir uns die Daten mit

$client_id = config('paypal.client_id');
$secret = config('paypal.secret');

Das wirkt jedenfalls aufgeräumter und hat Potenzial bei zukünftigen Herausforderungen mit dem Kunden flexibler zu arbeiten. Zum Beispiel könnt ihr ja in der paypal.php Config Datei einen automatischen switch zwischen Production und Development Umgebung vornehmen.

Problem mit dem Composer update/install und der PHP Version

Erst vor kurzem habe ich für eine Agentur eine Laravel Anwendung gebaut. Ich habe sie lokal entwickelt und nachdem sie fertig und getestet war dem Kunden vorgestellt. Da ich noch keine Servercredentials von der Agentur hatte, habe ich das Projekt in mein GitHub gelegt und auf meinen Server geclont und dort die Anwendung konfiguriert (Datenbank , Laravel Security Key und alles was mit der .env zu tun hat).

Dann composer install sowie artisan migrate durchlaufen lassen. Somit konnte ich der Agentur das Ergebnis zeigen und sie konnten testen. Das alles Remote von zu Hause.

Nachdem sie zufrieden waren, schickten sie mir ihre Server Zugangsdaten und ich machte das gleiche Prozedere wie auf meinem Server. Also über SSH auf den Server gelogt und los. Allerdings kam ich hier nur bis composer install. Denn der Composer meldete folgende Fehlermeldung:

Problem 1
    - Root composer.json requires php ^7.3 but your php version (7.2.34) does not satisfy that requirement.
  Problem 2
    - laravel/framework is locked to version v8.10.0 and an update of this package was not requested.
    - laravel/framework v8.10.0 requires php ^7.3 -> your php version (7.2.34) does not satisfy that requirement.
  Problem 3
    - mockery/mockery is locked to version 1.4.2 and an update of this package was not requested.
    - mockery/mockery 1.4.2 requires php ^7.3 || ^8.0 -> your php version (7.2.34) does not satisfy that requirement.
  Problem 4
    - nunomaduro/collision is locked to version v5.0.2 and an update of this package was not requested.
    - nunomaduro/collision v5.0.2 requires php ^7.3 -> your php version (7.2.34) does not satisfy that requirement.
  Problem 5
    - phpunit/php-code-coverage is locked to version 9.2.0 and an update of this package was not requested.
    - phpunit/php-code-coverage 9.2.0 requires php >=7.3 -> your php version (7.2.34) does not satisfy that requirement.
  Problem 6
    - phpunit/php-file-iterator is locked to version 3.0.5 and an update of this package was not requested.
    - phpunit/php-file-iterator 3.0.5 requires php >=7.3 -> your php version (7.2.34) does not satisfy that requirement.
  Problem 7
    - phpunit/php-invoker is locked to version 3.1.1 and an update of this package was not requested.
    - phpunit/php-invoker 3.1.1 requires php >=7.3 -> your php version (7.2.34) does not satisfy that requirement.
  Problem 8
    - phpunit/php-text-template is locked to version 2.0.3 and an update of this package was not requested.
    - phpunit/php-text-template 2.0.3 requires php >=7.3 -> your php version (7.2.34) does not satisfy that requirement.
  Problem 9
    - phpunit/php-timer is locked to version 5.0.2 and an update of this package was not requested.
    - phpunit/php-timer 5.0.2 requires php >=7.3 -> your php version (7.2.34) does not satisfy that requirement.
  Problem 10
    - phpunit/phpunit is locked to version 9.4.1 and an update of this package was not requested.
    - phpunit/phpunit 9.4.1 requires php >=7.3 -> your php version (7.2.34) does not satisfy that requirement.
  Problem 11
    - sebastian/cli-parser is locked to version 1.0.1 and an update of this package was not requested.
    - sebastian/cli-parser 1.0.1 requires php >=7.3 -> your php version (7.2.34) does not satisfy that requirement.
  Problem 12
- sebastian/code-
…

Auch ein composer update lief ins Leere.

Loading composer repositories with package information
Updating dependencies
Your requirements could not be resolved to an installable set of packages.

  Problem 1
    - Root composer.json requires php ^7.3 but your php version (7.2.34) does not satisfy that requirement.
  Problem 2
    - laravel/framework[v8.12.0, ..., 8.x-dev] require php ^7.3|^8.0 -> your php version (7.2.34) does not satisfy that requirement.
    - laravel/framework[v8.0.0, ..., v8.11.2] require php ^7.3 -> your php version (7.2.34) does not satisfy that requirement.
- Root composer.json requires laravel/framework ^8.0 -> satisfiable by laravel/framework[v8.0.0, ..., 8.x-dev].

Die Fehlermeldung ist dahingehen komisch, da ich auf dem server php 7.3 installiert bekommen habe. Also php -v gibt php 7.3 an.

PHP 7.3.23 (cli) (built: Oct  2 2020 14:41:47) ( NTS )
Copyright (c) 1997-2018 The PHP Group
Zend Engine v3.3.23, Copyright (c) 1998-2018 Zend Technologies
    with the ionCube PHP Loader (enabled) + Intrusion Protection from ioncube24.com (unconfigured) v10.3.4, Copyright (c) 2002-2019, by ionCube Ltd.
(10:20:50) [ve] 

Das bedeutet für mich, dass Composer davon ausgeht – warum auch immer – dass php 7.2 auf dem server installiert ist. Was ja auch richtig ist. Aber PHP hatte nachträglich einen Symlink auf die php7.3 gesetzt bekommen. Und das war sehr wahrscheinlich das Problem. Das erstemal wo composer lief, war standardmäßig noch php 7.2 aktiviert und wurde erst nachträglich auf php7.3 gesetzt. Composer merkte sich also die alte PHP Version. Also muss ich composer klar machen, vergiss die Alte und nimm die Neue 😉

Das macht man im nicht realen Leben über die Kommandozeile:

composer clear-cache
composer self-update
composer install --ignore-platform-reqs

Es lief durch und damit … Problem gelöst! Hoffe bei Euch auch?!

Was ist eigentlich protected $guard im Laravel Model

Gleich vorweg! Im Laravel Model gibt es mehrere protected Klassenvariablen die durch die Vererbung von Models bzw. Authenticatable im eigenen Model verfügbar sind. So habt ihr bestimmt schon mit protected $fillable und protected $guarded oder protected $hidden zu tun gehabt?

Leicht zu verwechseln mit $guarded ist nun $guard. Allerdings schützt $guarded nur das Model vor Mass Assignment. Also für unerwünschten modifizieren / updated oder erstellen eines Datenbankeintrages für die Tabelle für welches das Model steht. Hier eine genaue Erklärung zum Thema Mass Assignment.

Die Member $guard allerdings wird bei der Authentifizierung / Authentification benötigt. Sie gibt an welcher Guard für dieses Model zuständig ist. Es gibt bei Laravel von Hause aus zwei Guards. Ihr findet sie definiert in der config/auth.php.

    'guards' => [
        'web' => [
            'driver' => 'session',
            'provider' => 'users',
        ],

        'api' => [
            'driver' => 'token',
            'provider' => 'users',
            'hash' => false,
        ],

        // neuer Guard
        'customer' => [
            'driver' => 'session',
            'provider' => 'customers',            
        ],   
[...]
    'providers' => [
        'users' => [
            'driver' => 'eloquent',
            'model' => App\Models\User::class,
        ],
        // neuer Provider
        'customers' => [
            'driver' => 'eloquent',
            'model' => App\Models\Customer::class,
        ],

Wie ihr bereits am Code erkennt, habe ich im Beispiel ein Multiple Login. Soll heißen, einmal für die Kunden und einmal für die Admins / Mitarbeiter (users) eines Laravel angelehnten Shops.

Wenn sich ein Kunde nun einloggt wird seine Anfrage an den LoginController weitergeleitet. Im Constructor des LoginControllers bestimmen wir mit welchem Authentifizierungs Guard er die Anfrage kontrollieren soll.

class LoginController extends Controller
{
    [...]
    public function __construct()
    {
        $this->middleware('guest:customer')->except('logout'); 
    }
[...]
}

SeoTheater Autoren