Programmierung
VPN einrichten – was gibt es zu beachten?
Du hast schon einmal etwas von VPN gehört, bist Dir aber noch nicht so sicher, was es damit auf sich hat? Dann bist Du hier genau richtig. Wir verraten Dir alle wichtigen Informationen, die Du zum Thema VPN wissen musst. Was bedeutet das eigentlich, wie kann man es einrichten und was gibt es ansonsten noch zu beachten?
Was ist VPN?
Ein Virtual Private Network, kurz VPN ermöglicht es Dir ganz einfach eine sichere Verbindung zwischen dem Gerät, zum Beispiel einem Computer, Smartphone oder Tablet und einem entfernten Server herzustellen.
VPN in Deutschland wird gerne genutzt und bietet Dir eine Reihe von Vorteilen zum Beispiel im Bereich Sicherheit und Datenschutz. Dein Internetverkehr wird verschlüsselt, das bedeutet, dass der Zugriff von Dritten erschwert wird und niemand Deine Daten abfangen und lesen kann.
Zusätzlich lassen sich die geografischen Beschränkungen umgehen. Durch die Verwendung von VPN kannst Du also auch auf Inhalte zugreifen, die normalerweise aufgrund einer geografischen Einschränkung nicht verfügbar wären. Dabei handelt es sich beispielsweise um das Streamen von Inhalten aus anderen Ländern.
VPN bietet auch einen großen Vorteil im Bereich Remote-Arbeit. Mittels VPN kann ein sicherer Zugriff auf die Unternehmensressourcen erfolgen. Falls Du also im Homeoffice arbeitest, hast Du trotzdem einen sicheren Zugriff auf das Unternehmensnetzwerk und es wird eine sichere Datenübertragung gewährleistet.
VPN einrichten
Um VPN erfolgreich einzurichten, gibt es einige allgemeine Schritte, die Du in jedem Fall durchführen musst. Zunächst musst Du einen vertrauenswürdigen Dienstleister wählen. Recherchiere dazu und setzte auf das Feedback von anderen Nutzern. Zu beliebten Anbietern zählen beispielsweise ExpressVPN oder auch NordVPN.
Wenn Du einen Anbieter gefunden hast, musst Du Dich registrieren und Dich für ein Abonnement entscheiden. Im nächsten Schritt solltest Du dann die entsprechende Software für Dein Betriebssystem herunter und installiere alles.
Nun musst Du Dich anmelden und Deinen Serverstandort auswählen. Dieser bestimmt, über welchen Standort Dein Internetverkehr geleitet wird. Im letzten Schritt muss dann nur noch eine Verbindung hergestellt werden.
Weitere Aspekte und Tipps
Achte bei der Auswahl des VPN-Anbieters besonders auf die Vertrauenswürdigkeit und darauf, dass es klare Richtlinien zum Schutz Deiner Privatsphäre gibt. Informiere Dich auch über die Verschlüsselungsprotokolle, die der Anbieter verwendet. OpenVPN beispielsweise gilt als sehr sicher und wird oft verwendet.
Bei der Verwendung von VPN muss Dir klar sein, dass Deine Internetgeschwindigkeit beeinträchtigt werden kann. Außerdem solltest Du im Vorfeld überprüfen, ob der VPN-Anbieter Deine Geräte auch unterstützt.
Außerdem solltest Du die Funktion Kill Switch kennen. Bei dieser Funktion wird die Internetverbindung automatisch getrennt, falls die VPN-Verbindung einmal abbrechen sollte. So wird sichergestellt, dass Dein Datenverkehr nicht ungeschützt ist.
Zu Beginn solltest Du zudem schauen, welche Preise die verschiedenen Anbieter haben und schauen, ob sie kostenlose Testzeiträume oder eine Geld-zurück-Garantie anbieten.
Viele Vorteile
VPN bietet also diverse Vorteile vor allem in Bezug auf Sicherheitsaspekte Deiner Online-Aktivitäten. Egal ob beruflich oder privat, die Nutzung eignet sich für jeden. Das Einrichten ist auch nicht besonders kompliziert, wenn man die einzelnen Schritte kennt und der Reihe nach abarbeitet. Hilfreich ist es, sich im Vorfeld viele Informationen über den Anbieter und das Produkt einzuholen.
Wie funktioniert VPN und was ist das?
Als Internetnutzer sind Sie wahrscheinlich schon mit dem Begriff VPN (Virtual Private Network) in Kontakt gekommen. Aber was genau verbirgt sich hinter diesem Begriff und wie funktioniert ein Virtual Private Network eigentlich. Im folgenden Artikel gehen wir diesen Fragen auf den Grund.
Wie funktioniert eine Internetverbindung?
Um die Frage was ist VPN vollständig beantworten zu können, ist es wichtig, zunächst die Grundlagen einer Internetverbindung zu verstehen. Wenn Sie im Internet unterwegs sind, wie beispielsweise über Ihren Desktop-PC oder Tablet-PC, bekommt Ihr Computer von Ihrem Internet Service Provider (ISP) eine eindeutige Adresse, die sogenannte IP-Adresse, zugeteilt.
Die IP-Adresse kann als Ihre Heimadresse im Netz betrachtet werden, die Sie eindeutig im Internet identifiziert und Ihre Tätigkeiten im Netz nachvollziehbar macht. Sobald Sie eine Website im World Wide Web aufrufen, verbindet sich Ihr Computer mit dem Webserver, auf dem die jeweilige Website gehostet wird. Der Server kennt die IP-Adresse Ihres Computers und verwaltet diese, während er die Informationen der Website an Sie sendet.
Was ist ein VPN?
Bei VPN handelt es sich um ein Akronym für Virtual Private Network. Ein VPN sorgt dafür, dass Ihr Computer eine andere IP-Adresse zugeteilt bekommt, sodass Ihr Computer im Internet nicht mehr zurückverfolgt werden kann. Die Funktionsweise einer VPN-Verbindung lässt sich folgendermaßen erklären: Anstatt sich direkt mit Ihrem Internet Service Provider zu verbinden, baut Ihr Computer eine sichere und verschlüsselte Verbindung zu einem VPN-Server auf.
Sobald Sie eine Website aufrufen, kontaktiert der VPN-Server den Webserver, auf dem die jeweilige Website gehostet wird. Dabei werden alle Daten über Internetaktivität genau protokolliert und auf dem zuständigen Server gespeichert. Diese werden jedoch auf Basis der IP-Adresse des jeweiligen VPN-Servers und nicht auf Basis Ihrer eigenen IP-Adresse realisiert. Die von Ihnen besuchten Websites sammeln und zeichnen Daten über Ihren Besuch auf, sie sind jedoch nicht mehr in der Lage, diese Daten mit Ihnen in Relation zu bringen.
Wie funktioniert ein VPN?
Um die Funktionsweise eines Virtual Private Networks genauer zu erklären, schauen wir uns die Funktionsweise eines Kurierdienstes an. Das Internet ohne ein VPN zu nutzen, ist wie das Versenden eines Pakets. Ohne eine VPN-Verbindung ist Ihre Hausadresse in dem Moment für jeden sichtbar, in dem Sie Ihr Paket dem Boten übergeben.
Wenn Sie das Paket jedoch über einen VPN-Service senden, verwenden Sie automatisch auch eine andere Hausadresse. Außerdem wird Ihr Paket in eine andere Verpackung verpackt, sodass der Inhalt des Pakets nicht mehr anhand der Verpackung nachvollziehbar ist. Dies wird durch eine Verschlüsselungsschicht ermöglicht, die von meisten VPN-Server zur Verfügung gestellt wird.
Vorteile im Überblick
Ein Virtual Private Network bietet Ihnen die Möglichkeit, das eigene Surfverhalten vor den neugierigen und unerlaubten Blicken von Drittpersonen zu verbergen. Moderne VPN-Services sind so ausgerichtet, dass sie in der Regel über gigantische Kapazitäten verfügen und eine parallele Nutzung von Hunderten verschiedener User gewährleisten.
VPNs werden nicht nur zum Schutz der Privatsphäre eingesetzt, sondern sie finden auch immer öfter Einsatz, um regionsspezifische Einstellungen zu umgehen. Webdienste, wie Netflix, Disney+ oder Amazon Prime binden Inhalte an bestimmte regionale IP-Adressen. So können deutsche Nutzer beispielsweise einen VPN-Service nutzen, um die Angebote von Netflix US anzusehen.
Laravel 5.7 Multi-Authentication – Unterschiedliche Benutzertypen in 8 Schritten
Laravel ist ein sehr mächtiges PHP Framework das seinen Fokus auf einfachen, simplen Code legt. Es bringt alle Werkzeuge mit, die ein Webentwickler benötigt, um eine zeitgemäße Online-Plattform zu entwickeln. Ein Beispiel ist die Integration von Benutzern. Mit einem Befehl in der Konsole lässt sich Laravels vordefinierte Implementierung der Benutzer in die Plattform integrieren.
Problematischer ist es hingegen wenn man mehrere Benutzertypen benötigt. Unter Benutzertypen verstehe ich die Differenzierung von Benutzern, die nicht das selbe User Model verwenden, also unterschiedliche Eigenschaften innehaben. Ein einfaches Beispiel wäre die Unterscheidung der Benutzertypen in den Plattform-Administrator, der die Plattformeinstellungen verwalten kann und Zugriff auf Logs und Statistiken der Plattform hat, sowie den normalen Benutzer, der Zugriff auf den Service der jeweiligen Plattform hat. Dieser kann bspw. Einkäufe tätigen, ein Abonnement abschließen, oder sich an Deiner Community beteiligen.
Du benötigst keine unterschiedlichen Benutzertypen, wenn sich Deine geplanten Benutzer das selbe User Model teilen und auf der Plattform nur mit unterschiedlichen Rechten agieren. Ein einfaches Beispiel dafür sind die typischen CMS Benutzerrollen wie Mitarbeiter, Autor und Redakteur, bei denen sich die Rechte der Benutzer zunehmend staffeln. Strebst Du ein solches Modell an, solltest Du Dirüberlegen, die Standardimplementierung der Benutzer um ein Rollen- und Rechtemodell zu erweitern.
In diesem Blogartikel werde ich Dir zeigen, wie Du in acht Schritten Deine eigenen Benutzertypen in Deine Laravel Anwendung integrieren kannst. Du wirst zunächst die Standardimplementierung von Laravel erweitern, danach wirst Du die benötigten Views und Routen bereitstellen. Ein weiterer wichtiger Punkt ist die Erweiterung der Authentication Middleware und die Einrichtung der Redirects. Zum Schluss stellst Du Deinerm neuen Benutzertypen noch die Möglichkeit zur Verfügung sich abzumelden und sein Passwort zurückzusetzen.
Also Grundlage setze ich voraus, dass Du kein Einsteiger in Laravel bist. Du solltest die Grundbausteine von Laravel kennen und wissen wie Du Sie anwendest. Artisan, Controller, Views, Middlewares und Traits sollten für Dich keine Fremdwörter sein. Wir steigen im nächsten Abschnitt direkt mit der Implementierung ein, ein Projekt sollte also bereits bestehen.
Die Integration der normalen Laravel Authentication
Zur Vorbereitung der unterschiedlichen Benutzertypen integrierst Du zunächst die Standard Authentication von Laravel. Auf dieser baust Du Schritt für Schritt Deinen neuen Benutzertypen auf. Du verwendest Sie als Vorlage und sparst Dir damit viel Zeit. Du passt die integrierte Standard Authentication aber nicht an, sondern Du adaptierst sie. Um später zwei unterschiedliche Typen zu verwenden musst Du Dich lediglich an der Standard Implementierung entlanghangeln und baust Dir neues User Model.
Richte nun über Artisan die Authentication ein:
$ php artisan make:auth
Artisan generiert für Dich nun einige Dinge, wie unter anderem Views zur Anmeldung, Registrierung und für Dein Dashboard, sowie Routen zu den neuen Seiten und einen Controller (HomeController.php) für Dein Dashboard. Im app Order findest Du das User Model, an dieser Stelle erstellst Du gleich das neue User Model. All das machst Du im Zuge einer Datenbank Migration.
Unter Deinen Migrationen /database/migrations/ befinden sich bereits zwei vorgefertigte Migrationen von Laravel. Diese wurden nicht von Artisan hinzugefügt. Die Migrationen erzeugen Deine „users“ und Deine „forgot my password“ Tabelle. In der Tabelle „users“ werden unsere normalen Benutzer gespeichert, diese adaptierst Du im nächsten Schritt. Die Tabelle für „forgot my password“ musst Du nicht für Deinen neuen Benutzertyp dublizieren, denn alle Benutzertypen können diese gemeinsam benutzen.
In den nachfolgenden Beispielen verwende ich den Benutzertyp für den B2B Bereich „BusinessUser“ als representativen neuen Benutzertypen.
Erstelle nun mit Artisan im Terminal eine neue Migration um Deine Benutzer des neuen Benutzertypes speichern zu können:
$ php artisan make:migration create_business_users_table --create=business_users
Die gerade erstellte Migration wird mit einer Datierung versehen und unter /database/migrations/ gespeichert. Durch die flag „--create=business_users“ wird die Migration bereits so angepasst, dass die Tabelle mit dem Namen „business_users“ angelegt wird.
Eine alternative Herangehensweise wäre es, das Model über Artisan anzulegen und dabei die passende Migration zu erstellen:
$ php artisan make:model BusinessUser -m
Jetzt musst Du die Migrationsdatei um die Eigenschaften Deines neuen Benutzertypes erweitern. Suche die Migrationsdatei im Verzeichnis /database/migrations/ und öffne sie. Passe die benötigten Eigenschaften so an, dass die Tabelle über alle benötigten Spalten verfügt. Nachfolgend findest Du die angepasste Migration des BusinessUser Models:
<?php
use IlluminateSupportFacadesSchema;
use IlluminateDatabaseSchemaBlueprint;
use IlluminateDatabaseMigrationsMigration;
class CreateBusinessUsersTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('business_users', function (Blueprint $table) {
$table->increments('id');
$table->string('name');
$table->string('email')->unique();
$table->timestamp('email_verified_at')->nullable();
$table->string('company');
$table->string('vat');
$table->string('street_name');
$table->string('street_number');
$table->string('city');
$table->string('zip');
$table->string('password');
$table->rememberToken();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('business_users');
}
}
Solltest Du Fragen zum Erstellen von Migrationen haben, bietet Dir die Laravel Dokumentation ausreichend Informationen und Beispiele an: Laravel Dokumentation - Migrationen
In dieser Migration haben wir einige Spalten für die Eigenschaften unseres neuen Benutzertypes hinzugefügt, die dafür sorgen, das sich dieser von unserem User Benutzertyp unterscheidet. Beachte, dass die Spalte „email_verified_at“ Bestandteil des aus Laravel 5.7 stammenden, neuen Features zur E-Mail Verifizierung ist.
Durch das Hinzufügen von $table->rememberToken(); wird für das Model die Funktionalität „Angemeldet bleiben“ aktiviert und wir können dieses in der Login Maske für unseren neuen Benutzertypen nutzen. Im nächsten Schritt können wir die Migration bereits durchführen.
Hinweis: Da ich für diesen Artikel Vorwissen in der Entwicklung von Laravel Applikationen voraussetze gehe ich nicht auf die .env Konfiguration ein. Denke bitte daran, Deine Environment Datei zu konfigurieren und eine Datenbank einzurichten bevor Du die Migrationen ausführst.
$ php artisan migrate
Die Tabellen für die Authentication der normalen Benutzer und der B2B User wurden nun integriert. Im nächsten Schritt schauen wir uns die zugehörigen User Models an.
Aufsetzen der User Models
Das User Model für den normalen Benutzer existiert bereits im Ordner app. Das User Model funktioniert auch bereits und kann direkt verwendet werden. Du kannst das existierende User Model User.php nun einfach dublizieren und umbenennen. In diesem Beispiel heißt das neue Model BusinessUser.php. Lass uns nun einen Blick in das neue Model werfen:
<?php
namespace App;
use IlluminateNotificationsNotifiable;
use IlluminateContractsAuthMustVerifyEmail;
use IlluminateFoundationAuthUser as Authenticatable;
class BusinessUser extends Authenticatable implements MustVerifyEmail
{
use Notifiable;
protected $guard = 'business_user';
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'name',
'email',
'password',
'company',
'vat',
'street_name',
'street_number',
'city',
'zip'
];
/**
* The attributes that should be hidden for arrays.
*
* @var array
*/
protected $hidden = [
'password', 'remember_token',
];
}
Durch die Duplizierung des User Model hast Du alle relevanten Bestandteile für Dein eigenes User Model bereits integriert. Du solltest aber noch ein paar Anpassungen vornehmen.
Zeile 9: Passe den Klassennamen an Dein User Model an. Im Beispiel wurde User in „BusinessUser“ geändert.
Zeile 9: Implementiere in der Zeile 8 noch das Interface „MustVerifyEmail“. Dadurch muss der Benutzer nach der Registrierung seine E-Mail Adresse bestätigen. Der Benutzer erhält automatisch die Notification „NotificationsVerifyEmail“, in der sich ein Link zur E-Mail Bestätigung befindet. Das Interface kannst Du übrigens auch im User Model User.php hinzufügen, damit auch normale Benutzer ihre E-Mail Adresse bestätigen müssen.
Zeile 13: Um später den Zugriff auf Controller nur für Benutzer des neuen Benutzertypes zu beschränken hinterlegst Du in der protected Eigenschaft „guard“ den Namen Deines User Models. Im nächsten Schritt stellen wir die passende guard in der auth.php Konfiguration bereit.
Zeile 20: Integriere in das fillable Array all Deine Benutzereigenschaften, die per „create“ befüllt werden können.
Aufsetzen des Guards
In Laravel ist es möglich mehrere konfigurierte Benutzertypen zu identifizieren. Die Zugriffsbeschränkung wird mit sogenannten „Guards“ vorgenommen. In Laravel können so viele Guards konfiguriert werden, wie Du sie benötigst, es gibt keine Einschränkung.
Die Standard Guards und neue Guards werden in der Authentication Konfiguration unter config/auth.php verwaltet und konfiguriert. Im folgenden Beispiel siehst Du die Standard auth.php ohne die Kommentare:
<?php
return [
'defaults' => [
'guard' => 'web',
'passwords' => 'users',
],
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
'api' => [
'driver' => 'token',
'provider' => 'users',
],
],
'providers' => [
'users' => [
'driver' => 'eloquent',
'model' => AppUser::class,
],
],
…
Im ersten Teil des Arrays siehst Du die Standardeinstellungen. Als Standardwert für guard ist „web“ festgelegt. Sollte bei einer Middleware keine Guard festgelegt sein, wird automatisch die „web“ Guard verwendet.
Im nächsten Array Knoten „guards“ sind die vorhandenen Guards „web“ und „api“ hinterlegt. Beide teilen sich den Provider „users“. Unterscheiden tuen sie sich lediglich über den hinterlegten Driver. Bei einem Web Zugriff soll ein Session/ Cookie basierter Treiber verwendet werden und bei einem API-Request ein Token basierter.
Direkt unterhalb der Guards findest Du die Provider. Als einziger Provider sollte dort momentan „users“ hinterlegt sein. „users“ verwendet als Driver wiederum Eloquent und das zugeordnete User Model ist AppUser. An dieser Stelle kann auch database als Driver hinterlegt werden und anstelle des User Models trägst Du in diesem Fall den Tabellennamen ein.
Im nächsten Schritt legst Du Deinen eigenen Provider und Deine eigenen Guards an. Für dieses Tutorial beschränke ich mich im Detail darauf, einen Guard zu integrieren, der Session basiert arbeitet. Füge nun die Knoten an die jeweiligen Arrays an, Deine auth config sollte so ähnlich aussehen, wie in folgendem Beispiel:
<?php
return [
'defaults' => [
'guard' => 'web',
'passwords' => 'users',
],
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
'api' => [
'driver' => 'token',
'provider' => 'users',
],
'business_user' => [
'driver' => 'session',
'provider' => 'business_users',
],
'business_user_api' => [
'driver' => 'token',
'provider' => 'business_users',
],
],
'providers' => [
'users' => [
'driver' => 'eloquent',
'model' => AppUser::class,
],
'business_users' => [
'driver' => 'eloquent',
'model' => AppBusinessUser::class,
],
],
'passwords' => [
'users' => [
'provider' => 'users',
'table' => 'password_resets',
'expire' => 60,
],
'business_users' => [
'provider' => 'business_users',
'table' => 'password_resets',
'expire' => 10,
],
],
];
Stelle sicher, dass beim Model das richtige Model hinterlegt ist und dass Du die ::class Syntax verwendest.
Nun steht Dir der relevante Guard für Dein User Model zur Verfügung. Dieser steht jetzt in Verbindung mit Deinem User Model und in dem sollte jetzt auch die protected Eigenschaft mir dem Name der Guard übereinstimmen.
Testen der Zugriffsbeschränkung
Zum Testen der Zugriffsbeschränkung dublizierst Du in diesem Schritt die „home“ View. Dadurch baust Du ein Dashboard/ eine Startseite für Deinen neuen Benutzertyp. Auf diese Seite haben lediglich angemeldete Business User Zugriff. Gäste werden auf den Business User Login weitergeleitet und normale Benutzer ebenfalls. Da normale Benutzer aber bereits angemeldet sind, werden diese vom Login auf ihre eigene Startseite weitergeleitet.
Erstelle nun einen „Dashboard“ Controller für Deinen neuen Benutzertyp:
$ php artisan make:controller BusinessUserHomeController
In dem erzeugten Controller kannst Du die Funktion „index“ vom HomeController adaptieren:
<?php
namespace AppHttpControllers;
use IlluminateHttpRequest;
use IlluminateHttpResponse;
class BusinessUserHomeController extends Controller
{
/**
* Create a new controller instance.
*
* @return void
*/
public function __construct()
{
$this->middleware('auth:business_user');
}
/**
* Show the application dashboard.
*
* @return Response
*/
public function index()
{
return view('businessuser.home');
}
}
Wie Du im Konstruktor erkennen kannst, erweitere ich im Beispiel die Middleware „auth“ um einen Parameter, was an dem Doppelpunkt zu erkennen ist. Nach dem Doppelpunkt folgt die Guard, die wir in den vorherigen Schritten definiert haben :business_user. Mehr zur Verwendung von Parametern in der Middleware findest Du unter: Laravel Dokumentation - Middleware. In der Funktion „index“ hinterlegst Du noch Deine View, in meinem Fall ist das „businessuser.home“.
Nun kannst Du die „home“ View kopieren und änderst zu Testzwecken die Dashboad-Bezeichnungen, damit ersichtlich ist, welche View geladen wurde:
@extends('layouts.app')
@section('content')
<div class="container">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card">
<div class="card-header">Dashboard für Geschäftskunden</div>
<div class="card-body">
@if (session('status'))
<div class="alert alert-success" role="alert">
{{ session('status') }}
</div>
@endif
Sie befinden sich im Dashboard für Geschäftskunden.
</div>
</div>
</div>
</div>
</div>
@endsection
Zuletzt hinterlegst Du noch die Route für Deine neue Dashboard View in der Datei routes/web.php:
<?php
/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| contains the "web" middleware group. Now create something great!
|
*/
Route::get('/', function () {
return view('welcome');
});
Auth::routes();
Route::get('/home', 'HomeController@index')->name('home');
Route::get('/business-user/home', 'BusinessUserHomeController@index')->name('business_user.home');
Greifst Du nun als angemeldeter normaler Benutzer auf die Seite „/business-user/home“ zu, wirst Du auf Dein Dashboard „/home“ redirected.
Registrierung unterschiedlicher Benutzertypen
Kommen wir nun zur Registrierung. Über die Standardregistrierung, die mit „make:auth“ angelegt wurde, können keine eigenen Benutzertypen registriert werden. Ich würde kein Auswahlfeld für den Benutzertypen hinzufügen, da normale Benutzer ansonsten verwirrt seien könnten. Da der neue Benutzertyp sowieso andere Eigenschaften haben sollte als der Standardbenutzer, teilen sich diese nicht die Felder bei der Registrierung. Wenn es sich bei Deinem Benutzertypen um einen Administrator handelt, benötigst Du keine weitere Registrierung. Diesen kannst Du dann über einen Seed anlegen. Wie das funktioniert zeige ich Dir im nächsten Kapitel. Nun erst mal zur Registrierung. Erzeuge einen neuen Registrierungs Controller im Unterordner „Auth“:
$ php artisan make:controller Auth/RegisterBusinessUserController
Nun übernimmst Du schrittweise alle wichtigen Komponenten des RegisterController:
<?php
namespace AppHttpControllersAuth;
use AppBusinessUser;
use AppHttpControllersController;
use IlluminateFoundationAuthUser;
use IlluminateHttpResponse;
use IlluminateSupportFacadesHash;
use IlluminateSupportFacadesValidator;
use IlluminateFoundationAuthRegistersUsers;
class RegisterBusinessUserController extends Controller
{
use RegistersUsers;
/**
* Where to redirect users after registration.
*
* @var string
*/
protected $redirectTo = '/business-user/home';
/**
* Create a new controller instance.
*
* @return void
*/
public function __construct()
{
$this->middleware('guest');
}
/**
* Show the application registration form.
*
* @return Response
*/
protected function showRegistrationForm()
{
return view('businessuser.auth.register');
}
/**
* Get a validator for an incoming registration request.
*
* @param array $data
* @return IlluminateContractsValidationValidator
*/
protected function validator(array $data)
{
return Validator::make($data, [
'name' => 'required|string|max:255',
'email' => 'required|string|email|max:255|unique:users',
'password' => 'required|string|min:6|confirmed',
'company' => 'required|string|max:255',
'vat' => 'required|string|max:255',
'street_name' => 'required|string|max:255',
'street_number' => 'required|string|max:255',
'city' => 'required|string|max:255',
'zip' => 'required|string|max:255',
]);
}
/**
* Create a new user instance after a valid registration.
*
* @param array $data
* @return User
*/
protected function create(array $data)
{
return BusinessUser::create([
'name' => $data['name'],
'email' => $data['email'],
'password' => Hash::make($data['password']),
'company' => $data['company'],
'vat' => $data['vat'],
'street_name' => $data['street_name'],
'street_number' => $data['street_number'],
'city' => $data['city'],
'zip' => $data['zip']
]);
}
}
Zunächst integrierst Du den Trait RegistersUsers. Über die protected Eigenschaft redirectTo kannst Du das Redirect Ziel nach einer erfolgreichen Registrierung festlegen. Im Konstruktor legst Du über die Middleware „Guest“ fest, dass angemeldete Benutzer die Seite nicht besuchen können. Diese werden auf ihr Dashboard weitergeleitet.
Zum Schluss überschreibst Du noch die Funktionen „validator“ und „create“. Du erweiterst die Validierung um die Felder Deines Registrierungsformulars und in der Erstellungsfunktion initialisierst Du über die statische create Funktion Dein User Model und gibst es zurück. Dadurch, dass Dein User Model von der Basisklasse Authenticatable abgeleitet ist, werden alle weiteren Schritte durch die Funktionen im Trait erledigt.
Um den Prozess der Registrierung Deines eigenen Benutzertypen zu vervollständigen fehlt nun nur noch eine View. Im Controller überschreibst Du die Trait Funktion „showRegistrationForm“ und gibst dort die View Deiner Wahl zurück. Du kopierst nun die register View der normalen Registrierung und fügst die von Dir benötigten Felder hinzu. Hier findest Du die modifizierte View, mit den Eigenschaften aus meinem neuen Benutzertypen:
@extends('layouts.app')
@section('content')
<div class="container">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card">
<div class="card-header">Geschäftskundenregistrierung</div>
<div class="card-body">
<form method="POST" action="{{ route('business_user.register.post') }}">
@csrf
<div class="form-group row">
<label for="name" class="col-md-4 col-form-label text-md-right">Geschäftsführung</label>
<div class="col-md-6">
<input id="name" type="text" class="form-control{{ $errors->has('name') ? ' is-invalid' : '' }}" name="name" value="{{ old('name') }}" required autofocus>
@if ($errors->has('name'))
<span class="invalid-feedback" role="alert">
<strong>{{ $errors->first('name') }}</strong>
</span>
@endif
</div>
</div>
<div class="form-group row">
<label for="company" class="col-md-4 col-form-label text-md-right">Firma</label>
<div class="col-md-6">
<input id="company" type="text" class="form-control{{ $errors->has('company') ? ' is-invalid' : '' }}" name="company" value="{{ old('company') }}" required>
@if ($errors->has('company'))
<span class="invalid-feedback" role="alert">
<strong>{{ $errors->first('company') }}</strong>
</span>
@endif
</div>
</div>
<div class="form-group row">
<label for="vat" class="col-md-4 col-form-label text-md-right">USt-ID</label>
<div class="col-md-6">
<input id="vat" type="text" class="form-control{{ $errors->has('vat') ? ' is-invalid' : '' }}" name="vat" value="{{ old('vat') }}" required>
@if ($errors->has('vat'))
<span class="invalid-feedback" role="alert">
<strong>{{ $errors->first('vat') }}</strong>
</span>
@endif
</div>
</div>
<div class="form-group row">
<label for="street_name" class="col-md-4 col-form-label text-md-right">Straße</label>
<div class="col-md-6">
<input id="street_name" type="text" class="form-control{{ $errors->has('street_name') ? ' is-invalid' : '' }}" name="street_name" value="{{ old('street_name') }}" required>
@if ($errors->has('street_name'))
<span class="invalid-feedback" role="alert">
<strong>{{ $errors->first('street_name') }}</strong>
</span>
@endif
</div>
</div>
<div class="form-group row">
<label for="street_number" class="col-md-4 col-form-label text-md-right">Hausnummer</label>
<div class="col-md-6">
<input id="street_number" type="text" class="form-control{{ $errors->has('street_number') ? ' is-invalid' : '' }}" name="street_number" value="{{ old('street_number') }}" required>
@if ($errors->has('street_number'))
<span class="invalid-feedback" role="alert">
<strong>{{ $errors->first('street_number') }}</strong>
</span>
@endif
</div>
</div>
<div class="form-group row">
<label for="city" class="col-md-4 col-form-label text-md-right">Ort</label>
<div class="col-md-6">
<input id="city" type="text" class="form-control{{ $errors->has('city') ? ' is-invalid' : '' }}" name="city" value="{{ old('city') }}" required>
@if ($errors->has('city'))
<span class="invalid-feedback" role="alert">
<strong>{{ $errors->first('city') }}</strong>
</span>
@endif
</div>
</div>
<div class="form-group row">
<label for="zip" class="col-md-4 col-form-label text-md-right">PLZ</label>
<div class="col-md-6">
<input id="zip" type="text" class="form-control{{ $errors->has('zip') ? ' is-invalid' : '' }}" name="zip" value="{{ old('zip') }}" required>
@if ($errors->has('zip'))
<span class="invalid-feedback" role="alert">
<strong>{{ $errors->first('zip') }}</strong>
</span>
@endif
</div>
</div>
<div class="form-group row">
<label for="email" class="col-md-4 col-form-label text-md-right">{{ __('E-Mail Address') }}</label>
<div class="col-md-6">
<input id="email" type="email" class="form-control{{ $errors->has('email') ? ' is-invalid' : '' }}" name="email" value="{{ old('email') }}" required>
@if ($errors->has('email'))
<span class="invalid-feedback" role="alert">
<strong>{{ $errors->first('email') }}</strong>
</span>
@endif
</div>
</div>
<div class="form-group row">
<label for="password" class="col-md-4 col-form-label text-md-right">Passwort</label>
<div class="col-md-6">
<input id="password" type="password" class="form-control{{ $errors->has('password') ? ' is-invalid' : '' }}" name="password" required>
@if ($errors->has('password'))
<span class="invalid-feedback" role="alert">
<strong>{{ $errors->first('password') }}</strong>
</span>
@endif
</div>
</div>
<div class="form-group row">
<label for="password-confirm" class="col-md-4 col-form-label text-md-right">Passwort wiederholen</label>
<div class="col-md-6">
<input id="password-confirm" type="password" class="form-control" name="password_confirmation" required>
</div>
</div>
<div class="form-group row mb-0">
<div class="col-md-6 offset-md-4">
<button type="submit" class="btn btn-primary">Jetzt registrieren</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
@endsection
Als nächstes platzierst Du in Deiner „welcome“ View einen Link auf die neue Registrierung:
<!doctype html>
<html lang="{{ app()->getLocale() }}">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Laravel</title>
<!-- Fonts -->
<link href="https://fonts.googleapis.com/css?family=Nunito:200,600" rel="stylesheet" type="text/css">
<!-- Styles -->
<style>
html, body {
background-color: #fff;
color: #636b6f;
font-family: 'Nunito', sans-serif;
font-weight: 200;
height: 100vh;
margin: 0;
}
.full-height {
height: 100vh;
}
.flex-center {
align-items: center;
display: flex;
justify-content: center;
}
.position-ref {
position: relative;
}
.top-right {
position: absolute;
right: 10px;
top: 18px;
}
.content {
text-align: center;
}
.title {
font-size: 84px;
}
.links > a {
color: #636b6f;
padding: 0 25px;
font-size: 12px;
font-weight: 600;
letter-spacing: .1rem;
text-decoration: none;
text-transform: uppercase;
}
.m-b-md {
margin-bottom: 30px;
}
</style>
</head>
<body>
<div class="flex-center position-ref full-height">
@if (Route::has('login'))
<div class="top-right links">
@auth
<a href="{{ url('/home') }}">Home</a>
@else
<a href="{{ route('login') }}">Login</a>
<a href="{{ route('register') }}">Registrierung</a>
<a href="{{ route('business_user.register.get') }}">Geschäftskundenregistrierung</a>
@endauth
</div>
@endif
<div class="content">
<div class="title m-b-md">
Laravel
</div>
<div class="links">
<a href="https://laravel.com/docs">Documentation</a>
<a href="https://laracasts.com">Laracasts</a>
<a href="https://laravel-news.com">News</a>
<a href="https://nova.laravel.com">Nova</a>
<a href="https://forge.laravel.com">Forge</a>
<a href="https://github.com/laravel/laravel">GitHub</a>
</div>
</div>
</div>
</body>
</html>
Damit die integrierten Routen funktionieren, müssen diese noch in Deinen Web-Routen hinterlegt werden:
<?php
/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| contains the "web" middleware group. Now create something great!
|
*/
Route::get('/', function () {
return view('welcome');
});
Auth::routes();
Route::get('/home/', 'HomeController@index')
->name('home');
Route::prefix('business-user')->group(function () {
Route::get('/home/', 'BusinessUserHomeController@index')
->name('business_user.home');
Route::get('/register/', 'AuthRegisterBusinessUserController@showRegistrationForm')
->name('business_user.register.get');
Route::post('/register/', 'AuthRegisterBusinessUserController@register')
->name('business_user.register.post');
});
Zur besseren Gliederung der Routen wurden diese im einem Routen Prefix gruppiert. Du kannst Dir nun mit Hilfe von Artisan Deine Routing-Tabelle anzeigen lassen:
$ php artisan route:list
Oder die Kurzform:
$ php artisan r:l
Eigene E-Mail-Verifizierung – Seit Laravel 5.7
Diesen Schritt kannst Du nur integrieren, wenn Du mit einem Laravel 5.7 Projekt arbeitest. Vorher gab es die eigens von Laravel bereitgestellte E-Mail-Verifizierung noch nicht.
Im nächsten Schritt integrierst Du das E-Mail-Verifizierungsverfahren. Laravel hat in der Version 5.7 ein eigenes E-Mail Verifizierungsverfahren erhalten. Dieses ist ebenfalls abgestimmt auf den Standard Laravel Benutzer und Du integrierst nun Dein eigenes Verifizierungsverfahren, basiert auf dem von Laravel. Da Du zuletzt Routen in der web.php ergänzst hast setzen wir hier auch wieder an.
In der Zeile 18 muss beim Aufruf von Auth::routes(); noch ein Array mit Argumenten übergeben werden. Als Parameter setzt Du wie im folgenden Beispiel verify = true.
Im Prefixknoten „business-user“ integrierst Du nun drei neue Verifizierungsrouten:
<?php
/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| contains the "web" middleware group. Now create something great!
|
*/
Route::get('/', function () {
return view('welcome');
});
Auth::routes(['verify' => true]);
Route::get('/home/', 'HomeController@index')
->name('home');
Route::prefix('business-user')->group(function () {
Route::get('/home/', 'BusinessUserHomeController@index')
->name('business_user.home');
Route::get('/register/', 'AuthRegisterBusinessUserController@showRegistrationForm')
->name('business_user.register.get');
Route::post('/register/', 'AuthRegisterBusinessUserController@register')
->name('business_user.register.post');
Route::get('/email/resend/', 'BusinessUserVerificationController@resend')
->name('business_user.verification.resend');
Route::get('/email/verify/', 'BusinessUserVerificationController@show')
->name('business_user.verification.notice');
Route::get('/email/verify/{id}/', 'BusinessUserVerificationController@verify')
->name('business_user.verification.verify');
});
Lege nun den Verifizierungs-Controller über Artisan an:
$ php artisan make:controller BusinessUserVerificationController
In dem Controller impelementierst Du alle Inhalte des AuthVerificationController, dieser dient nun als Grundlage.
<?php
namespace AppHttpControllers;
use IlluminateHttpRequest;
use IlluminateHttpResponse;
use IlluminateFoundationAuthVerifiesEmails;
class BusinessUserVerificationController extends Controller
{
use VerifiesEmails;
/**
* Where to redirect users after verification.
*
* @var string
*/
protected $redirectTo = '/business-user/home';
/**
* Create a new controller instance.
*
* @return void
*/
public function __construct()
{
$this->middleware('auth:business_user');
$this->middleware('signed')->only('verify');
$this->middleware('throttle:6,1')->only('verify', 'resend');
}
/**
* Show the email verification notice.
*
* @param Request $request
* @return Response
*/
public function show(Request $request)
{
return $request->user()->hasVerifiedEmail()
? redirect($this->redirectPath())
: view('businessuser.verify');
}
}
Am Controller musst Du nun noch einige Anpassungen durchführen. Wie im Beispiel zu sehen ist, wurde die protected Eigenschaft redirectTo an die Dashboard URL des neuen Benutzertypen angepasst und die „auth“ Middleware wurde um den Parameter „business_user“ erweitert. Weiterhin wurde die Funktion „show“ aus dem VerifiesEmails Trait überschrieben, damit der Benutzer korrekt weitergeleitet wird.
Als nächstes musst Du das E-Mail-Template/ die Laravel Notification für die Verifizierung anpassen. Der Link in der E-Mail zeigt auf die falsche Verifizierungsseite. Zunächst implementierst Du in Deinem neuen User Model, bei mir ist das AppBusinessUser.php, die Funktion „sendEmailVerificationNotification“, die vom Interface „MustVerifyEmail“ vorgegeben wird. Du musst nicht alle Funktionen des Interfaces integrieren. Diese kommen aus dem Trait IlluminateAuthMustVerifyEmail, der im Authenticatable integriert wird.
In der Funktion integrierst Du nun Deine eigene Notification, die Du im Anschluss erstellst:
<?php
namespace App;
use IlluminateNotificationsNotifiable;
use IlluminateContractsAuthMustVerifyEmail;
use IlluminateFoundationAuthUser as Authenticatable;
class BusinessUser extends Authenticatable implements MustVerifyEmail
{
use Notifiable;
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'name',
'email',
'password',
'company',
'vat',
'street_name',
'street_number',
'city',
'zip'
];
/**
* The attributes that should be hidden for arrays.
*
* @var array
*/
protected $hidden = [
'password', 'remember_token',
];
/**
* Versendet eine E-Mail-Verifizierungs Benachrichtigung an den Benutzer
*/
public function sendEmailVerificationNotification()
{
$this->notify(new NotificationsBusinessUserVerifyEmail);
}
}
Erstelle jetzt mit Artisan eine Notification:
$ php artisan make:notification BusinessUserVerifyEmail
Laravel hat nun im Verzeichnis app/Notifications die neue Notification BusinessUserVerifyEmail angelegt. Nun müssen wir den Inhalt der alten Notification ermitteln und übernehmen. Das einzige was wir anpassen müssen, ist die temporäre URL zur Aktivierung. Die ursprüngliche Notification ist im Trait MustVerifyEmail hinterlegt und ist im Namespace IlluminateAuthNotifications enthalten:
<?php
namespace AppNotifications;
use IlluminateBusQueueable;
use IlluminateNotificationsNotification;
use IlluminateContractsQueueShouldQueue;
use IlluminateNotificationsMessagesMailMessage;
use IlluminateSupportCarbon;
use IlluminateSupportFacadesLang;
use IlluminateSupportFacadesURL;
class BusinessUserVerifyEmail extends Notification implements ShouldQueue
{
use Queueable;
/**
* The callback that should be used to build the mail message.
*
* @var Closure|null
*/
public static $toMailCallback;
/**
* Get the notification's delivery channels.
*
* @param mixed $notifiable
* @return array
*/
public function via($notifiable)
{
return ['mail'];
}
/**
* Get the mail representation of the notification.
*
* @param mixed $notifiable
* @return IlluminateNotificationsMessagesMailMessage
*/
public function toMail($notifiable)
{
if (static::$toMailCallback) {
return call_user_func(static::$toMailCallback, $notifiable);
}
return (new MailMessage)
->subject(Lang::getFromJson('Verify Email Address'))
->line(Lang::getFromJson('Please click the button below to verify your email address.'))
->action(
Lang::getFromJson('Verify Email Address'),
$this->verificationUrl($notifiable)
)
->line(Lang::getFromJson('If you did not create an account, no further action is required.'));
}
/**
* Get the verification URL for the given notifiable.
*
* @param mixed $notifiable
* @return string
*/
protected function verificationUrl($notifiable)
{
return URL::temporarySignedRoute(
'business_user.verification.verify', Carbon::now()->addMinutes(60), ['id' => $notifiable->getKey()]
);
}
/**
* Set a callback that should be used when building the notification mail message.
*
* @param Closure $callback
* @return void
*/
public static function toMailUsing($callback)
{
static::$toMailCallback = $callback;
}
}
In diesem Beispiel habe ich noch das Interface ShouldQueue implementiert und in den Zeilen 65-67 die temporär signierte Route auf die Verifizierungsseite des BusinessUser‘s geändert.
Anlegen von Benutzern über Database Seeding
Dieser Schritt ist zur Integration nicht notwendig. Es ist Dir selbst überlassen, ob Du das Database Seeding für Deinen eigenen Benutzertypen benötigst.
Wenn Dein neuer Benutzertyp bspw. ein Administrator ist und Du nicht möchtest, dass sich dieser registrieren kann, so kannst Du das Database Seeding von Laravel nutzen. Detaillierte Informationen zum Seeding findest Du unter: Laravel Dokumentation - Seeding.
Seeding ermöglicht Dir, über einen Befehl Deine Datenbank mit Daten zu befüllen. So kannst Du einen Prozess entwickeln, um einen default Administrator anzulegen, sollte noch keiner im System hinterlegt sein. Um Dir einen Administratoren Seed anzulegen, führe folgenden Artisan Befehl im Terminal aus:
$ php artisan make:seeder AdminTableSeeder
Unter database/seeds wurde nun Dein Seeder AdminTableSeeder angelegt. Um einen default Administrator zu hinterlegen, kannst Du folgendes Beispiel-Script als Grundlage verwenden:
<?php
use AppAdministrator;
use IlluminateDatabaseSeeder;
use IlluminateSupportFacadesHash;
use IlluminateDatabaseEloquentCollection;
class AdminTableSeeder extends Seeder
{
/**
* Run the database seeds.
*
* @return void
*/
public function run()
{
/** @var Collection $administrators */
$administrators = Administrator::all();
if($administrators->isEmpty()) {
Administrator::create([
'name' => 'Administrator',
'email' => 'info@email.de',
'password' => Hash::make('insecure')
])
}
}
}
Damit Dein Seeder verwendet werden kann, musst Du ihn im DatabaseSeeder hinzufügen:
<?php
use IlluminateDatabaseSeeder;
class DatabaseSeeder extends Seeder
{
/**
* Seed the application's database.
*
* @return void
*/
public function run()
{
$this->call(AdminTableSeeder::class);
}
}
Um den Seeder auszuführen wechselst Du in Dein Terminal und generierst Composers autoload neu:
$ composer dump-autoload
Danach führst Du das Seeding über Artisan aus:
$ php artisan db:seed
Du hast auch die Möglichkeit das Seeding während der Migrations durchzuführen:
$ php artisan migrate:refresh --seed
Integration des Logins
In diesem Abschnitt integrierst Du einen neuen Login Bereich für den neuen Benutzertyp. Der aktuell verfügbare Login-Bereich ist starr auf das User Model ausgerichtet und es logged auch nur diesen ein.
Um den Login für den Business User bereitzustellen musst Du nun einige Komponenten dses regulären Logins adaptieren und ein wenig anpassen. Du benötigst einen neuen Controller, einen eigenen AuthenticatsUsers Trait und eine eigene Login View. Des weitern musst Du noch die neuen Routen hinterlegen. All das gehen wir nun Schritt für Schritt durch.
Beginnen wir mit dem Trait. Lege den Ordner „Traits“ bei Deinen Auth Controlleren an (app/Http/Controllers/Auth/Traits). In diesem erstellst Du den Trait AuthenticatesBusinessUsers, der zunächst eine exakte Kopie des Traits AuthenticatesUsers aus dem Namespace „IlluminateFoundationAuth“ ist.
Im Trait passt Du zunächst den Namespace an und fügst die usings für „IlluminateFoundationAuthRedirectsUsers“ und „IlluminateFoundationAuthThrottlesLogins“ hinzu, damit die eingebundenen Traits weiterhin funktionieren.
Zum Schluss passt Du noch die showLoginForm und die verwendete Guard an. In folgendem Beispiel findest Du alle Anpassungen hervorgehoben:
<?php
namespace AppHttpControllersAuthTraits;
use IlluminateFoundationAuthRedirectsUsers;
use IlluminateFoundationAuthThrottlesLogins;
use IlluminateHttpRequest;
use IlluminateSupportFacadesAuth;
use IlluminateValidationValidationException;
trait AuthenticatesBusinessUsers
{
use RedirectsUsers, ThrottlesLogins;
/**
* Show the application's login form.
*
* @return IlluminateHttpResponse
*/
public function showLoginForm()
{
return view('businessuser.auth.login');
}
/**
* Handle a login request to the application.
*
* @param IlluminateHttpRequest $request
* @return IlluminateHttpRedirectResponse|IlluminateHttpResponse|IlluminateHttpJsonResponse
*
* @throws IlluminateValidationValidationException
*/
public function login(Request $request)
{
$this->validateLogin($request);
// If the class is using the ThrottlesLogins trait, we can automatically throttle
// the login attempts for this application. We'll key this by the username and
// the IP address of the client making these requests into this application.
if ($this->hasTooManyLoginAttempts($request)) {
$this->fireLockoutEvent($request);
return $this->sendLockoutResponse($request);
}
if ($this->attemptLogin($request)) {
return $this->sendLoginResponse($request);
}
// If the login attempt was unsuccessful we will increment the number of attempts
// to login and redirect the user back to the login form. Of course, when this
// user surpasses their maximum number of attempts they will get locked out.
$this->incrementLoginAttempts($request);
return $this->sendFailedLoginResponse($request);
}
/**
* Validate the user login request.
*
* @param IlluminateHttpRequest $request
* @return void
*/
protected function validateLogin(Request $request)
{
$this->validate($request, [
$this->username() => 'required|string',
'password' => 'required|string',
]);
}
/**
* Attempt to log the user into the application.
*
* @param IlluminateHttpRequest $request
* @return bool
*/
protected function attemptLogin(Request $request)
{
return $this->guard()->attempt(
$this->credentials($request), $request->filled('remember')
);
}
/**
* Get the needed authorization credentials from the request.
*
* @param IlluminateHttpRequest $request
* @return array
*/
protected function credentials(Request $request)
{
return $request->only($this->username(), 'password');
}
/**
* Send the response after the user was authenticated.
*
* @param IlluminateHttpRequest $request
* @return IlluminateHttpResponse
*/
protected function sendLoginResponse(Request $request)
{
$request->session()->regenerate();
$this->clearLoginAttempts($request);
return $this->authenticated($request, $this->guard()->user())
?: redirect()->intended($this->redirectPath());
}
/**
* The user has been authenticated.
*
* @param IlluminateHttpRequest $request
* @param mixed $user
* @return mixed
*/
protected function authenticated(Request $request, $user)
{
//
}
/**
* Get the failed login response instance.
*
* @param IlluminateHttpRequest $request
*
* @return void
*/
protected function sendFailedLoginResponse(Request $request)
{
throw ValidationException::withMessages([
$this->username() => [trans('auth.failed')],
]);
}
/**
* Get the login username to be used by the controller.
*
* @return string
*/
public function username()
{
return 'email';
}
/**
* Log the user out of the application.
*
* @param IlluminateHttpRequest $request
* @return IlluminateHttpResponse
*/
public function logout(Request $request)
{
$this->guard()->logout();
$request->session()->invalidate();
return $this->loggedOut($request) ?: redirect('/');
}
/**
* The user has logged out of the application.
*
* @param IlluminateHttpRequest $request
* @return mixed
*/
protected function loggedOut(Request $request)
{
//
}
/**
* Get the guard to be used during authentication.
*
* @return IlluminateContractsAuthStatefulGuard
*/
protected function guard()
{
return Auth::guard('business_user');
}
}
Da nun der angepasste Trait zur Verfügung steht, kannst Du den LoginController kopieren und anpassen. In diesem änderst Du den verwendeten Trait zu AuthenticatesBusinessUsers und den „redirect to“ Pfad zu Deinem neuen Home-Verzeichnis. Im Konstruktor änderst Du die Guard der „guest“ Middleware auf „guest:business_user“ damit angemeldete Benutzer dieser Guard nicht auf die Login Seite gelangen.
Hier findest Du den Code des angepassten Controllers. Die Angepassten Stellen sind wieder markiert:
<?php
namespace AppHttpControllersAuth;
use AppHttpControllersAuthTraitsAuthenticatesBusinessUsers;
use AppHttpControllersController;
class LoginBusinessUserController extends Controller
{
/*
|--------------------------------------------------------------------------
| Login Business User Controller
|--------------------------------------------------------------------------
|
| This controller handles authenticating users for the application and
| redirecting them to your home screen. The controller uses a trait
| to conveniently provide its functionality to your applications.
|
*/
use AuthenticatesBusinessUsers;
/**
* Where to redirect users after login.
*
* @var string
*/
protected $redirectTo = 'business-user/home';
/**
* Create a new controller instance.
*
* @return void
*/
public function __construct()
{
$this->middleware('guest:business_user')->except('logout');
}
}
Nun fehlen noch die Einträge der Routen und die Login-View. Diese integrierst Du im nächsten Schritt. Damit ist die Anmeldung auch bereits komplett.
Öffne die Routes Datei „web.php“ und füge die beiden Login Routen GET & POST in die Gruppierung ein. Die hinzugefügten Routen sind hervorgehoben:
<?php
/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| contains the "web" middleware group. Now create something great!
|
*/
Route::get('/', function () {
return view('welcome');
});
Auth::routes(['verify' => true]);
Route::get('/home/', 'HomeController@index')
->name('home');
Route::prefix('business-user')->group(function () {
Route::get('/home/', 'BusinessUserHomeController@index')
->name('business_user.home');
Route::get('/register/', 'AuthRegisterBusinessUserController@showRegistrationForm')
->name('business_user.register.get');
Route::post('/register/', 'AuthRegisterBusinessUserController@register')
->name('business_user.register.post');
Route::get('/email/resend/', 'BusinessUserVerificationController@resend')
->name('business_user.verification.resend');
Route::get('/email/verify/', 'BusinessUserVerificationController@show')
->name('business_user.verification.notice');
Route::get('/email/verify/{id}/', 'BusinessUserVerificationController@verify')
->name('business_user.verification.verify');
Route::get('/login/', 'AuthLoginBusinessUserController@showLoginForm')
->name('business_user.login');
Route::post('/login/', 'AuthLoginBusinessUserController@login')
->name('business_user.login.submit');
});
Kopiere nächstes die Login View im „auth“ Verzeichnis und füge sie in dem neuen Verzeichnis Deines neuen Benutzertypes ein. Passe dort die Überschrift, sowie die Post-Action an. Damit hast Du alle relevanten Anpassungen für Deinen eigenen Login Beriech vorgenommen:
@extends('layouts.app')
@section('content')
<div class="container">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card">
<div class="card-header">{{ __('Business User Login') }}</div>
<div class="card-body">
<form method="POST" action="{{ route('business_user.login.submit') }}">
@csrf
<div class="form-group row">
<label for="email" class="col-sm-4 col-form-label text-md-right">{{ __('E-Mail Address') }}</label>
<div class="col-md-6">
<input id="email" type="email" class="form-control{{ $errors->has('email') ? ' is-invalid' : '' }}" name="email" value="{{ old('email') }}" required autofocus>
@if ($errors->has('email'))
<span class="invalid-feedback" role="alert">
<strong>{{ $errors->first('email') }}</strong>
</span>
@endif
</div>
</div>
<div class="form-group row">
<label for="password" class="col-md-4 col-form-label text-md-right">{{ __('Password') }}</label>
<div class="col-md-6">
<input id="password" type="password" class="form-control{{ $errors->has('password') ? ' is-invalid' : '' }}" name="password" required>
@if ($errors->has('password'))
<span class="invalid-feedback" role="alert">
<strong>{{ $errors->first('password') }}</strong>
</span>
@endif
</div>
</div>
<div class="form-group row">
<div class="col-md-6 offset-md-4">
<div class="form-check">
<input class="form-check-input" type="checkbox" name="remember" id="remember" {{ old('remember') ? 'checked' : '' }}>
<label class="form-check-label" for="remember">
{{ __('Remember Me') }}
</label>
</div>
</div>
</div>
<div class="form-group row mb-0">
<div class="col-md-8 offset-md-4">
<button type="submit" class="btn btn-primary">
{{ __('Login') }}
</button>
<a class="btn btn-link" href="{{ route('password.request') }}">
{{ __('Forgot Your Password?') }}
</a>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
@endsection
Damit Du nun alle Redirects korrekt zum Laufen bringst, müssen wir noch einige Dateien von Laravel Anpassen und umschreiben. Beginne nun damit die Authenticate Mittleware unter „app/Http/Middleware“ folgendermaßen Anzupassen:
<?php
namespace AppHttpMiddleware;
use Closure;
use IlluminateAuthMiddlewareAuthenticate as Middleware;
class Authenticate extends Middleware
{
/**
* @var array
*/
protected $guards = [];
/**
* Handle an incoming request.
*
* @param IlluminateHttpRequest $request
* @param Closure $next
* @param string[] ...$guards
* @return mixed
*
* @throws IlluminateAuthAuthenticationException
*/
public function handle($request, Closure $next, ...$guards)
{
$this->guards = $guards;
return parent::handle($request, $next, ...$guards);
}
/**
* Get the path the user should be redirected to when they are not authenticated.
*
* @param IlluminateHttpRequest $request
* @return string
*/
protected function redirectTo($request)
{
if (!$request->expectsJson()) {
if (array_first($this->guards) === 'business_user') {
return route('business_user.login');
}
return route('login');
}
}
}
Danach öffnest Du die Klasse „RedirectIfAuthenticated“ im selben Verzeichnis und passt sie folgendermaßen an:
<?php
namespace AppHttpMiddleware;
use Closure;
use IlluminateSupportFacadesAuth;
class RedirectIfAuthenticated
{
/**
* Handle an incoming request.
*
* @param IlluminateHttpRequest $request
* @param Closure $next
* @param string|null $guard
* @return mixed
*/
public function handle($request, Closure $next, $guard = null)
{
if ($guard == "business_user" && Auth::guard($guard)->check()) {
return redirect()->route('business_user.home');
}
if (Auth::guard($guard)->check()) {
return redirect('/home');
}
return $next($request);
}
}
Da „MustVerifyEmail“ im User und im BusinessUser integriert wurden, müssen auch diese Redirects angepasst werden. Dafür legst Du Deine eigene Middleware an, die dann von der Ilumniate Middleware erbt. Dadurch können wir die „handle“ Funktion anpassen, ohne die Vendor-Klasse zu verändern.
Lege nun die Middleware im Terminal an:
$ php artisan make:middleware EnsureEmailIsVerified
Passe nun die erzeugte Middleware folgendermaßen an:
<?php
namespace AppHttpMiddleware;
use Closure;
use IlluminateAuthMiddlewareEnsureEmailIsVerified as Middleware;
use IlluminateContractsAuthMustVerifyEmail;
use IlluminateHttpRequest;
use IlluminateSupportFacadesAuth;
use IlluminateSupportFacadesRedirect;
class EnsureEmailIsVerified extends Middleware
{
/**
* Handle an incoming request.
*
* @param IlluminateHttpRequest $request
* @param Closure $next
* @param null $guard
*
* @return IlluminateHttpResponse|IlluminateHttpRedirectResponse
*/
public function handle($request, Closure $next, $guard = null)
{
/** @var array $config */
$configGuards = array_keys(config('auth.guards'));
/** @var string $guard */
foreach($configGuards as $configGuard) {
/** @var string $guardFunction */
$guardFunction = sprintf('handleGuard_%1$s', $configGuard);
if(method_exists($this, $guardFunction)) {
/** @var null|IlluminateHttpRedirectResponse $result */
$result = $this->{$guardFunction}($configGuard, $request);
if($result instanceof IlluminateHttpRedirectResponse) {
return $result;
}
}
}
return $next($request);
}
/**
* Behandelt Requests der "web" Guard.
*
* @param string $guard
* @param Request $request
*
* @return IlluminateHttpRedirectResponse|void
*/
private function handleGuard_web($guard, $request)
{
if (Auth::guard($guard)->check()) {
if (! Auth::guard($guard)->user() ||
(Auth::guard($guard)->user() instanceof MustVerifyEmail &&
! Auth::guard($guard)->user()->hasVerifiedEmail())) {
return $request->expectsJson()
? abort(403, 'Your email address is not verified.')
: Redirect::route('verification.notice');
}
}
}
/**
* Behandelt Requests der "business_user" Guard.
*
* @param string $guard
* @param Request $request
*
* @return IlluminateHttpRedirectResponse|void
*/
private function handleGuard_business_user($guard, $request)
{
if (Auth::guard($guard)->check()) {
if (! Auth::guard($guard)->user() ||
(Auth::guard($guard)->user() instanceof MustVerifyEmail &&
! Auth::guard($guard)->user()->hasVerifiedEmail())) {
return $request->expectsJson()
? abort(403, 'Your email address is not verified.')
: Redirect::route('business_user.verification.notice');
}
}
}
}
Damit die neue Middleware „EnsureEmailIsVerified“ verwendet wird musst Du diese im Kernel hinterlegen. Öffne die Klasse Kernel im Verzeichnis „app/Http“ und passe die Klasse in der routeMiddleware Property zum Key „verified“ an. Hinterlege hier Deine neue Klasse der Middleware „AppHttpMiddlewareEnsureEmailIsVerified::class“.
/** * The application's route middleware. * * These middleware may be assigned to groups or used individually. * * @var array */ protected $routeMiddleware = [ 'auth' => AppHttpMiddlewareAuthenticate::class, 'auth.basic' => IlluminateAuthMiddlewareAuthenticateWithBasicAuth::class, 'bindings' => IlluminateRoutingMiddlewareSubstituteBindings::class, 'cache.headers' => IlluminateHttpMiddlewareSetCacheHeaders::class, 'can' => IlluminateAuthMiddlewareAuthorize::class, 'guest' => AppHttpMiddlewareRedirectIfAuthenticated::class, 'signed' => IlluminateRoutingMiddlewareValidateSignature::class, 'throttle' => IlluminateRoutingMiddlewareThrottleRequests::class, 'verified' => AppHttpMiddlewareEnsureEmailIsVerified::class, ];
Die letzte benötigte Anpassung ist das Error Handling für den „Unauthenticated“ Error und den nachfolgenden Redirect. Dazu fügst Du in der Handler Klasse „app/Exceptions/Handler.php“ folgende Funktion ein:
/**
* Convert an authentication exception into a response.
*
* @param IlluminateHttpRequest $request
* @param IlluminateAuthAuthenticationException $exception
* @return IlluminateHttpResponse
*/
protected function unauthenticated($request, AuthenticationException $exception)
{
if ($request->expectsJson()) {
return response()->json(['error' => 'Unauthenticated.'], 401);
}
/** @var string $guard */
$guard = array_get($exception->guards(),0);
switch($guard) {
case 'business_user':
$login = 'business_user.login';
break;
default:
$login = 'login';
break;
}
return redirect()->route($login);
}
Der Login an der Plattform und die Weiterleitungen sollten nun ohne Probleme funktionieren. Im nächsten Schritt schließen wir dieses Tutorial ab. Es fehlt nur noch die Funktion „Passwort vergessen“.
Integration der Passwort vergessen Funktion
Um die Integration des eigenen Benutzertyps abzuschließen, zeige ich Dir in diesem Schritt, wie Du die „Passwort vergessen“ Funktion für Deinen eigenen Benutzertyp integrierst. Dazu musst Du die bestehenden Controller, Views und E-Mail Vorlagen adaptieren. Zuletzt müssen wir noch die neuen Routen bereitstellen.
Beginnen wir nun mit den Views. Kopiere das „passwords“ Verzeichnis mit den Views aus dem Verzeichnis „resources/views/auth“ und füge sie in Deinem View Ordner ein. In meinem Beispiel ist das „resources/vies/businessuser“. Nun passe noch die Routen der Formulare an Deine späteren Routen an.
Dieser Schritt bedarf keiner weiteren Erläuterung. Nachfolgend findest Du aber noch mals die neuen Views.
@extends('layouts.app')
@section('content')
<div class="container">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card">
<div class="card-header">{{ __('Reset Password') }}</div>
<div class="card-body">
@if (session('status'))
<div class="alert alert-success" role="alert">
{{ session('status') }}
</div>
@endif
<form method="POST" action="{{ route('business_user.password.email') }}">
@csrf
<div class="form-group row">
<label for="email" class="col-md-4 col-form-label text-md-right">{{ __('E-Mail Address') }}</label>
<div class="col-md-6">
<input id="email" type="email" class="form-control{{ $errors->has('email') ? ' is-invalid' : '' }}" name="email" value="{{ old('email') }}" required>
@if ($errors->has('email'))
<span class="invalid-feedback" role="alert">
<strong>{{ $errors->first('email') }}</strong>
</span>
@endif
</div>
</div>
<div class="form-group row mb-0">
<div class="col-md-6 offset-md-4">
<button type="submit" class="btn btn-primary">
{{ __('Send Password Reset Link') }}
</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
@endsection
@extends('layouts.app')
@section('content')
<div class="container">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card">
<div class="card-header">{{ __('Reset Password') }}</div>
<div class="card-body">
<form method="POST" action="{{ route('business_user.password.update') }}">
@csrf
<input type="hidden" name="token" value="{{ $token }}">
<div class="form-group row">
<label for="email" class="col-md-4 col-form-label text-md-right">{{ __('E-Mail Address') }}</label>
<div class="col-md-6">
<input id="email" type="email" class="form-control{{ $errors->has('email') ? ' is-invalid' : '' }}" name="email" value="{{ $email ?? old('email') }}" required autofocus>
@if ($errors->has('email'))
<span class="invalid-feedback" role="alert">
<strong>{{ $errors->first('email') }}</strong>
</span>
@endif
</div>
</div>
<div class="form-group row">
<label for="password" class="col-md-4 col-form-label text-md-right">{{ __('Password') }}</label>
<div class="col-md-6">
<input id="password" type="password" class="form-control{{ $errors->has('password') ? ' is-invalid' : '' }}" name="password" required>
@if ($errors->has('password'))
<span class="invalid-feedback" role="alert">
<strong>{{ $errors->first('password') }}</strong>
</span>
@endif
</div>
</div>
<div class="form-group row">
<label for="password-confirm" class="col-md-4 col-form-label text-md-right">{{ __('Confirm Password') }}</label>
<div class="col-md-6">
<input id="password-confirm" type="password" class="form-control" name="password_confirmation" required>
</div>
</div>
<div class="form-group row mb-0">
<div class="col-md-6 offset-md-4">
<button type="submit" class="btn btn-primary">
{{ __('Reset Password') }}
</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
@endsection
Nun duplizierst Du die Controller „ForgotPasswordController“, sowie „ResetPasswordController“ und gibt’s ihnen einen neuen Namen. Im neuen „ResetPasswordController“ passt Du den RedirectTo Pfad an und den beiden neuen Controllern die Middleware im Konstruktor. Wir werden in den Controllern mit dem Password Broker von Laravel arbeiten. Dieser verwaltet alles rund um die Passwörter des Benutzers und ist wieder festgelegt auf das User Model.
In den duplizierten Controllern müssen nun einige Funktionen integriert werden um die verwendeten Traits zu überschreiben. Im folgenden findest Du den angepassten „ForgotPasswordController“:
<?php
namespace AppHttpControllersAuth;
use AppHttpControllersController;
use IlluminateFoundationAuthSendsPasswordResetEmails;
use IlluminateSupportFacadesPassword;
class ForgotBusinessUserPasswordController extends Controller
{
/*
|--------------------------------------------------------------------------
| Password Reset Controller
|--------------------------------------------------------------------------
|
| This controller is responsible for handling password reset emails and
| includes a trait which assists in sending these notifications from
| your application to your users. Feel free to explore this trait.
|
*/
use SendsPasswordResetEmails;
/**
* Create a new controller instance.
*
* @return void
*/
public function __construct()
{
$this->middleware('guest:business_user');
}
/**
* Get the broker to be used during password reset.
*
* @return IlluminateContractsAuthPasswordBroker
*/
protected function broker()
{
return Password::broker('business_users');
}
/**
* Display the form to request a password reset link.
*
* @return IlluminateHttpResponse
*/
public function showLinkRequestForm()
{
return view('businessuser.passwords.email');
}
}
Im neuen „ForgotPasswordController“ wurde der Passwort Namespace von Laravel hinzugefügt, damit wir Zugriff auf den Passwort Broker haben. Danach wurden die beiden Trait-Funktionen „broker“ und „showLinkRequestForm“ überschrieben. Es ist für diesen Schritt übrigens sehr wichtig, dass Du in der „auth“ Konfiguration den Knoten für Passwort mit Deinem neuen User Provider befüllt hast, wie es zu beginn dieses Tutorials erklärt wurde. Ansonsten Kommt es im Broker zu Problemen. Dadurch dass der Passwort Broker nun den korrekten Provider übergeben bekommen hat, kümmert er sich um alle relevenaten Schritte des Passwort-Managements.
Und hier folgt noch der zweite Controller „ResetPasswordController“:
<?php
namespace AppHttpControllersAuth;
use AppHttpControllersController;
use IlluminateFoundationAuthResetsPasswords;
use IlluminateHttpRequest;
use IlluminateSupportFacadesPassword;
use IlluminateSupportFacadesAuth;
class ResetBusinessUserPasswordController extends Controller
{
/*
|--------------------------------------------------------------------------
| Password Reset Controller
|--------------------------------------------------------------------------
|
| This controller is responsible for handling password reset requests
| and uses a simple trait to include this behavior. You're free to
| explore this trait and override any methods you wish to tweak.
|
*/
use ResetsPasswords;
/**
* Where to redirect users after resetting their password.
*
* @var string
*/
protected $redirectTo = '/business-user/home';
/**
* Create a new controller instance.
*
* @return void
*/
public function __construct()
{
$this->middleware('guest:business_user');
}
/**
* Display the password reset view for the given token.
*
* If no token is present, display the link request form.
*
* @param IlluminateHttpRequest $request
* @param string|null $token
* @return IlluminateContractsViewFactory|IlluminateViewView
*/
public function showResetForm(Request $request, $token = null)
{
return view('business_user.passwords.reset')->with(
['token' => $token, 'email' => $request->email]
);
}
/**
* Get the guard to be used during password reset.
*
* @return IlluminateContractsAuthStatefulGuard
*/
protected function guard()
{
return Auth::guard('business_user');
}
/**
* Get the broker to be used during password reset.
*
* @return IlluminateContractsAuthPasswordBroker
*/
protected function broker()
{
return Password::broker('business_users');
}
}
Genauso wie im ersten Controller wurden hier einige Trait-Funktionen überschrieben, damit der korrekte Broker, sowie die korrekte Guard verwendet werden.
Als nächstes betrachten wir die Anpassung der E-Mail Notification. Diese wird nicht über die Controller gesteuert, sondern über den versteckten Trait „CanResetPassword“ der in der User Klasse mit dem Namespace „IlluminateFoundationAuth“ integriert wurde. Deine neuer Benutzertyp verwendet also ebenfalls diesen Trait und verfügt somit über die Funktion „sendPasswordResetNotification“, welche die E-Mail zum zurücksetzten des Passworts verschickt. Leider ist auch diese auf das normale User Model gemünzt, was aber kein Problem darstellt, da wir die Funktion des Traits nun einfach überschreiben.
Erzeuge zunächst eine neue Notification mittels Artisan:
$ php artisan make:notification ResetBusinessUserPasswordNotification
Die Notification passt Du nun folgendermaßen an:
<?php
namespace AppNotifications;
use IlluminateBusQueueable;
use IlluminateSupportFacadesLang;
use IlluminateNotificationsNotification;
use IlluminateContractsQueueShouldQueue;
use IlluminateNotificationsMessagesMailMessage;
class ResetBusinessUserPasswordNotification extends Notification implements ShouldQueue
{
use Queueable;
/**
* The password reset token.
*
* @var string
*/
public $token;
/**
* The callback that should be used to build the mail message.
*
* @var Closure|null
*/
public static $toMailCallback;
/**
* Create a notification instance.
*
* @param string $token
* @return void
*/
public function __construct($token)
{
$this->token = $token;
}
/**
* Get the notification's delivery channels.
*
* @param mixed $notifiable
* @return array
*/
public function via($notifiable)
{
return ['mail'];
}
/**
* Build the mail representation of the notification.
*
* @param mixed $notifiable
* @return IlluminateNotificationsMessagesMailMessage
*/
public function toMail($notifiable)
{
if (static::$toMailCallback) {
return call_user_func(static::$toMailCallback, $notifiable, $this->token);
}
return (new MailMessage)
->subject(Lang::getFromJson('Reset Password Notification'))
->line(Lang::getFromJson('You are receiving this email because we received a password reset request for your account.'))
->action(Lang::getFromJson('Reset Password'), url(config('app.url').route('business_user.password.reset', $this->token, false)))
->line(Lang::getFromJson('If you did not request a password reset, no further action is required.'));
}
/**
* Set a callback that should be used when building the notification mail message.
*
* @param Closure $callback
* @return void
*/
public static function toMailUsing($callback)
{
static::$toMailCallback = $callback;
}
/**
* Get the array representation of the notification.
*
* @param mixed $notifiable
* @return array
*/
public function toArray($notifiable)
{
return [
//
];
}
}
Die neue Notification nimmt genau wie die Alte einen Token entgegen, über den die Plattform später den Benutzer identifizieren kann. Zusätzlich wird in der Url der korrekte Pfad auf die neue Reset View hinterlegt.
Öffne nun Dein neues User-Model und füge folgende Funktion hinzu:
<?php
namespace App;
use AppNotificationsResetBusinessUserPasswordNotification;
use IlluminateNotificationsNotifiable;
use IlluminateContractsAuthMustVerifyEmail;
use IlluminateFoundationAuthUser as Authenticatable;
class BusinessUser extends Authenticatable implements MustVerifyEmail
{
use Notifiable;
protected $guard = 'business_user';
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'name',
'email',
'password',
'company',
'vat',
'street_name',
'street_number',
'city',
'zip'
];
/**
* The attributes that should be hidden for arrays.
*
* @var array
*/
protected $hidden = [
'password', 'remember_token',
];
/**
* Versendet eine E-Mail-Verifizierungs Benachrichtigung an den Benutzer
*/
public function sendEmailVerificationNotification()
{
$this->notify(new NotificationsBusinessUserVerifyEmail);
}
/**
* Send the password reset notification.
*
* @param string $token
* @return void
*/
public function sendPasswordResetNotification($token)
{
$this->notify(new ResetBusinessUserPasswordNotification($token));
}
}
Die letzte Änderung, die Du nun noch vornehmen musst, ist die Änderung des Links in Deiner neuen Login-View. Dort ist noch die alte Route zum „Passwort vergessen“ des normalen User-Models hinterlegt. Öffne nun die View und passe die Route darin an:
<a class="btn btn-link" href="{{ route('business_user.password.request') }}">
{{ __('Forgot Your Password?') }}
</a>
Zusammenfassung
In diesem etwas umfangreicheren Tutorial hast Du gelernt, wie Du Deinen eigenen Benutzertypen ergänzend zum normalen Laravel User Model integrieren kannst. Du hast zu Beginn den Unterschied zwischen einer Benutzerrolle und einem Benutzertypen kennengelernt. Direkt im Anschluss hast Du die normale Laravel Authentication integriert und migriert.
Basierend auf dem normalen User Model hast Du Dein eigenes User Model samt Migration angelegt. Um später mit der Zugriffsbeschränkung und den Controllern arbeiten zu können hast Du in der „auth“ Konfiguration von Laravel Deine Guard, Deinen neuen Provider und die Konfiguration des eigenen Passwort Brokers hinterlegt.
Mittels der „auth“ Middleware von Laravel hast Du erfolgreich Deine Controller vor dem unbefugten Zugriff geschützt und sichergestellt, dass nur Benutzer mit dem neuen Benutzertyp in das zugehörige Backend gelangen.
Im Anschluss an die Zugriffsbeschränkung hast Du entweder die Registrierung Deines neuen Benutzertypes oder das Database Seeding integriert, je nachdem, ob sich Dein neuer Benutzertyp registrieren darf. Wenn Du die Registrierung integriert hast, hast Du ebenfalls gelernt, wie Du Deine eigene E-Mail Verifizierung nach dem Vorbild von Laravel 5.7 integrierst.
Nach der Registrierung oder dem Database Seeding habe ich Dir gezeigt, wie Du Deinen eigenen Login Controller samt Views einbaust. Somit können sich Deine Benutzer des neuen Benutzertypes an Deiner Plattform anmelden.
Last but not least hast Du die Passwort vergessen Funktion integriert und Dich mit dem Passwort Broker von Laravel auseinandergesetzt.
Fragen und Anregungen
Solltest Du Fragen, Anregungen oder Ergänzungen zum Artikel haben, zögere nicht mir diese in einem Kommentar mitzuteilen.
Zugriff auf FTP Storage über Laravel 5.6
Der Zugriff auf einen FTP Server, sowie auf viele andere Speicherorte ist eine gängige Aufgabe in der Programmierung. Das PHP Framework Laravel bietet für den Zugriff auf viele unterschiedliche Speicherorte eine generische Zugriffsmethode. Amazons S3, der lokale Server-Speicher, FTP Server, etc. werden als disks behandelt. Laravel bietet diese Funktion dank des PHP Pakets Flysystem von Frank de Jonge an. Detailliertere Informationen zu allen Speichermethoden findet Ihr wie immer in der Dokumentation von Laravel.
In diesem Artikel betrachten wir das Szenario, dass es einen täglichen CSV Export in ein Verzeichnis eines FTP Servers gibt. Die Plattform soll diese Datei dann mit einem nächtlich geplanten Task vom FTP Server laden, überprüfen, ob die Datei bereits importiert wurde und wenn dies nicht der Fall ist, die Datei über die Task Queue importieren. Ich habe auch einen interessanten Artikel über die Laravel Task Queue und einen damit verbundenen CSV Import geschrieben, der für Dich ebenfalls interessant sein könnte.
Ich gehe in diesem Artikel nicht auf die Einrichtung eines kompletten Laravel Projektes ein. Eine gute Anleitung dazu findest Du ebenfalls in der Laravel Dokumentation zur Projekterstellung.
FTP Filesystem Konfiguration
Damit Du FTP als disk benutzen kannst, ist es zunächst wichtig Änderungen an der Konfigurationsdatei "config/filesystems.php" vorzunehmen. Der Array-Knoten "disks" muss dazu um folgenden Block "ftp" erweitert werden:
'disks' => [
'local' => [
'driver' => 'local',
'root' => storage_path('app'),
],
'public' => [
'driver' => 'local',
'root' => storage_path('app/public'),
'url' => env('APP_URL').'/storage',
'visibility' => 'public',
],
's3' => [
'driver' => 's3',
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION'),
'bucket' => env('AWS_BUCKET'),
'url' => env('AWS_URL'),
],
'ftp' => [
'driver' => 'ftp',
'host' => 'ftp.example.com',
'username' => 'your-username',
'password' => 'your-password',
// Optional FTP Settings...
// 'port' => 21,
// 'root' => '',
// 'passive' => true,
// 'ssl' => true,
// 'timeout' => 30,
],
],
Du kannst nun die Zugangsdaten zum FTP Server in den Feldern "host", "username" und "password" hinterlegen. Entweder trägst Du diese direkt ein, oder Du hinterlegst diese in der ".env" Datei Deines Projektes und lädst die Daten wie im Knoten "s3" über die "env()" Funktion. Dies hat den Vorteil, dass Du je nach Umgebung andere Daten hinterlegen kannst, ohne jedesmal Änderungen am Quellcode durchführen zu müssen. Hier findest nochmal ein Beispiel mit env Variablen:
'ftp' => [
'driver' => 'ftp',
'host' => env('FTP_HOST', 'ftp.example.com'),
'username' => env('FTP_USERNAME', 'your-username'),
'password' => env('FTP_PASSWORD', 'your-password'),
// Optional FTP Settings...
// 'port' => env('FTP_PORT', 21),
// 'root' => env('FTP_ROOT', ''),
// 'passive' => env('FTP_MODE_PASSIVE', true),
// 'ssl' => env('FTP_SSL', true),
// 'timeout' => env('FTP_TIMEOUT', 30),
],
In der ".env" Datei musst Du diese Variablen jetzt natürlich auch noch hinterlegen. Füge diese am Ende (oder einer von Dir favorisierten Stelle) der Datei ein:
FTP_HOST=ftp.example.com FTP_USERNAME=your-username FTP_PASSWORD=your-password FTP_PORT=21 FTP_ROOT= FTP_MODE_PASSIVE=true FTP_SSL=true FTP_TIMEOUT=30 FTP_IMPORT_DIR="test_verzeichnis/csv_exporte"
Um später den Import-Pfad dynamisch anpassen zu können, hinterlege ich auch diesen als Environment Variable. Nun legen wir noch eine eigene Konfigurationsdatei an, deren Werte wir zum späteren Zeitpunkt mit der "config" Helper-Funktion auslesen können. Infos zu dieser Funktion findest Du im Abschnitt Helper Dokumentation von Laravel. Erstelle nun die Datei "ftpcsvimport.php" im Verzeichnis "config" mit folgendem Inhalt:
<?php
return [
/*
|--------------------------------------------------------------------------
| FTP Import-Verzeichnis
|--------------------------------------------------------------------------
|
| Das Verzeichnis ist das Basisverzeichnis für die zu importierenden
| Dateien.
|
*/
'importDirectory' => env('FTP_IMPORT_DIR', ''),
/*
|--------------------------------------------------------------------------
| Benötigte Spalten-Header
|--------------------------------------------------------------------------
|
| Die folgenden Spalten müssen für den Import bereitstehen.
|
*/
'headers' => [
'dataId',
'dataName',
'dataValue',
],
/*
|--------------------------------------------------------------------------
| Pflichtspalteninhalte
|--------------------------------------------------------------------------
|
| Die folgenden Spalten der CSV Datei müssen über einen validen Inhalt
| verfügen.
|
*/
'requiredColumns' => [
'dataId',
'dataName',
'dataValue',
],
];
Neben der Konfiguration für das Import-Verzeichnis wurden auch die Konfigurationen für den CSV Import bereits hinterlegt. An dieser Stelle musst Du Deine benötigten CSV Spalten-Header und Deine Pflichtspalten, also Spalten die über Inhalte verfügen müssen, hinterlegen. Ich verwende lediglich Beispieldaten zu Anschauungszwecken.
Damit Deine durchgeführte Konfiguration wirksam wird, musst Du zunächst den Laravel Cache neu befüllen. Das kannst Du ganz leicht über den folgenden Konsolenbefehl:
$ php artisan config:cache
Erstellung des CSV-Import Jobs
Die Konfiguration der disks ist abgeschlossen, aber wie bekommen wir nun unsere CSV Datei vom FTP Server? Wir müssen zunächst ein neues Eloquent Model samt Migration anlegen, um bereits importierte Dateien auszuschließen. Dazu führst Du folgenden Befehl im Terminal aus:
$ php artisan make:model ImportedFile -m
Dieser Befehl legt das neue Model "ImportedFile" im Ordner "app/ImportedFile.php" an. Durch das "-m" wird eine passende Datenbankmigration für die neue Tabelle "imported_files" im Ordner "database/migrations" angelegt. Der Name der Datei sollte folgendem Aufbau besitzen "2018_07_16_093346_create_imported_files_table.php". Selbstverständlich ist bei Dir der Zeitpunkt vor "create_imported_files_table.php" ein anderer als bei mir.
Öffne nun die Migrationsdatei und passe den Inhalt von Schema::create in der Funktion "up" folgendermaßen an:
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('imported_files', function (Blueprint $table) {
$table->increments('id');
$table->timestamps();
$table->string('file_name')
->unique();
});
}
Durch den "unique" Index muss der Dateiname der Importierten Datei eindeutig und einzigartig sein, und darf nicht nochmals importiert werden. Diese Property muss nun auch noch im Model implementiert werden. Dazu öffnest Du die Datei "app/ImportedFile.php" und fügst folgenden Inhalt ein:
<?php
namespace App;
use IlluminateDatabaseEloquentModel;
/**
* @property string file_name
*/
class ImportedFile extends Model
{
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'file_name',
];
}
Zum Schluss musst Du die Migration noch durchführen, damit die Änderungen per "code first" in die Datenbank übertragen werden. Verwende dazu folgenden Befehl in der Konsole (Vergiss aber nicht zuvor die Datenbank Verbindungsvariablen in der .env Datei korrekt zu befüllen):
$ php artisan migrate
Jetzt können wir mit der Erstellung des Jobs fortfahren. Der Job wird später automatisch zu einem geplanten Zeitpunkt (task scheduling) "gequeued" und von einem freien konfigurierten Queue-Worker ausgeführt. Um einen Job anzulegen führst Du folgenden Befehl aus:
$ php artisan make:job ImportCsvFromFtpServer
Im Verzeichnis "app/Jobs" wurde nun die Job Datei "ImportCsvFromFtpServer.php" angelegt. Der CSV Import im Job wird später über das PHP Paket league/csv in der Version 9.0 durchgeführt. Dieses kannst Du über den PHP Abhängigkeitsmanager Composer mit folgendem Befehl installieren:
$ composer require league/csv:^9.0
Es steht Dir natürlich frei, ein anderes PHP Paket, oder zu einem späteren Zeitpunk eine aktuellere Version von league/csv zum Import zu verwenden, oder den Import bspw. über die PHP Funktion str_getcsv selbst zu implementieren. Ich habe mit dem Paket von thephpleague bisher gute Erfahrungen gemacht, da der Implementierungsaufwand um ein vielfaches geringer ist. Das zahlt sich nicht nur für Dich aus, sondern auch für Projektkosten Deines Kunden. Durch den "autoload" Knoten in der "league/csv" composer.json musst Du das Paket nicht in Deiner "app.php" Konfiguration hinterlegen.
Im Import Job ImportCsvFromFtpServer erstellst Du nun zunächst die privaten Properties für das Import Verzeichnis und den CSV Import und befüllst sie im Konstruktor:
/**
* @var array
*/
private $requiredHeaders;
/**
* @var array
*/
private $requiredColumns;
/**
* @var string
*/
private $importDirectory;
/**
* ImportCsvFromFtpServer constructor.
*/
public function __construct()
{
$this->requiredHeaders = config('ftpcsvimport.headers');
$this->requiredColumns = config('ftpcsvimport.requiredColumns');
$this->importDirectory = config('ftpcsvimport.importDirectory');
}
Nachdem im Konstruktor das Import-Verzeichnis und die Import-Konfigurationen ermittelt wurden, können wir uns mit dem Import beschäftigen. Aber wie importieren wir nun alle bisher nicht importierten Dateien? Um zu überprüfen, welche Dateien noch nicht importiert wurden, müssen wir erst einmal alle Dateinamen aus dem Import-Verzeichnis ermitteln. Dazu können wir auf unserer FTP disk die Methode "files" verwenden, der wir einen Verzeichnispfad übergeben und zwar den, den wir in der Environment Variable hinterlegt haben.
Danach werden alle Dateinamen der mit einer Schleife durchlaufen und es wird für jeden Dateinamen überprüft, ob er in der Datenbank existiert. Ist das nicht der Fall, so können wir ihn in eine Liste mit nicht importierten Dateien aufnehmen, die zuletzt geladen und importiert werden. In der Praxis sieht das nun folgendermaßen aus:
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
/** @var array $allFilesInDirectory */
$allFilesInDirectory = Storage::disk('ftp')
->files($this->importDirectory);
if(empty($allFilesInDirectory))
return;
/** @var array $filesToImport */
$filesToImport = [];
/** @var string $file */
foreach ($allFilesInDirectory as $file) {
if(ImportedFile::where('file_name', '=', $file)->exists())
continue;
$filesToImport[] = $file;
}
if(!empty($filesToImport))
$this->importFiles($filesToImport);
}
Am Ende der Funktion "handle" wird die Funktion "importFiles" aufgerufen. Diese ist zuständig für das Abarbeiten der Liste aller zu importierenden Dateien. Des weiteren beinhaltet sie die Verwaltung der Datenbank-Transaktion und das Error-Logging:
/**
* Bearbeitet eine Liste von zu importierenden CSV Dateien.
*
* @param array $filesToImport
*/
private function importFiles( $filesToImport )
{
try {
DB::beginTransaction();
/** @var string $file */
foreach ($filesToImport as $file) {
if (empty($file) || !Storage::disk('ftp')->exists($file))
continue;
if($this->importFile($file))
ImportedFile::create([
'file_name' => $file
]);
}
DB::commit();
} catch (PDOException $e) {
DB::rollBack();
Log::error('Während des automatischen CSV-Imports vom FTP-Server kam es zum PDO Fehler innerhalb der
Transaktion. Es wurde ein Rollback durchgeführt um den alten Stand der Datenbank wieder
herzustellen.');
Log::error(sprintf('FTP CSV-Import Fehlermeldung: %1$s', $e->getMessage()));
Log::error(sprintf('FTP CSV-Import Stacktrace: %1$s', $e->getTraceAsString()));
} catch (Exception $e) {
DB::rollBack();
Log::error('Während des automatischen CSV-Imports vom FTP-Server kam es zum allgemeinen Fehler innerhalb der
Transaktion. Es wurde ein Rollback durchgeführt um den alten Stand der Datenbank wieder
herzustellen.');
Log::error(sprintf('FTP CSV-Import Fehlermeldung: %1$s', $e->getMessage()));
Log::error(sprintf('FTP CSV-Import Stacktrace: %1$s', $e->getTraceAsString()));
}
}
Sollte während des Imports ein Fehler auftreten, so wird ein Rollback der Transaktion durchgeführt und die importierten Daten wieder entfernt. Nach dem Rollback erfolgt das Logging mit dem konfigurierten Treiber.
Innerhalb der Schleife kann man prüfen, ob die aktuelle Datei im Dateisystem existiert. Ist das der Fall, dann wird der Dateiname an die Funktion "importFile" übergeben, die dann mittels league/csv den CSV Import in die Datenbank durchführt. Wurde die Datei erfolgreich importiert, so gibt die Funktion ein "true" zurück und es wird ein neuer Eintrag in der Tabelle "imported_files" hinzugefügt.
Zu guter Letzt müssen wir nun noch die Funktion "importFile" implementieren. Diese benötigt noch einige Helfer-Funktionen die wir zuvor noch im Job integrieren. Die folgenden Funktionen überprüfen, ob die benötigten Spalten-Header in der CSV-Datei existieren und ob die benötigten Spalten mit Inhalten gefüllt sind. Zuletzt folgt noch eine Funktion zur einfachen Ermittlung eines Array Wertes.
/**
* Überprüft, ob alle benötigten Spalten vorhanden sind.
*
* @param string[] $csvHeaders
*
* @return bool
*/
private function requiredHeadersExist( $csvHeaders )
{
if(empty($this->requiredHeaders))
return false;
/** @var array $reqKeysFlipped */
$reqKeysFlipped = array_flip($this->requiredHeaders);
/** @var array $headersFlipped */
$headersFlipped = array_flip( $csvHeaders);
/** @var array $intersection */
$intersection = array_intersect_key($reqKeysFlipped, $headersFlipped);
return count($intersection) === count($this->requiredHeaders);
}
/**
* Überprüft, ob alle benötigten Spalten gefüllt sind
*
* @param array $row
* @return bool
*/
private function requiredColumnsFilled( $row )
{
/** @var string $requiredColumnKey */
foreach ($this->requiredColumns as $requiredColumnKey) {
if(trim($row[$requiredColumnKey]) === "")
return false;
}
return true;
}
/**
* Holt einen korrekten Wert auf einem ResultSet Array
*
* @param mixed $val
* @param mixed $default
* @return mixed
*/
private function getValue($val, $default)
{
return $val === "" ? $default : $val;
}
Die Import Funktion ist sehr komplex und kann auch noch weiter refaktorisiert und aufgespalten werden. Die einzelnen Abschnitte der Funktion kann man im Nachhinein erklären. Wichtig ist, dass Du genau wie in der "ftpcsvimport.php" Dein Model integrierst, das importiert werden soll. Im Beispiel ist das Model "Data" symbolisch für Dein Model hinterlegt. Das Beispiel dient lediglich als Implementierungsgrundlage und muss von Dir angepasst werden.
/**
* Importiert eine CSV Datei.
*
* @param string $file
* @return bool
*/
private function importFile( $file )
{
try {
/** @var string $fileData */
$fileData = Storage::disk( 'ftp' )
->get( $file );
/** @var Reader $reader */
$reader = Reader::createFromString($fileData);
$reader->setDelimiter(';');
$reader->setHeaderOffset(0);
/** @var ResultSet $resultSetRecords */
$resultSetRecords = (new Statement())->process($reader);
if(!$this->requiredHeadersExist($resultSetRecords->getHeader())) {
Log::error(sprintf('Die zu importierende CSV-Datei "%1$s" verfügt nicht über die benötigten Spalten-Header. Die Datei wurde nicht importiert.', $file));
return false;
}
/** @var Generator $csvRows */
$csvRows = $resultSetRecords->getRecords();
if(empty($csvRows) || !($csvRows instanceof Generator) || (is_array($csvRows) && sizeof($csvRows) === 0)) {
Log::error(sprintf('Die zu importierende CSV-Datei "%1$s" verfügt über keinen Inhalt. Die Datei wurde nicht importiert.', $file));
return false;
}
/** @var int $countImportedRows */
$countImportedRows = 0;
/** @var array $dataRow */
foreach ($csvRows as $dataRow) {
if(!$this->requiredColumnsFilled($dataRow))
continue;
/** @var int $dataId */
$dataId = $this->getValue($dataRow['dataId'], 0);
/** @var Data $data */
$data = Data::where('id', $dataId)
->first();
if(empty($data)) {
Data::create([
'id' => $dataId,
'name' => $this->getValue($dataRow['dataName'], ''),
'value' => $this->getValue($dataRow['dataValue'], '')
]);
$countImportedRows++;
}
}
if($countImportedRows === 0) {
Log::error(sprintf('Die zu importierende CSV-Datei "%1$s" verfügt über keinen Inhalt. Die Datei wurde nicht importiert.', $file));
return false;
}
return true;
} catch (FileNotFoundException $e) {
Log::error(sprintf('Beim Importieren der Datei "%1$s" trat eine FileNotFoundException auf. Die Datei wurde nicht importiert.', $file));
return false;
} catch (CsvException $e) {
Log::error(sprintf('Beim Importieren der Datei "%1$s" trat eine LeagueCsvException auf. Die Datei wurde nicht importiert.', $file));
return false;
}
}
Im ersten Abschnitt der Funktion werden die Inhalte der zu importierenden Datei vom FTP Server als Zeichenkette geladen. Über den League Reader werden diese danach mit den CSV Einstellungen, wie bspw. dem Trennzeichen, als ResultSet geladen:
/** @var string $fileData */
$fileData = Storage::disk( 'ftp' )
->get( $file );
/** @var Reader $reader */
$reader = Reader::createFromString($fileData);
$reader->setDelimiter(';');
$reader->setHeaderOffset(0);
/** @var ResultSet $resultSetRecords */
$resultSetRecords = (new Statement())->process($reader);
Das ResultSet überprüft man im Anschluss. Es sollte die benötigten Header, sowie einen Inhalt besitzen:
if(!$this->requiredHeadersExist($resultSetRecords->getHeader())) {
Log::error(sprintf('Die zu importierende CSV-Datei "%1$s" verfügt nicht über die benötigten Spalten-Header. Die Datei wurde nicht importiert.', $file));
return false;
}
/** @var Generator $csvRows */
$csvRows = $resultSetRecords->getRecords();
if(empty($csvRows) || !($csvRows instanceof Generator) || (is_array($csvRows) && sizeof($csvRows) === 0)) {
Log::error(sprintf('Die zu importierende CSV-Datei "%1$s" verfügt über keinen Inhalt. Die Datei wurde nicht importiert.', $file));
return false;
}
Nachdem sichergestellt wurde, dass die Datei über die Header, sowie über Inhalt verfügt, beginnt der Import der einzelnen Datensätze der Datei. Mit einer Schleife werden alle Reihen des "Generator" durchlaufen, sichergestellt, dass die benötigten Spalten über Inhalte verfügen und zuletzt das Model in der Datenbank erstellt:
/** @var int $countImportedRows */
$countImportedRows = 0;
/** @var array $dataRow */
foreach ($csvRows as $dataRow) {
if(!$this->requiredColumnsFilled($dataRow))
continue;
/** @var int $dataId */
$dataId = $this->getValue($dataRow['dataId'], 0);
/** @var Data $data */
$data = Data::where('id', $dataId)
->first();
if(empty($data)) {
Data::create([
'id' => $dataId,
'name' => $this->getValue($dataRow['dataName'], ''),
'value' => $this->getValue($dataRow['dataValue'], '')
]);
$countImportedRows++;
}
}
if($countImportedRows === 0) {
Log::error(sprintf('Die zu importierende CSV-Datei "%1$s" verfügt über keinen Inhalt. Die Datei wurde nicht importiert.', $file));
return false;
}
Am Ende der Funktion befindet sich noch ein wenig Error-Handling das du beliebig verfeinern und erweitern kannst.
Task Scheduling implementieren
Die Planung von Aufgaben erfolgt in der Datei "Kernel.php". Diese befindet sich im Verzeichnis "app/Console/". In der Funktion "schedule" wird unser zuvor erstellter Job nun implementiert:
/**
* Define the application's command schedule.
*
* @param IlluminateConsoleSchedulingSchedule $schedule
* @return void
*/
protected function schedule(Schedule $schedule)
{
/**
* Nächtlich laufender Task zum Importieren einer CSV-Datei
* vom FTP Server
* Laufzeit: 0:00 Uhr
*/
$schedule->job(new ImportCsvFromFtpServer)
->dailyAt('0:00');
}
Wird "schedule" nun Aufgerufen, so wird unser Job "geplant" und täglich um "0:00" in die Task-Queue "eingereiht". Aber woher kommt nun die Queue und wie ruft Laravel automatisch um Mitternacht das "einreihen" in die Task-Queue auf? Welche Queue Du verwendest ist zunächst Dir überlassen. Es gibt viele unterschiedliche Anbieter einer Task-Queue. Detailliertere Informationen zur Einrichtung einer Queue kannst Du der Dokumentation zu Queues entnehmen. Auf der Server-Management Plattform Laravel Forge lassen sich sehr einfach produktive Laravel-Umgebungen auf dem Cloud-Server Anbieter Deiner Wahl einrichten. Deine Queues können im Backend einfach angelegt und konfiguriert werden.
Das selbe gilt für die Ausführung Deiner geplanten Tasks. Dazu ist es nötig, dass Du einen Cron-Job auf Deinem Server einrichtest. Wenn Du Dich nicht mit der Einrichtung von Cron-Jobs auskennst, ist die Verwendung eines von Laravel Forge konfigurierten Webservers für Dich die beste Wahl. Dort kannst Du im Backend einfach konfigurieren, wann "schedule" ausgeführt werden soll. Alternativ kannst Du selbstverständlich selber Cron-Jobs anlegen. Hierzu rufst Du in der Konsole die Cron-Job Verwaltung "crontab" auf:
$ crontab -e
Hier kannst Du nun Deine Cron-Jobs des Servers hinterlegen. Über den folgend Eintrag wird der "scheduler" jede Minute einmal "schedule" aufrufen. Sollte der dort konfigurierte Zeitpunkt erreicht sein, wird bspw. der Import Job gequeued.
* * * * * cd /path-to-your-project && php artisan schedule:run >> /dev/null 2>&1
Fazit zum FTP Server
In diesem Artikel habe ich Dir gezeigt, wie Du in Deiner Laravel-Anwendung einen automatischen CSV-Import von einem FTP Server implementierst. Ich hoffe, dass er Dir weiterhelfen konnte. Solltest Du Fragen haben, oder ich etwas wichtiges Vergessen habe, was Dir weiterhelfen könnte, lass mir doch einen Kommentar da und ich versuche Dir weiterzuhelfen.
Durch unsere langjährige Arbeit und über 100 erfolgreiche Projekte, konnten wir viele Erfahrungen sammeln. Dieses Know-How im Online-Marketing gaben wir u.a. bei Vorträgen von Google, der Industrie- und Handelskammer und der Handwerkskammer weiter.
Mit Know-How, Kreativität und Leidenschaft entwickeln wir auf unsere Kunden abgestimmte Marketing-Strategien, die Sie sicher und nachhaltig zum Erfolg führen. Gemeinsam setzen wir Ihr Online-Marketing so um, dass Sie langfristig Ihren Umsatz und Return-On-Investment steigern.
Jetzt kostenlosen Beratungstermin vereinbaren oder unter 0561 / 850 194 76 anrufen.
Impressionen des Sicherheitsforums Nordhessen 2018
Am 20. März war es wieder soweit – das Sicherheitsforum Nordhessen lud Interessierte ein, sich zum Thema IT Sicherheit auszutauschen.
Nach mehreren abwechslungsreichen Impulsvorträgen von unter anderem der Deutschen Post AG, kAssekura, Müller-Goebel und Kollegen, Starke + Reichert und der Moers GmbH folgte eine kurze Pause mit leckeren Snacks und geselligem Austausch.
Dabei trafen wir einige bekannte Gesichter der Kasseler IT-Szene, aber freundeten uns auch mit neuen Teilnehmern an.
Zu Beginn wurden die Teilnehmer vom Datenschutzbeauftragten der Moers GmbH über die Chancen der EU-Datenschutzgrundverordnung aufgeklärt. Auch wenn diese von vielen Teilnehmern kritisch aufgefasst wird, wurden uns so auch die möglichen Vorteile näher gebracht. Mehr dazu erfahren Sie hier: DSB Moers
So berichtete uns die Deutsche Post AG von der Entwicklung ihrer Elektrofahrzeuge Streetscouter und deren aktuellen Erfolgen. Kein Autohersteller konnte damals die Anforderungen der Post erfüllen und so baute man sich einfach selbst eines. Diese Art der Herangehensweise ist sowohl innovativ – fast "Startup-typisch" – als auch etwas trotzig optimistisch und das im positivsten Sinne. Frei nach dem Motto „Wo kein Weg ist, bauen wir uns eben einen.“ Umso herzlicher gratulieren wir der Post zu solch innovativem Denken und der daraus wachsenden Erfolge. Für mehr Infos hier weiterlesen: Streetscouter
kAssekura veranschaulichte anhand zahlreicher Fallbeispiele, dass ein Notfallplan zum Thema IT-Sicherheit äußert wichtig ist, um potentielle Gefahren soweit es geht auszuschließen oder im Ernstfall durch gute Vorbereitung schneller darauf reagieren zu können. Durch Rechenbeispiele wurde einem die potentielle Gefahr in Zahlen aufbereitet vorgestellt, um für das Thema zu sensibilisieren. Mehr dazu finden Sie auf Ihrer Website unter: kAssekura
Der Rechtsanwalt Herr Sittig von Müller-Goebel und Kollegen überraschte uns in seinem Vortrag über die aktuelle Entwicklung von Bitcoins und deren schnell voranschreitende Verbreitung.
Herr Sittig ist deutschlandweit einer der wenigen Anwälte, der im Bezug auf Kryptowährungen und Bitcoins zu den Rechtsexperten zählt und unter anderem die Bafin in diesen Themen berät. Als langjähriger Experte im Bereich Bitcoins ist er nicht nur mit der Szene vernetzt und über aktuelle Entwicklungen vertraut, sondern versteht auch deren Hintergründe und Überzeugungen, was für die Qualität seiner Beratung wesentlich ist. So veranschaulichte er in seinem Vortrag zunächst gekonnt die Eigentümlichkeiten und Mechanismen von Blockchain und Kryptowährungen, um anschließend auf Widersprüche neuer staatlicher Reglementierungen in diesem Bereich aufmerksam zu machen.
In vielen Ländern sind Kryptowährungen, wie Bitcoin, bereits als Zahlungsmittel zugelassen und werden sich vermutlich auch weiter verbreiten, so nach Ansicht des Experten Herr Sittig. Durch die enorm beschleunigte Abwicklung von Zahlungsprozessen sind diese außerdem auch aus Perspektive der Banken äußert relevant, weshalb diese mit eigenen Kryptowährungen experimentieren. Die Website der Kanzlei und Herr Sittig finden Sie hier: Müller-Goebel und Kollegen
Starke + Reichert berichtete uns über Digitalisierungsstrategien und die Notwendigkeit der IT-Sicherheit dafür. Dies müsse selbst für kleine Betriebe nicht unglaublich komplex sein, sondern könne mit einfachen, aber durchdachten Konzepten nachhaltig die Sicherheit verbessern. Für mehr Infos besuchen Sie die Website: Starke + Reichert
Der anschließende Vortrag des Keynote-Speakers Thorsten Höhnke war mehr als nur spannend. Fesselnd berichtete er über aktuelle Entwicklungen, Möglichkeiten It-System zu hacken und Anekdoten aus seiner früheren Zeit beim Chaos-Computer-Club.
Sein Vortrag war ein Einblick in das, was uns „normalen“ IT-affinen Menschen noch verborgen bleibt und gleichzeitig ein erschreckendes Wachrütteln für die Möglichkeiten der Hacking-Optionen.
Dabei wurde uns ausführlich erklärt wie man beispielsweise die SMS-Tan-Methode mit der Man-In-The-Middle Attacke angreift, die Steuergeräte moderner PKW’s spielend umprogrammiert oder man sogar mit mehreren Metern Abstand ein Abbild fremder Kreditkarten ausliest, um diese auf eine andere Kreditkarte zu übertragen und Abbuchungen vorzunehmen.
Doch nicht nur aus technischer Sicht war der Vortrag interessant, sondern auch in Bezug auf den Umgang mit Daten. So fragte Herr Höhnke was wir über die Aussage „Sicherheit brauch ich nicht! Ich habe doch nichts zu verbergen.“ denken. Sein Impuls dazu warum wir dann nicht alle nackt rumlaufen, sondern uns mit Kleidung bedecken, hinterließ einen nachhaltigen Eindruck bei allen Teilnehmern.
Dass der Vortrag nicht nur uns mitriss, ließ sich leicht an der gesteigerten Aufmerksamkeit aller Teilnehmer feststellen. So wechselten sich wissbegierige Nachfragen mit staunendem Raunen ab.
Zum Abschluss gab es einen Blick in die Zukunft von Fujitsu und der Überwachung von Smarten Fabriken, in denen jede Maschine von der Ferne aus kontrolliert werden könne. Produktionsflussoptimierung, Überwachung und Steuerung als Ausblick auf das, was uns demnächst mit der Digitalisierung und Industrie 4.0 erwarten wird. Mehr Informationen und Kontakt zu Thorsten Höhnke finden Sie hier: Hochschule Augsburg - Thorsten Höhnke
Wir freuen uns auf das nächste Sicherheitsforum 2019 und wünschen allen eine sichere (und verschlüsselte) Zeit. ;)
WordPress - Eigener Widget Bereich
Widgets sind ein fester Bestandteil von WordPress, durch sie können unterschiedliche Inhalte wie Bilder, Texte und auch HTML in festgelegten Bereichen der Website hinterlegt werden. Dadurch können auch Website-Betreiber die nicht viel mit Webentwicklung, HTML und CSS zu tun haben, Webinhalte selbstständig tauschen, ohne Gefahr zu laufen, große Fehler in die Website einzubauen.
In diesem Artikel möchte ich Dir erklären, wie Du selbstständig Bereiche für Widgets auf Deiner Website festlegst und wie Du im Anschluss WordPress mitteilst, dass es einen neuen Widget-Bereich gibt, damit dieser im Backend von WordPress auch befüllt werden kann.
Widget breich erstellen – Für wen ist das Tutorial gedacht?
Dieses kleine Tutorial richtet sich an halbwegs erfahrene und natürlich erfahrene WordPress Entwickler, die noch keine Widgets in WordPress integriert haben. Im Zuge dieses Tutorials wirst Du im funktionalen Herzen Deines Themes oder Child-Themes, der Datei „functions.php“ Änderungen vornehmen.
Solltest Du Dir das nicht zutrauen, so frage am besten einen Bekannten mit Kenntnissen in PHP, oder kontaktiere eine Web-Agentur, beide werden Dir sicherlich weiterhelfen können, da die eigentlichen Änderungen nicht allzu komplex sind, aber trotzdem Deine Website lahmlegen können. Änderungen in dieser Datei lassen sich auch nicht einfach zurücksetzen oder wiederherstellen, die einzige Möglichkeit ist der Cache des Editors den Du verwendest, oder ein Server-Backup.
Weiterhin solltest Du keine Code-Änderungen direkt in Deinem WordPress Theme machen. Lege für alle Änderungen am besten ein Child-Theme an. Wie das funktioniert, zeige ich Dir in meinem Artikel „Wie Du in 4 Schritten Dein eigenes WordPress Child Theme erstellst“.
Schritt 1 - Registrierung des Widget Bereiches in der „functions.php“
Kommen wir nun zur Registrierung Deines neuen Widget-Bereiches. Dies geschieht in der sogenannten „functions.php“ Datei. Diese Datei befindet sich im Basis-Verzeichnis Deines Child-Themes. Sollte diese Datei noch nicht existieren, musst Du sie anlegen. Wichtig ist der Name, dieser darf keine Abweichungen haben, sonst wird diese Datei von WordPress nicht berücksichtigt.
Solltest Du die Datei anlegen müssen, achte darauf, dass der erste Text in der Datei „<?php“ ist. Damit erkennt der Webserver, dass nun PHP Code folgt. Das ist wichtig, damit der Code-Schnippsel zur Registrierung Deines neuen Widget-Bereiches ausgeführt wird.
Nun fügst Du folgenden Code-Schnippsel in die Datei ein:
function themename_widgets_init() {
register_sidebar(array(
'name' => 'Mein Widget-Bereich',
// Wichtig: Die "id" kleingeschrieben, keine Leerzeichen
'id' => 'einzigartiger-widget-name',
'description' => 'Das ist meine eigener Widget-Bereich',
'class' => 'widget--einzigartiger-widget-name',
'before_widget' => '<div id="%1$s" class="widget %2$s">',
'after_widget' => '</div>',
'before_title' => '<h2>',
'after_title' => '</h2>',
));
}
add_action( 'widgets_init', 'themename_widgets_init' );
Den Code-Schnippsel fügst Du am besten am Ende der Datei ein, scheue nicht davor, zwei Leerzeilen einzufügen, bevor Du den Widget-Bereich Code einfügst, das sorgt für eine bessere Lesbarkeit und Übersicht. Nachfolgend habe ich für Dich noch ein paar Beispiele dafür, wo Du den Code einfügen solltest.
Hier siehst du, wie die Datei aussehen sollte, wenn die „function.php“ zuvor leer war, oder Du sie anlegen musstest:
Und hier findest Du noch ein Beispiel dafür, wo Du den Code-Schnippsel einfügen solltest, wenn die Datei bereits existiert und auch schon Inhalt hat:
Schritt 2 - Ersetzung von Platzhaltern im Widget-Code-Schnippsel
Nachdem Du den Code-Schnippsel eingefügt hast, solltest Du einzelne Texte und Inhalte anpassen, er dient nämlich nur als Vorlage.
Beginnen solltest Du mit dem Namen des Code-Schnippsels, auch „Funktion“ genannt. Deine Funktion startet mit der Zeile „function themename_widgets_init() {“. Nun kannst Du „themename“ gegen einen beliebigen Text tauschen. Am besten ist es, wenn dieser Text eindeutig ist, da es in Deinem PHP Progamm-Code der Website keine Duplikate bei den Namen der Funktionen geben darf. Sollte es mehrfach die Funktion mit dem Namen „themename_widgets_init“ geben, so weiß Dein Webserver nicht mehr, welche der Funktionen er verwenden soll, wenn die Funktion verwendet werden soll. Das führt dazu, dass Deine Seite nicht mehr erreichbar ist.
Weiterhin solltest Du beachten, dass Namen für Funktionen keine Leerzeichen, Sonderzeichen und Zahlen enthalten dürfen. Beschränke dich auf alle Buchstaben des englischen Alphabets und auf Unterstriche anstelle von Leerzeichen.
Den neuen Name der Funktion musst Du ebenfalls in der letzten Zeile des Code-Schnippsels einfügen. In der letzten Zeile verwendest Du eine Funktion von Wordpress, mit der Du Wordpress „mitteilst“, dass es Dein Widget registrieren soll. Tausche nun also den Name nach folgendem Schema aus und setzt statt „neuer_theme_name“ den Namen Deines Themes ein:
function neuer_theme_name_widgets_init() {
register_sidebar(array(
'name' => 'Mein Widget-Bereich',
// Wichtig: Die "id" kleingeschrieben, keine Leerzeichen
'id' => 'einzigartiger-widget-name',
'description' => 'Das ist meine eigener Widget-Bereich',
'class' => 'widget--einzigartiger-widget-name',
'before_widget' => '<div id="%1$s" class="widget %2$s">',
'after_widget' => '</div>',
'before_title' => '<h2>',
'after_title' => '</h2>',
));
}
add_action( 'widgets_init', 'neuer_theme_name_widgets_init' );
Im nächsten Schritt geht es an die Anpassung der Widget-Konfiguration. Diese befindet sich ein einem sogenannten „Array“. Array steht übersetzt ins deutsche für „Reihenfolge“, „Anordnung“, oder auch „Matrix“. Ein Array ist nichts anderes als eine Auflistung. Du kannst Dir diese vorstellen wie eine zweispaltige Excel Tabelle. Der Aufbau der Liste ist „Key-Value“ basiert, also eine einfache Auflistung mit „Name => Wert“. Der Name steht auf der linken Seite und ist wichtig für WordPress, an ihm darfst Du keine Änderungen vornehmen.
In Deiner Funktion hast Du die folgenden „Key‘s“:
- name
- id
- description
- class
- before_widget
- after_widget
- before_title
- after_title
Wichtig für Dich sind aber nur die Array-Einträge zu den Key‘s:
- name
- id
- description
- class
Über die Key‘s „name“ und „description“ kannst Du die Darstellung des Widget-Bereiches im WordPress Backend anpassen. „id“ und „class“ werden für die Darstellung des Widgets im Frontend der Website benötigt.
Die „id“ sollte einen eindeutigen Namen erhalten, ähnlich wie bei Deiner Funktion. Ein möglicher Name könnte sich aus dem Name Deines Themes und der Funktion des Widgets ergeben „themename-greeting-widget“.
Die „class“ kannst Du verwenden, um über den CSS-Customizer ein eigenes Aussehen für Dein Widget festzulegen. Eine mögliche Klasse („class“) für Dein Widget könnte sich ähnlich wie bei der „id“ zusammensetzen „widget—themename-greeting“. Im CSS-Customizer könntest Du Dein Widget nun wie folgt ansprechen und anpassen:
.widget--themename-greeting {
background-color: #eeeeee;
border: 1px solid #000000;
}
Schritt 3 – Platzierung des Widgets im Theme
Nun hast Du Dein Widget bereits registriert und die Grundlage geschaffen, eine gestalterische Anpassung im CSS Customizer durchzuführen. Im letzten Schritt musst Du nun noch an der Stelle in Deinem Theme einen Code einbauen, an der Dein Widget später dargestellt werden soll. Dies ist der leichteste Schritt, insofern Du diese Stelle leicht identifizieren kannst.
Der Code zur Integration lautet wie folgt:
<?php if ( is_active_sidebar( 'einzigartiger-widget-name' ) ) {
dynamic_sidebar( 'einzigartiger-widget-name' );
} ?>
Was hier genau passiert und wie es passiert, ist für Beginner im Bereich PHP-Programmierung nicht allzu wichtig. Das einzige was Du verstehen musst ist, dass Du „einzigartiger-widget-name“ gegen die „id“ tauschen musst, die Du bei der Registrierung Deines Widgets angegeben hast.
Im ersten Teil dieses Codes wird mit einer sogenannten „if – Anweisung“ überprüft, ob Dein Widget im WordPress Backend mit Inhalt gefüllt wurde. Diese „Abfrage“ wird mit Hilfe der WordPress Funktion „is_active_sidebar“ durchgeführt. Sollte WordPress nun als Ergebnis der Anfrage Antworten, dass der Widget-Bereich mit Inhalten wie Bildern gefüllt wurde, dann wird der zweite Teil des Codes aufgerufen.
Durch die zweite WordPress Funktion „dynamic_sidebar“ wird automatisch Dein Widget im Quellcode eingefügt und der Inhalt den Du im WordPress Backend hinterlegt hast eingefügt.
Wie findest Du aber nun die Stelle an der Du diesen Code einfügen musst?
Dazu gibt es mehrere Herangehensweisen. Die erste grobe Variante ist, dass Du in Deinem Child-Theme die gewünschte Datei des Parent-Themes überschreibst und den Code dort einbaust. Diese Methode setzt aber voraus, dass Du die gesamte Datei selber nachbaust.
Die schönere Variante ist es, wenn Du Deinen Code über Aktionen oder Filter integrierst. Dazu Informierst Du dich am besten darüber, ob Dein Parent-Theme passende Aktionen oder Filter anbietet, an denen Du eigene Inhalte einbauen kannst. Werfe dazu am besten einen Blick in die Dokumentation des Themes, oder kontaktiere den Hersteller des Themes. Dieser sollte Dir ein Feedback geben können, ob es passende Aktionen oder Filter an den Orten gibt, an denen Du Dein Widget integrieren möchtest. Solltest Du dort nicht fündig werden, dann kannst Du noch einen Blick auf die Aktionen und Filter werfen, die Dir WordPress von Haus aus liefert.
Leider bietet die WordPress Funktion „dynamic_sidebar“ nicht die Möglichkeit einfach den Inhalt des Widget Bereiches zu generieren und zu ermitteln. Der Inhalt wird dort ausgegeben, wo die Funktion aufgerufen wird. Um nun aber an den Inhalt des Widgets innerhalb eines Filters zu gelangen, musst Du zwei weitere kurze Code Schnippsel in Deiner „functions.php“ hinterlegen. Diese baust Du einfach unterhalb des Codes zur Registrierung des Widget-Bereiches ein.
Im ersten Code Schnippsel baust Du das Widget selber im sogenannten Ausgabe-Buffer zusammen und ermittelst so den Inhalt.
function themename_get_sidebar_string( $name_or_id )
{
ob_start();
$sidebar_return_value = '';
try {
$build_successful = dynamic_sidebar( $name_or_id);
if ( $build_successful ) {
$sidebar_return_value = ob_get_contents();
}
} catch (Exception $e) {
$sidebar_return_value = 'Es ist ein Fehler aufgetreten: '.$e->getMessage();
} finally {
ob_end_clean();
}
return $sidebar_return_value;
}
Der zweite Code Schnippsel zeigt Dir, wie Du anhand des WordPress Filters „the_content“ ein Widget am Ende des Inhaltsbereiches einfügen kannst. Dies ist lediglich ein vereinfachtes Beispiel zu Integration. Du solltest Dir genau überlegen auf welchen Seiten Du das Widget an den Content anhängen möchtest, solltest Du diesen Code Schnippsel verwenden.
In diesem Beispiel würde das Widget bei jedem Aufruf der Funktion „the_content“ integriert werden.
function themename_add_widget_after_content($content) {
$after_content = themename_get_sidebar_string('einzigartiger-widget-name');
$full_content_with_widget = $content . $after_content;
return $full_content_with_widget;
}
add_filter('the_content', 'themename_add_widget_after_content');
Du solltest auch in diesen Beispielen wieder „themename“ gegen den Name Deines Themes austauschen.
Fazit zum Wordpress Widget Bereich
In diesem Artikel hast Du gelernt, wie Du Deinen eigenen Widget-Bereich in Deinem Child-Theme integrierst und registrierst. Danach habe ich Dir gezeigt, wie Du innerhalb der Code-Vorlage Anpassungen vornehmen kannst, so dass das Widget zu Deinen Anforderungen passt. Im letzten Teil des Artikels hast Du gelernt, mit welchem Code Du das registrierte Widget im Child-Theme anzeigen kannst. Du kannst nun entscheiden, ob Du komplette Komponenten des Parent-Themes neu entwickelst, oder ob Du Dir eine Aktion oder einen Filter suchst, um Deine Widget zu integrieren.
Wie immer gilt, solltest Du Fragen, Anregungen oder Kritik zum Artikel haben, lass es mich über Deinen Kommentar wissen.
Beste Grüße,
Lukas
Task Queue & CSV Import mit Laravel 5.5 – so funktioniert's
Task oder Job Queues sind ein bewährtes Mittel um komplexe Aufgaben asynchron auszuführen und den Benutzer einer Anwendung nicht unnötig vor einer ladenden Website oder „eingefrorenen“ Software warten zu lassen. Ein weiterer, nicht zu unterschätzender Vorteil ist Auslagerung möglichererweise rechenintensiver Operationen in andere Prozesse, die nichts mit dem Webserver zutun haben.
Das PHP Framework Laravel liefert in seinem Funktionsumfang ebenfalls eine Queue-Funktionalität mit und gibt seinen Nutzeren auf diesem Weg ein Werkzeug an die Hand, das Nutzungsverhalten von komplexeren Laravel-Projekten stark zu verbessern.
In diesem Artikel befasse ich mich mit der Herausforderung einen Importer für CSV-Dateien in Laravel zu integrieren. Die Besonderheit ist, dass diese CSV Datein ungf. 50000 – 100000 Datensätze beinhalten können und der „Echtzeit-Import“ somit nicht möglich ist, schließlich kann der Benutzer der Anwendung nicht 30-60 Minuten warten, bis die Website wieder reagiert.
Für das „Abarbeiten“ der CSV-Datensätze werde ich kein Plugin verwenden. Ich entwickle gemeinsam mit Dir eigene kleine Code–Schnippsel, um CSV-Dateien Datensatz für Datensatz auszulesen und die beinhalteten Daten in eine Datenbank zu speichern.
Was ist eine Task Queue?
Eine Task Queue ist im Grunde eine Liste an Aufgaben, die sequenziell abgearbeitet wird. Welche Aufgabe als nächstes bearbeitet wird hängt von unterschiedlichen Kriterien ab. Der einfachste Fall ist „first in, first out“, also „wer zuerst kommt, mahlt zuerst“. Die Aufgabe die sich zuerst an unserer Queue angemeldet hat, wird auch als erstes bearbeitet. Weiterhin ist es möglich die Aufgaben mit weiteren Parametern zu versehen, wie bspw. einer Priorität, oder einem Wert für die Zeit, die die Aufgabe schon in der Queue verbringt, damit nicht nur Jobs einer höheren Priorität bearbeitet werden.
Queues die auf Prioritäten basieren haben aber eine andere Struktur als einfache Queues. Bei diesem Vorgehen gibt es für jede Prioriät eine eigene Queue. Verändert sich also die Priorität eines Jobs, wird er in eine andere Queue verschoben. Damit befassen ich mich aber in diesem Artikel noch nicht.
Betrachten wir nun erst mal die typischen Rollen, die in einer Queue agieren. Typische Rollen in einem Queue System sind der „Dispatcher“, der die Aufgaben entgegennimmt und sie gegebenenfalls parametrisiert in die Queue einfügt, die Queue als solche, der Queue Worker, der immer die Aufgabe aus der Queue entnimmt, die als nächstes an der Reihe ist und „last but not least“ der Code, der die Aufgabe verarbeitet. In unserem Beispiel ist das der Import einer großen CSV-Datei.
Nachfolgend findest Du eine Grafik, die den Aufbau eines Queue Systems grob skizziert:
Durch diesen Ablauf wird gewährleistet, dass der Webserver durch die komplexen Operationen nicht überlastet wird. Jeder ausstehende Import wird der Reihe nach abgearbeitet.
In der folgenden Grafik habe ich noch mal grob skizziert, wie die Rollenvertreilung in einer priorisierten Queue aufgebaut ist.
Wie wird die Task Queue in Laravel integriert?
Laravel bietet zu diesem Thema, wie auch zu vielen anderen Themen, eine sehr gute Dokumentation unter: Laravel 5.5 - Queues. Beginnen wir erst mal mit der Konfiguration. Im Verzeichnis „config“ Deiner Laravel-Installation findest Du die Queue Konfiguration „queue.php“. In dieser Datei Konfigurierst Du die Queue-Connection. Laravel bietet die Möglichkeit, Deine Queues in unterschiedlichen Systemen zu hinterlegen, wie unteranderem einer Datenbank, Amazons SQS, oder auch Redis. Wie Du Paypal in Laravel integrierst, erfährst Du hier.
In diesem Beitrag entscheide ich mich für den Connection-Typ „database“. Du kannst auch gerne eine andere Variante wählen. Ich gehe im Zuge dieses Beitrags aber lediglich auf die Variante Datenbank ein, da Performance in diesem Fall kein Kriterium ist.
Um nun die Datenbankmigration vorzubereiten, die Dir die benötigten Tabellenstrukturen für die Verwaltung der Jobs bereitstellt, verwendest Du Laravels CLI artisan.
$ php artisan queue:table
Die erstellte Migration integrierst Du über den Befehl „migrate“.
$ php artisan migrate
Die Integration der Job Klasse ist ebenfalls sehr simpel. Du kannst über den „make:job“ Befehl ein Template erzeugen lassen.
$ php artisan make:job ImportCsvFile
Das erzeugte Template sieht nun in etwas wie folgt aus:
<?php
namespace AppJobs;
use IlluminateBusQueueable;
use IlluminateQueueSerializesModels;
use IlluminateQueueInteractsWithQueue;
use IlluminateContractsQueueShouldQueue;
use IlluminateFoundationBusDispatchable;
class ImportCsvFile implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $csvFilePath;
/**
* Create a new job instance.
*
* @param string $csvFilePath
* @return void
*/
public function __construct($csvFilePath)
{
$this->csvFilePath = $csvFilePath;
}
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
// Importieren des CSV Datei
}
}
Jobs verfügen über die Implementierung gegen das Interface ShouldQueue über die statische Methode dispatch. Diese ermöglicht es, den Job an die Queue anzuhängen. Als Parameter erwartet die Methode den CSV Pfad der Datei.
Integration des CSV-Import Controllers
Um das Beispiel etwas greifbarer zu machen zeige ich Dir nun noch, wie Du einen passenden Controller bereitstellst, der den Dispatcher später ausführt. Dafür erstellst Du den Controller „CsvImportController“ über artisan.
$ php artisan make:controller CsvImportController
Im folgen Beispiel führe ich drei Wege an, über die Du den dispatcher ansteuern kannst.
- Direkt
- Verzögert
- Mit anderen Jobs verkettet
<?php
namespace AppHttpControllers;
use AppJobsImportCsvFile;
use IlluminateHttpRequest;
use IlluminateSupportFacadesStorage;
class CsvImportController extends Controller
{
public function index () {
return view('import.csvfile');
}
public function store (Request $request) {
// CSV Datei hochladen und temporär speichern
$csvFilePath = Storage::putFile('uploads', $request->file('csvfile'));
// Import dispatchen
ImportCsvFile::dispatch($csvFilePath);
/* ### Weitere dispatch Optionen ## */
// Mit Verzögerung
// -------------------------------------
// ImportCsvFile::dispatch($csvFilePath)
// ->delay(now()->addMinutes(10));
// Job Verkettung
// -------------------------------------
// ImportCsvFile::withChain([
// new ValidateCsvFile,
// new ImportCsvFileToDatabase
// ])->dispatch();
}
}
Integration der CSV-Import View und Route
Die View (csvfile.blade.php) ist in diesem Beispiel minimalistisch dargestellt. Sie enthält lediglich ein Formular um die Datei hochzuladen. Als action ist die „named route“ → „csvimport.store“ hinterlegt, die Du im nachfolgenden Schritt integrierst.
<h2>CSV Importieren</h2>
<form action="{{ route('csvimport.store') }}" method="post" enctype="multipart/form-data">
{{ csrf_field() }}
<label for="csvfile">Bitte wählen Sie eine CSV Datei aus</label>
<input type="file" name="csvfile" id="csvfile"> <br />
<button type="submit">Datei importieren</button>
</form>
Die Routen trägst Du in der „web.php“ unter dem Verzeichnis „routes“ ein.
Route::get('/csvimport', 'CsvImportController@index')->name('csvimport.index');
Route::post('/csvimport/store', 'CsvImportController@store')->name('csvimport.store');
Ein Blick auf die CSV Datei und ihr Model
Die CSV Datei in diesem Beispiel ist ebenfalls minimalistisch aufgebaut. Ich habe mich für eine simple „Sales“ CSV Datei (Demo: "task_queue_sales.csv") entschieden, die über die Spalten „day“ und „revenue“ verfügt. Zunächst erstellst Du für das Beispiel das Model „Sales“ und die zugehörige Migrationsdatei über folgenden Befehl.
$ php artisan make:model Sales -m
Im erzeugten Model hinterlegst Du nun noch die beiden Properties „table“ und „fillable“.
<?php
namespace App;
use IlluminateDatabaseEloquentModel;
class Sales extends Model
{
protected $table = 'sales';
protected $fillable = ['day', 'revenue'];
}
Nun erzeugst Du noch eine Migration um die Spalten „day“ und „revenue“ zum Sales Model hinzuzufügen.
$ php artisan make:migration add_properties_to_sales
Die Migrationsdatei passt Du nun folgendermaßen an.
<?php
use IlluminateSupportFacadesSchema;
use IlluminateDatabaseMigrationsMigration;
class AddPropertiesToSales extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('sales', function($table) {
$table->date('day')->unique();
$table->float('revenue');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('sales', function($table) {
$table->dropColumn('day');
$table->dropColumn('revenue');
});
}
}
Nun gilt es die Migrationen anzulegen. Dazu verwendest Du den Befehl Migrate. Dazu ist es aber wichtig, dass Du die Datenbankverbindung in Laravel konfiguriert hast.
$ php artisan migrate
Nach einer erfolgreichen Migration sind die vorbereiteten Spalten in der Tabelle „sales“ hinterlegt. Wir können nun damit beginnen das Model in den CSV Importer zu integrieren. Dazu öffnest Du nun wieder die angelegte Job Klasse „ImportCsvFile“. In der „handle“ Methode hinterlegst Du nun den Code um die gespeicherte CSV Datei zeilenweise auszulesen und in die passenden Models anzulegen. Wichtig ist ebenfalls, dass Du den Namespace des Sales Models einträgst.
<?php
namespace AppJobs;
use AppSales;
use Exception;
use IlluminateBusQueueable;
use IlluminateQueueSerializesModels;
use IlluminateQueueInteractsWithQueue;
use IlluminateContractsQueueShouldQueue;
use IlluminateFoundationBusDispatchable;
use IlluminateSupportFacadesStorage;
class ImportCsvFile implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $csvFilePath;
/**
* Create a new job instance.
*
* @param string $csvFilePath
* @return void
*/
public function __construct($csvFilePath)
{
$this->csvFilePath = $csvFilePath;
}
/**
* Execute the job.
*
* @throws Exception
* @return void
*/
public function handle()
{
if (!Storage::disk('local')->exists($this->csvFilePath))
throw new Exception(sprintf(
'Die CSV Datei konnte nicht unter dem angegebenen Dateipfad gefunden werden. Dateipfad: "%1$s"'
, $this->csvFilePath
));
// Importieren der CSV Datei
$fileData = Storage::get($this->csvFilePath);
$csvData = str_getcsv($fileData, "n");
foreach ($csvData as &$row) {
$row = str_getcsv($row, ",");
if(is_array($row) && sizeof($row) == 2) {
$day = date('Y-m-d', strtotime($row[0]));
Sales::create([
'day' => $day,
'revenue' => $row[1]
]);
}
}
}
}
Der in der „handle“ Methode integrierte Code ist sehr simpel und um alles kurz und knapp zu halten, nicht refaktorisiert. Diverse Blöcke lassen sich wunderbar auslagern. Desweiteren sollten an mehreren Stellen Validierungen integriert werden.
Im ersten Abschnitt der Methode validiere Ich lediglich kurz, ob die Datei im „storage“ Ordner hinterlegt wurde. Ist dass der Fall, dann wird deren Inhalt geladen und mit der PHP Methode „str_getcsv()“ als Array geparsed.
Im letzten Abschnitt werden alle Zeilen der CSV Datei durchlaufen und die Daten jeder Zeile als Sale in der Datenbank gespeichert.
Queue Listener starten
Damit die Queue nun auch abgearbeitet wird, muss der Queue Listener gestartet werden.
$ php artisan queue:listen
Im Grunde stützt sich die Abarbeitung der Queue auf diesen CLI Prozess. Sollte die Queue auf einem Server eingerichtet werden und auch nach einem Fehler oder Serverneustart wieder erreichbar sein, so lohnt es sich für Dich einen Blick auf das Programm Supervisor zu werfen. Dieses läuft unter Linux Betriebssystemen und kann schnell installiert und konfiguriert werden. Laravel bietet in der Dokumentation ein Kapiel zu Supervisor an Laravel 5.5 - Supervisor configuration.
Fazit
In diesem Blogeintrag hast Du gelernt, wie Du Deine eigene Queue gestalten und entwickeln kannst. Du hast zu Beginn des Artikels die Queue in Deinem Laravel Projekt eingerichtet und hast Deinen eigenen Queue Job erstellt. Gegen ende des Artikels hast Du noch erfahren, wie Du aus dem Queue Job heraus eine CSV Datei in Deine Datenbank importieren kannst.
Solltest Du Fragen oder Anmerkungen haben, zögere nicht einen Kommentar zu hinterlassen.
Unit Testing in Laravel 5.5 – Unit Tests mit PHPUnit
Unit Testing ist wichtig und das scheint Dir schon bewusst zu sein, da Du es bis zu diesem Artikel geschafft hast. Die Gründe Unit Tests in Softwareprojekte zu integrieren wurden schon oft erläutert und sind Dir vormutlich auch bereits bekannt. In diesem Artikel werde ich Dir zeigen, wie Du Unit Tests mit dem PHP Test-Framework PHPUnit 6.4 in dem PHP Framwork Laravel 5.5 integrierst. Du wirst in Deinem Laravel Projekt über Artsian Unit Tests anlegen und diese mit Test-Logik befüllen.
Danach zeige ich Dir noch, wie Du automatisch ansehliche Code Coverage Reports für Dein Laravel Projekt generieren kannst, und dass alles mit Hilfe von PHPUnit. So hast Du immer die Testabdeckung Deines PHP Codes im Auge. Zum Schluss ergänze ich noch einige Fehler, die auftreten können, wo deren Ursprung oft nicht leicht zu ermitteln ist.
Einführung in das Unit Testing in Laravel
Laravel liefert von Haus aus Unit Testing und Integrationstest (Feature Tests) Methoden aus. Das starke PHP Test Framework PHPUnit ist fest in Laravel integriert und wird in der aktuellen Laravel Version 5.5 mit der Version 6.4 ausgeliefert. PHPUnit 6.4 unterstützt PHP 7.0 und PHP 7.1.
PHPUnit wird über den PHP Abhängigkeitsmanager Composer ausgeliefert und liegt nach dem „install“ Befehl im Verzeichnis „vendor/bin/“ (phpunit.php).
Ebenfalls mit an Board ist eine von Laravel bereitgestellte PHPUnit Konfigurationsdatei „phpunit.xml“. Diese liegt im Basisverzeichnis Deines Projektes. Über die „phpunit.xml“ kannst Du „environment“ Variablen überschreiben, „Testsuites“ für unterschiedliche Test-Kategorien anlegen und vieles mehr. Hier wirst Du später auch den automatischen PHP Code Coverage Report konfigurieren.
Laravel stellt in der „phpunit.xml“ Konfiguration bereits Test-Suites für Unit Tests und Integrationstests (Feature Tests) bereit. In Deinem Laravel-Projekt findest Du im Verzeichnis „tests“ bereits die beiden Test-Suite Verzeichnisse „Unit“ und „Feature“ in denen Du später die jeweiligen Tests hinterlegen kannst.
Im „tests“ Verzeichnis sind neben den Test-Suites auch die zwei Klassen „TestCase.php“ und „CreatesApplication.php“ hinterlegt. Diese „booten“ für Dich während der Tests die Laravel-Anwendung. Näheres dazu aber in den folgenden Abschnitten.
Laravel Testumgebung
Wenn Du Deine Tests mit PHPUnit ausführst, läuft die Laravel-Anwendung durch die überschreibenden Environment Variablen der „phpunit.xml“ im „Test-Modus“. Innerhalb dieses „Test-Modus“ ändert Laravel automatisch den Cache Treiber in den Modus „array“. Der Treiber dient zur Cache und Session-Verwaltung. Den Treiber kannst Du auch selber anpassen. Du findest ihn im Verzeichnis „config“ unter dem Name „cache.php“. Der „array“ Modus sorgt dafür, dass der Cache und die Sessions nicht persistent gespeichert werden (d.h. in der Datenbank oder im Dateisystem).
Du kannst die Konfiguration nach Deinen Wünschen anpassen und um weitere Einstellungen erweitern. Wichtig ist nur, dass Du nach vorgenommenen Änderungen den Laravel Konfig-Cache leerst. Dies kannst Du mit Artisan über den Befehl „config:clear“ tun:
$ php artisan config:clear
Einen Unit Test in Laravel anlegen und Tests ausführen
Wie bei fast allem hilft Dir auch hier das Laravel Tool Artisan aus. Du kannst den „make“ Befehl mit dem Parameter „test“ ausführen und einen Namen für den Test angeben. Wie das in der Konsole aussieht siehst Du anhand des folgendne Beispiels:
$ php artisan make:test FooBarTest --unit
Über den Zusatz „--unit“ wird ein Unit Test angelegt, lässt man ihn weg erhält man einen Integrationstest / Feature Test. Der Test wurde im jeweiligen Ordner angelegt. In diesem Beispiel wurde der Test im Unit Test Ordner abgelegt.
Wenn Du den Test öffnest kannst Du mit Hilfe aller PHPUnit Funktionen Unit Tests erstellen um Deine Code Units zu testen. Hier siehst Du noch mal den angelegten Unit Test:
<?php
namespace TestsUnit;
use TestsTestCase;
use IlluminateFoundationTestingRefreshDatabase;
class FooBarTest extends TestCase
{
/**
* A basic test example.
*
* @return void
*/
public function testBasicTest()
{
$this->assertTrue(true);
}
}
Solltest Du Deine eigene „setUp“ Funktion zum vorbereiten Deiner Tests erstellen, dann musst Du daran denken, die Funktion „parent:setUp“ mit aufzurufen, damit die Tests funktionieren.
Um nun Deinen frisch angelegten Unit Tests auszuführen, musst Du PHPUnit ausführen. Dazu gibt es mehrere Wege. Im Falle, dass Du PHPUnit nicht installiert hast, kannst Du die von Laravel mitgelieferte PHPUnit Datei „vendor/bin/phpunit.php“ über den folgenden Befehl in Deinem Laravel-Projektverzeichnis nutzen:
$ php ./vendor/bin/phpunit
Die zweite Möglichkeit ist, dass Du Dir PHPUnit in der benötigten Version 6.4.3 installierst und PHPUnit dann einfach über den folgenden Befehl in Deinem Laravel-Projektverzeichnis ausführst:
$ phpunit
Danach solltest Du ein ähnliches Ergebnis wie folgendes erhalten:
$ phpunit PHPUnit 6.4.3 by Sebastian Bergmann and contributors. .. 2 / 2 (100%) Time: 348 ms, Memory: 20.00MB OK (2 tests, 2 assertions)
Komplettes Beispiel für Unit Tests in Laravel
Um das Ganze für Dich noch ein Stück greifbarer zu gestalten, werde ich Dir anhand von „Produkten“ zeigen, wie Du Deine Unit Tests aufbauen kannst. Du wirst in diesem Abschnitt ein Produkt Model und eine zugehörige Migration anlegen und einen passenden Controller dafür genierieren. Im Controller wirst Du passente CRUD (Create, Read, Update, Delete) Operationen, sowie die „Kaufen“ Funktion integrieren, die die Anzahl des gekauften Produktes reduziert.
Diese Funktionen wirst Du im Anschluss mit Unit Tests testen. Du wirst außerdem die „phpunit.xml“ um Environment Konfigurationen erweitern, damit nicht die „produktive“ Datenbank, sondern eine einfache SQLite Datenbank zum Testen verwendet wird. Wichtig ist, dass Du Dir SQLite3 und die PHP Extension zu SQLite3 installierst, sollte das nooch nicht der Fall sein. Unter Linux kannst Du dies mit den folgenden drei Befehlen tun:
$ sudo apt-get install sqlite3
Hier musst Du Deine installierte PHP Version kennen:
$ sudo apt-get install php7.0-sqlite3
Zum Schluss musst Du noch kurz den Apache Server neuladen:
$ sudo service apache2 reload
Du startest nun mit den Einträgen in der „phpunit.xml“. Ergänze die nachfolgenden Einträge in den „php“-Knoten:
<env name=“DB_CONNECTION“ value=“sqlite“ /> <env name=“DB_DATABASE“ value=“:memory:“ />
Als nächstes generierst Du mit Hilfe von Artisan das Produkt Model und die zugehörige Datenbank Migrationsdatei. Der Konsolenbefehl dazu lautet wie folgt:
$ php artisan make:model Product -m
Durch den Parameter „-m“ wird die Migration angelegt. Das Produkt wird sehr einfach aufgebaut sein, um die Komplexität in diesem Artikel gering zu halten. Du ergänzt gleich in der Migration die Spalten: Titel (string), Preis (decimal) und Anzahl (integer). Die erstellte Migration findest Du im Verzeichnis „database/migrations/“ und sie sollte einen ähnlichen Dateinamen wie folgenden tragen „2017_10_31_111518_create_products_table.php“. Mit den Spalten sollte Deine Migrationsdatei wie folgt aussehen:
<?php
use IlluminateSupportFacadesSchema;
use IlluminateDatabaseSchemaBlueprint;
use IlluminateDatabaseMigrationsMigration;
class CreateProductsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('products', function (Blueprint $table) {
$table->increments('id');
$table->timestamps();
$table->string('title');
$table->decimal('price', 6, 2);
$table->integer('stock');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('products');
}
}
Nach dem Du die Migrationsdatei befüllt hast, solltest Du nun die „fillable“ Eigenschaften im Model hinterlegen. Dazu öffnest Du das Model „Product.php“ im Verzeichnis „app“ und fügst folgende Variable ein:
<?php
namespace App;
use IlluminateDatabaseEloquentModel;
class Product extends Model
{
protected $fillable = ['title', 'price', 'stock'];
}
In den Unit Testings musst Du später die Produkte „mocken“ oder „faken“. Dafür bringt Laravel eine „Faker“ Library mit. Diese Library benötigt eine „Factory“ zu dem Model Produkt, damit sie die richtigen Inhalte anlegen kann. Eine Factory für das Produkt legst Du über folgen Befehl an:
$ php artisan make:factory ProductFactory --model=Product
Artisan hat nun für Dich die Factory „ProductFactory.php“ unter „database/factories“ erstellt. Diese öffnest Du nun und fügst folgende „return“ Anweisung hinzu:
<?php
use FakerGenerator as Faker;
/* @var IlluminateDatabaseEloquentFactory $factory */
$factory->define(AppProduct::class, function (Faker $faker) {
return [
'title' => $faker->text(20),
'price' => $faker->randomFloat(2),
'stock' => $faker->randomNumber()
];
});
Jetzt bist Du soweit, dass Du den Unit Test anlegen kannst. Wie im ersten Teil dieses Beitrages, hilft Dir nun wieder Artisan:
$ php artisan make:test ProductTest --unit
Im Unit Test „ProductTest.php“ fügst Du nun die beiden Namespaces „AppProduct“ und „IlluminateFoundationTestingDatabaseMigrations“ hinzu und ergänzt zu Beginn der Klasse „use DatabaseMigrations;“ um die Datenbank Migrationen durchzuführen. Dadurch kannst Du nun in der SQLite Datenbank die in Deinem Hauptspeicher läuft Datenbankoperationen durchführen und so tun, als würdest Du auf der produktiven Datenbank arbeiten.
Danach legst Du den Unit Test „test_product_can_be_created“ als Funktion an und „fakest“ mit der zuvor erstellten Factory ein Produkt, dass direkt im Anschluss per „create“ in der SQLite Datenbank gespeichert wird.
Zu guter Letzt prüfst Du per „assertDatabaseHas“, ob das erstellte Produkt auch in der Datenbank gespeichert wurde.
Deine Unit Testing Sammlung sollte nun wie folgt aussehen:
<?php
namespace TestsUnit;
use TestsTestCase;
use IlluminateFoundationTestingRefreshDatabase;
use AppProduct;
use IlluminateFoundationTestingDatabaseMigrations;
class ProductTest extends TestCase
{
use DatabaseMigrations;
/** @test */
public function test_product_can_be_created()
{
$product = factory(Product::class)->create();
$this->assertDatabaseHas('products', [
'id' => 1,
'title' => $product->title,
'price' => $product->price,
'stock' => $product->stock
]);
}
}
Den Unit Test führt Du nun mit $ phpunit aus, wie ich es Dir zu Begin des Artikels gezeigt habe.
Sollten hier nun Fehler auftreten, wie bspw. „IlluminateDatabaseQueryException: could not find driver (SQL: PRAGMA writable_schema = 1;)“, dann hast Du vermutlich vergessen SQLite3 und/oder die PHP Extension zu SQLite3 zu installieren. Weiter oben findest Du drei Befehle zur installation unter Linux. Weitere vorkompilierte Binaries findest Du unter „SQLite - Download“.
Um nun noch den Fall zu testen, dass auch Produkte gekauft werden können, legst Du den passenden Produkt Controller über Artisan an:
$ php artisan make:controller ProductController
Zuerst fügst Du im im Controller wieder den Namespace „AppProduct“ hinzu und im Anschluss erstellst Du die Funktion „buy_product“ mit den Parametern „id“ und „quantity“:
<?php
namespace AppHttpControllers;
use IlluminateHttpRequest;
use AppProduct;
class ProductController extends Controller
{
public function buy_product($id, $quantity) {
$product = Product::findOrFail($id);
if($quantity <= 0) {
return response()->json( [
'error' => true,
'message' => 'Es muss mindestens ein Produkt bestellt werden.'
], 403 );
} else if ($quantity > $product->stock) {
return response()->json( [
'error' => true,
'message' => 'Es sind leider nicht genügend Produkte auf Lager um diese Bestellung durchzuführen.'
], 403 );
}
$product->stock -= $quantity;
$product->save();
return response()->json( [
'error' => false,
'message' => 'Produkte erfolgreich gekauft.'
], 200 );
}
}
Damit Du die Funktion „buy_product“ aufrufen kannst, musst Du die Route konfigurieren. Öffne dazu die Datei „web.php“ im Verzeichnis „routes“. Dort ergänzt Du folgende Route:
Route::get('/products/{id}/buy/{quantity}', 'ProductController@buy_product');
Integriert wird hier eine Route die mit „/products/“ beginnt, danach muss eine Produkt ID übergeben werden. Im Anschluss folgt die Route „/buy/“ und die Anzahl der Produkte die gekauft werden sollen. Wird diese Route über HTTP Get aufgerufen wird die Funktion „buy_product“ im ProductController ausgeführt und die in der URL übergebenen Parameter an die Funktion übergeben.
Jetzt kannst Du für die folgenden Scenarien Test Funktionen anlegen, um den kompletten Kaufprozess und alle Möglichen Code Pfade zu Prüfen:
- Das Produkt kann gekauft werden und der Warenbestand verringert sich
- Das Produkt in der URL existiert nicht
- Bei einem leeren Warenbestand kann das Produkt nicht gekauft werden
- Es wurde keine Anzahl in der URL übergeben, obwohl mindestens ein Produkt gekauft werden muss
Diese drei Scenarien könnten wie folgt getestet werden:
/**
* @test
* Das Produkt kann gekauft werden und der Warenbestand verringert sich
*/
public function test_product_can_be_bought()
{
$product = factory(Product::class)->create(['stock' => 16]);
$response = $this->get('/products/1/buy/15');
$response->assertStatus(200);
$result = Product::find(1);
$this->assertEquals($result->stock, 1);
}
/**
* @test
* Das Produkt in der URL existiert nicht
*/
public function test_not_existing_product_cant_be_bought()
{
$product = factory(Product::class)->create();
$response = $this->get('/products/42/buy/15');
$response->assertStatus(404);
}
/**
* @test
* Bei einem leeren Warenbestand kann das Produkt nicht gekauft werden
*/
public function test_product_with_empty_stock_cant_be_bought()
{
$product = factory(Product::class)->create(['stock' => 0]);
$response = $this->get('/products/1/buy/1');
$response->assertStatus(403);
}
/**
* @test
* Es wurde keine Anzahl in der URL übergeben, obwohl mindestens ein Produkt gekauft werden muss
*/
public function test_product_cant_be_bought_without_quantity()
{
$product = factory(Product::class)->create(['stock' => 15]);
$response = $this->get('/products/1/buy/0');
$response->assertStatus(403);
}
Nun hast Du über Unit Tests das Produkt Model und den Produkt Controller erfolgreich getestet und alle Code Pfade im Controller wurden mit den Tests „abgelaufen“. Als nächstes zeige ich Dir, wie Du einen Code Coverage Report zu Deinen PHPUnit Unit Tests im Laravel Projekt automatisch erstellen lassen kannst.
PHPUnit Code Coverage Report für Unit Testing in Laravel
Die Gründe Code Coverage Reports zu erzeugen sind vielfältig. Sie können für sich betrachtet werden um manuell zu prüfen, wie gut die Tesstabdeckung des eigenen Projektes ist. Ein weiterer Grund könnte die Integration in einen Build-Prozess darstellen.
PHPUnit liefert von Haus aus die Möglichkeit, Code Coverage Reports zu erzeugen. Das wird, wie alles andere auch, in der „phpunit.xml“ konfiguriert. Füge dazu am Ende der „phpunit.xml“ einfach folgenden Knoten an:
<logging>
<log type="coverage-html" target="./report_code-coverage" charset="UTF-8"
yui="true" highlight="true"
lowUpperBound="50" highLowerBound="80" />
</logging>
Durch den „log“ Eintrag im „logging“ Knoten wird nach Abschluss der Tests eine HTML Struktur im Verzeichnis „/report_code-coverage“ erzeugt. Die Grenzen für schlecht und gut getesten Code lassen sich mit „lowUpperBound“ und „highLowerBound“ festlegen. Eine mehr Infos findest Du in der offiziellen Dokumentation von PHPUnit. Informationen zum Logging findest Du im Unterpunkt „Logging“ unter „PHPUnit - Configuration“
Das Ergebnis Deines Produkt Controllers nach dem Ausführen der Tests sieht folgendermaßen aus:
Du hast nun die grundlegenden Werkzeuge für das Unit Testing in Laravel 5.5 an der Hand. Du hast gelernt, wie Du Deine „phpunit.xml“ erweitern kannst, wie Du eine Testdatenbank mit SQLite3 einrichtest und wie Du Tests für Models und Controller erstellst. Zum Schluss hast Du noch die automatische Generierung von Code Coverage Reports aktiviert.
Mögliche Fehler beim Ausführen der PHPUnit Unit Tests in Laravel
Anbei folgt eine Liste möglicher Fehler die beim Unit Testing mit PHPUnit unter Laravel 5.5 auftreten können. Du kannst dabei helfen, diese Liste zu erweitern in dem Du von Deinen Fehlern berichtest.
Fehler 1 - Expected status code 200 but received 404
Beim Ausführen Deiner HTTP – Unit Tests (get / call) tritt folgender Fehler auf:
There was 1 failure: 1) TestsFeatureExampleTest::testBasicTest Expected status code 200 but received 404. Failed asserting that false is true.
Mögliche Lösung:
Du hast für Dein Laravel-Projekt einen „virtual host“ / „vhost“ Eintrag angelegt, damit Du Deine Website über eine „richte“ URL erreichen kannst: „http://mein-projekt.dev“.
In diesem Fall musst Du in der „phpunit.xml“ den folgenden Environment Eintrag ergänzen:
<env name="APP_URL" value="http://mein-projekt.dev"/>
Durch unsere langjährige Arbeit und über 100 erfolgreiche Projekte, konnten wir viele Erfahrungen sammeln. Dieses Know-How im Online-Marketing gaben wir u.a. bei Vorträgen von Google, der Industrie- und Handelskammer und der Handwerkskammer weiter.
Mit Know-How, Kreativität und Leidenschaft entwickeln wir auf unsere Kunden abgestimmte Marketing-Strategien, die Sie sicher und nachhaltig zum Erfolg führen. Gemeinsam setzen wir Ihr Online-Marketing so um, dass Sie langfristig Ihren Umsatz und Return-On-Investment steigern.
Jetzt kostenlosen Beratungstermin vereinbaren oder unter 0561 / 850 194 76 anrufen.
Wordpress Custom Post Type - Einen eigenen Post Type in Wordpress anlegen
Was ist ein Post Type und welche Möglichkeiten bietet er mir?
Von Wordpress hat mittlerweile jeder schon mal gehört und die meisten wissen auch, dass Wordpress als Blog System verwendet wird. Ein einfaches Blog System ist Wordpress aber schon lange nicht mehr. Seit der Software Version 1.5 („Strayhorn“) kann Wordpress zu mehr verwendet werden, als lediglich dazu, Blogeinträge anzulegen, es ist nun ein CMS (Content Management System). Durch eine strikte Datenbanknormalisierung (Wikipedia - Normalisierung (Datenbank)) wurde dafür gesorgt, dass Wordpress über sogenannte Post Types beliebig erweitert werden kann. Was ein Post Type ist und wie Du Deinen eigenen Custom Post Type anlegen kannst, dass erfährst Du in diesem Artikel.
Was sind nun diese Post Types? Post Types sind im Grunde die Spezifizierung von den Inhalten Deiner Wordpress Seite. Alle „Inhalte“ werden in der Datenbanktabelle „wp_posts“ gespeichert. Inhalte sind beispielsweise:
- Seiten (Post Type = „page“)
- Blogeinträge (Post Type = „post“)
- Medien wie: Bilder, Videos, etc. (Post Type = „attachment“)
Damit man diese Inhalte nun auseinanderhalten kann, gibt es in dieser Tabelle die Spalte „post_type“, die Aufschluss darüber gibt, was in dem jeweiligen Datensatz hinterlegt ist.
Dadurch kannst Du als Entwickler Deine eigenen Content-Typen / Post Types in Wordpress hinterlegen.
Beispiele für eigene Post Types:
- Produkte
- Leistungen
- Referenzen
- Profile
- Gegenstände / Objekte (Sofas, Tische, Häuser, Hardware)
- etc.
Wie Du sehen kannst, sind Post Types sehr flexibel einsetzbar. Darin liegt ihre Stärke. Zudem lassen sie sich über die Tabelle „wp_post_meta“ um zusätzliche Felder erweitern, da die Tabelle „wp_posts“ lediglich über folgende Felder verfügt:
ID, post_author, post_name, post_type, post_title, post_date, post_date_gmt, post_content, post_excerpt, post_status, comment_status, ping_status, post_password, post_parent, post_modified, post_modified_gmt, comment_count, menu_order
Die Tabelle „wp_post_meta“ ist Key – Value basiert aufgebaut und direkt mit einem Post verknüpft. So kann ein Produkt bspw. folgende Post Meta Felder besitzen:
- product_price
- product_inventory_count
- product_default_delivery_period
- etc.
Leider sind den Verwendungszwecken von Post Types und ihren Meta Feldern auch Grenzen gesetzt. Welche das sind, schauen wir uns im nächsten Absatz an.
Grenzen von Post Types
Durch Post Types können wir unser Wordpress Theme oder unser Wordpress Plugin leicht individualisieren, wie das geht erfährst Du übrigens im nächsten Schritt, aber Post Types treffen auch irgendwann auf ihre Grenzen. Eine dieser Grenzen ist die Geschwindigkeit. Durch die strikte Normalisierung der Post und Post Meta Tabellen müssen teilweise pro Seitenaufruf im Schnitt 20 Datenbankabfragen getätigt werden. Im ungünstigsten Fall bei hunderten Website Besuchern gleichzeitig.
Gerade die Erweiterung von Post Types über Post Meta Felder ist nicht besonders schön gelöst. Jedes Post Meta Feld ist ein eigener Datensatz, was schnell dazu führen kann, dass je nach Post Type, diese Tabelle enorm wächst und Redundanzen in den Datensätzen entstehen. Ein weiterer Nachteil ist, dass die Post Meta Werte nicht im Type spezifizierbar sind. Jeder Post Meta Wert ist vom Typ „longtext“, damit Entwickler jeden Wert in die Tabelle speichern können. Dies führt wiederum dazu, dass keine Constraints für die Post Meta Werte eingerichtet werden können.
Willst Du bspw. große Datenmengen wie Statistiken und Reports mit tausenden Datensätzen in Deiner Datenbank hintelegen und diese Datensätze dann einzelnen Monaten und Nutzern zuordnet, so lässt sich dies ebenfalls nicht mit Post Types abbilden. In diesem Fall lohnt es sich bereits eigene Tabellen anzulegen (Wordpress - Creating Tables with Plugins), oder Wordpress gegen ein anderes PHP Framework zu tauschen.
Weiterhin ist es möglich, komplexe „wp_query“ Ergebnisse in Wordpress Transients zu speichern, was leider wieder Datenbank Abfragen zur Folge hat. Ein alternative dazu ist die PHP Funktion Memcache (PHP - memcache). Mit Hilfe dieser lassen sich die „wp_query“ Ergebnisse im Zwischenspeicher ablegen.
In 4 Schritten zum eigenen Post Type
Eigene Post Types können in Wordpress an vielen Stellen benötigt werden. Sie lassen sich im eigenen Theme, im Child Theme oder im eigenen Plugin integrieren. Dabei gibt es keinen Unterschied darin, wie sie integriert werden. Solltest Du Dich für die Erstellung Deines eigenen Child Themes interessieren, so kann ich Dir unseren Artikel „Wie Du in 4 Schritten Dein eigenes WordPress Child Theme erstellst“ empfehlen.
Ich helfe Dir nun dabei in nur 4 Schritten Deinen eigenen Post Type zu registrien und gebe Dir im nächsten Kapitel noch Tipps mit auf den Weg, wie Du eigene Post Meta Boxen im Wordpress Backend erstellst.
Schritt 1)
Im ersten Schritt legst Du den Ordner „post_types“ in deinem Plugin oder Theme an. Danach erstellst Du in diesem Ordner eine PHP Datei mit dem Namen Deines Post Types, bspw. „product.php“. Diese Datei kannst Du nun in deinem Plugin oder Theme mit der PHP Funktion „require_once“ einbinden.
require_once( 'post_types/product.php' );
Schritt 2)
In diesem Schritt wirst Du deinen eigenen Post Type mit der Wordpress Funktion „register_post_type“ registrieren. Dies funktioniert aber nur während der Aktion „init“. Wird „register_post_type“ vorher aufgerufen, wird die Registrierung fehlschlagen.
Registriere nun zuerst eine Funktion auf die „init“ Aktion in Deiner Post Type PHP Datei:
<?php
/**
* Registriert den Post Type „product“
*/
function register_product_post_type() {
// Hier wird der Post Type registriert
}
add_action( 'init', 'register_product_post_type' );
Tipp: Versuche für Deinen Post Type einen individuellen Namen zu finden, da simple Namen für Post Types schnell von anderen Plugins oder Themes verwendet werden können. Du kannst bspw. den Namen Deines Themes oder Plugins dem Post Type voranstellen: „example_plugin_product“
Dadurch vermeidest Du von vornherein Dopplungen von Post Types und minimierst die Fehleranfälligkeit Deines Codes.
Kommen wir nun zur Registrierung. „register_post_type“ nimmt zwei Parameter entgegen. Als ersten Parameter erwartet die Funktion den Namen des Post Types und als zweiten Parameter ein Array mit Argumenten. Du kannst nun folgende Vorlage zur Vorbereitung der Registrierung innerhalb der zuvor implementierten Funktion integrieren:
Teil 1 – Die Texte vorbereiten:
$labels = array(
'name' => __( 'Produkte', TRANSLATION_CONST ),
'singular_name' => __( 'Produkt', TRANSLATION_CONST ),
'add_new' => __( 'Hinzufügen', TRANSLATION_CONST ),
'add_new_item' => __( 'Produkt hinzufügen', TRANSLATION_CONST ),
'edit_item' => __( 'Produkt bearbeiten', TRANSLATION_CONST ),
'new_item' => __( 'Produkt hinzufügen', TRANSLATION_CONST ),
'view_item' => __( 'Produkt anzeigen', TRANSLATION_CONST ),
'view_items' => __( 'Produkte anzeigen', TRANSLATION_CONST ),
'search_items' => __( 'Produkt suchen', TRANSLATION_CONST ),
'not_found' => __( 'Keine Produkte gefunden', TRANSLATION_CONST ),
'not_found_in_trash' => __( 'Keine Produkte im Papierkorb gefunden', TRANSLATION_CONST ),
'parent_item_colon' => __( 'Übergeordnete Produkte:', TRANSLATION_CONST ),
'all_items' => __( 'Alle Produkte:', TRANSLATION_CONST ),
'archives' => __( 'Produkt Archiv:', TRANSLATION_CONST ),
'attributes' => __( 'Produkt Attribute:', TRANSLATION_CONST ),
'insert_into_item' => __( 'Zum Produkt hinzufügen', TRANSLATION_CONST ),
'uploaded_to_this_item' => __( 'Zum Produkt hinzugefügt', TRANSLATION_CONST ),
'featured_image' => __( 'Produktbild', TRANSLATION_CONST ),
'set_featured_image' => __( 'Produktbild setzen:', TRANSLATION_CONST ),
'remove_featured_image' => __( 'Produktbild entfernen:', TRANSLATION_CONST ),
'use_featured_image' => __( 'Als Produktbild verwenden:', TRANSLATION_CONST ),
'menu_name' => __( 'Produkte', TRANSLATION_CONST ),
);
In diesem Array sind nun alle wichtigen Texte für Deinen Post Type hinterlegt. Weitere Informationen dazu findest Du unter WordPress - register post type. In diesem Beispiel und in einigen folgenden Beispielen wird auch die PHP Konstante „TRANSLATION_CONST“ verwendet, die die gewünschte Text Domain (WordPress - I18n) enthält.
Teil 2 – Funktionen und Felder:
Kommen wir nun zu den Funktionen, die Dein Post Type unterstützen soll:
$supports = array(
'title',
'editor', // Content Bereich
'excerpt', // Kurzer Auszug des Contents für Archivseiten
'author',
'thumbnail', // featured_image / Produktbild
//'trackbacks',
'custom-fields',
//'revisions',
'page-attributes',
'comments'
);
Hiermit kannst Du einzelne Funktionen und Post Felder, die Dir Wordpress bereitstellt aktivieren und deaktivieren. In dem Beispiel sind „trackbacks“ und „revisions“ auskommentiert, da wir sie für das Produkt nicht benötigen. Solltest Du bspw. „thumbnail“ auskommentieren, so besitzt das Produkt keine Möglichkeit ein Produktbild zu hinterlegen.
Teil 3 – Argumente zusammenstellen:
Zu guter Letzt stellen wir noch die Argumente zusammen:
$args = array(
'labels' => $labels,
'hierarchical' => false,
'description' => __( 'Produkte für mein eigenes E-Commerce Plugin', TRANSLATION_CONST ),
'supports' => $supports,
'public' => true,
'show_ui' => true,
'show_in_menu' => true,
'menu_position' => 5, // Unterhalb der Posts
'menu_icon' => 'dashicons-card',
'show_in_nav_menus' => true,
'publicly_queryable' => true,
'exclude_from_search' => false,
'has_archive' => true,
'query_var' => true,
'can_export' => true,
'capability_type' => 'post'
);
Die Argumente sind sehr umfangreich. Eine vollständige Liste der Funktionsdefinitionen von Wordpress findest Du unter WordPress - register post type, deshalb beschreibe ich folgend nur einige der Argumente. In dieses Array von Argumenten fügen wir unter „label“ und unter „supports“ unsere zuvor definierten Arrays ein. Unter dem Punkt „hierarchical“ kannst Du festlegen, ob das Produkt auch ein übergeordnetes Produkt hat. Diese Funktion lässt sich aber nur nutzen, wenn unter „supports“ auch der Punkt „page-attributes“ aktiviert wurde.
Das Argument „capability_type“ kann in Kombination mit dem Argument „capabilities“ dazu verwendet werden, die Rechteverwaltung der Nutzer genauer zu spezifizieren. In unserm Beispiel werden die selben Rechte wie für den Post Type „post“ verwendet. Eine Auflistung der Rollen und der Zuordnung zu den Capabilities findest Du unter WordPress - Roles and Capabilities.
Über das Argument „menu_icon“ kannst Du ein Menü Icon für das Wordpress Backend festlegen. Eine Übersicht aller verfügbaren Icons findest Du unter WordPress - Dashicons.
Teil 4 – Post Type registrieren:
Nun musst Du mit Hilfe der Argumente nur noch die passende Wordpress Funktion „register_post_type“ aufrufen:
<?php
/**
* Registriert den Post Type „product“
*/
function register_product_post_type() {
$labels = array(
'name' => __( 'Produkte', TRANSLATION_CONST ),
'singular_name' => __( 'Produkt', TRANSLATION_CONST ),
'add_new' => __( 'Hinzufügen', TRANSLATION_CONST ),
'add_new_item' => __( 'Produkt hinzufügen', TRANSLATION_CONST ),
'edit_item' => __( 'Produkt bearbeiten', TRANSLATION_CONST ),
'new_item' => __( 'Produkt hinzufügen', TRANSLATION_CONST ),
'view_item' => __( 'Produkt anzeigen', TRANSLATION_CONST ),
'view_items' => __( 'Produkte anzeigen', TRANSLATION_CONST ),
'search_items' => __( 'Produkt suchen', TRANSLATION_CONST ),
'not_found' => __( 'Keine Produkte gefunden', TRANSLATION_CONST ),
'not_found_in_trash' => __( 'Keine Produkte im Papierkorb gefunden', TRANSLATION_CONST ),
'parent_item_colon' => __( 'Übergeordnete Produkte:', TRANSLATION_CONST ),
'all_items' => __( 'Alle Produkte:', TRANSLATION_CONST ),
'archives' => __( 'Produkt Archiv:', TRANSLATION_CONST ),
'attributes' => __( 'Produkt Attribute:', TRANSLATION_CONST ),
'insert_into_item' => __( 'Zum Produkt hinzufügen', TRANSLATION_CONST ),
'uploaded_to_this_item' => __( 'Zum Produkt hinzugefügt', TRANSLATION_CONST ),
'featured_image' => __( 'Produktbild', TRANSLATION_CONST ),
'set_featured_image' => __( 'Produktbild setzen:', TRANSLATION_CONST ),
'remove_featured_image' => __( 'Produktbild entfernen:', TRANSLATION_CONST ),
'use_featured_image' => __( 'Als Produktbild verwenden:', TRANSLATION_CONST ),
'menu_name' => __( 'Produkte', TRANSLATION_CONST ),
);
$supports = array(
'title',
'editor', // Content Bereich
'excerpt', // Kurzer Auszug des Contents für Archivseiten
'author',
'thumbnail', // featured_image / Produktbild
//'trackbacks',
'custom-fields',
//'revisions',
'page-attributes',
'comments'
);
$args = array(
'labels' => $labels,
'hierarchical' => false,
'description' => __( 'Produkte für mein eigenes E-Commerce Plugin', TRANSLATION_CONST ),
'supports' => $supports,
'public' => true,
'show_ui' => true,
'show_in_menu' => true,
'menu_position' => 5, // Unterhalb der Posts
'menu_icon' => 'dashicons-card',
'show_in_nav_menus' => true,
'publicly_queryable' => true,
'exclude_from_search' => false,
'has_archive' => true,
'query_var' => true,
'can_export' => true,
'capability_type' => 'post'
);
register_post_type( 'product', $args );
}
add_action( 'init', 'register_product_post_type' );
Schritt 3)
In diesem Schritt zeige ich Dir noch exemplarisch an Hand des Produktpreises, wie Du den zuvor registrieren Post Type um ein Post Meta Feld erweitern kannst und wie Du Deine eigene Post Meta Box im Wordpress Backend hinzufügen kannst.
Für das Hinzufügen der Post Meta Box verwendest Du die Wordpress Funktion „add_meta_box“. Dies Funktion nimmt eine Reihe an Parametern entgegen. In diesem Beispiel definieren wir die Parameter zuvor als Variablen, damit der Befehl nicht zu lang wird und damit Du dem Beispiel besser folgen kannst. Du kannst die Parameter auch alle direkt in die Funktion schreiben.
Um Text-Redundanzen zu vermeiden, baust Du nun zuerst eine Funktion, die für Dich die Preis Meta Box (und später vielleicht noch weitere Meta Boxen) registriert.
Jeden Code den Du in diesem Schritt implementierst, solltest Du in Deiner Post Type Datei (z.B. products.php) hinterlegen.
function add_custom_product_meta_box($meta_box_id, $meta_box_title) {
$plugin_prefix = 'product_post_type_';
$html_id_attribute = $plugin_prefix . $meta_box_id . '_meta_box';
$php_callback_function = $plugin_prefix . 'build_' . $meta_box_id . '_meta_box';
$show_me_on_post_type = 'product';
$box_placement = 'side';
$box_priority = 'low';
add_meta_box(
$html_id_attribute,
$meta_box_title,
$php_callback_function,
$show_me_on_post_type,
$box_placement,
$box_priority
);
}
Wordpress bietet Dir nun die dynamische Aktion „add_meta_boxes_{$post_type}“ (WordPress - Add meta boxes) die Du registrieren kannst um Deine Meta Boxen zu integrieren. Dort verwendest Du dann auch die Funktion, die Du eben hinzugefügt hast.
function product_post_type_add_meta_boxes( $post ){
add_custom_product_meta_box('price', __( 'Preis', TRANSLATION_CONST ));
// Hier können problemlos noch weitere Meta Boxen registriert werden.
}
add_action( 'add_meta_boxes_product', 'product_post_type_add_meta_boxes' );
Nun musst Du noch drei Funktionen ergänzen. Die erste Funktion dient zum Darstellen der Post Meta Box, die zweite Funktion dient zum Speichern des Wertes, der in der Post Meta Box eingegeben wird und die dritte Funktion dient zum Ausgeben des Preises im Frontend. Die erste Funktion trägt den Namen des Callbacks, der in der Funktion „add_custom_product_meta_box“ für unser Post Meta Feld „price“ zusammengestellt wurde. Die zweite Funktion wird als Callback an die dynamische Wordpress Aktion „save_post_product“ angehangen (WordPress - Save post action).
/**
* Darstellung der "price" Meta Box
*
* @param object $post Das Post Objekt.
*/
function product_post_type_build_price_meta_box($post) {
wp_nonce_field( basename( __FILE__ ), 'product_post_type_price_meta_box_nonce' );
$current_price = get_post_meta( $post->ID, 'product_price', true );
?>
<div class="inside">
<section id="price-meta-box-container">
<p>
<input type="number" name="product_price" id="product-price"<?php echo ' value="'.$current_price.'"'; ?>>
</p>
</section>
</div>
<?php
}
Im ersten Teil dieser Funktion fügen wir ein „nonce“ Feld (WordPress - Nonces) ein um XSS (Cross-Site-Scripting) (WordPress - Cross site scripting) zu unterbinden. Nachfolgend ermitteln wir den aktuellen Post Meta Value mit der Wordpress Funktion „get_post_meta“ (WordPress - Get post meta). Mit dieser Funktion kann auch zukünftig das Post Meta Feld immer abgerufen werden, solltest Du die Post ID kennen. Und als letztes wird noch ein wenig HTML ausgegeben, damit die Meta Box ein Eingabefeld besitzt.
Kommen wir nun zur Speicherung:
/**
* Speicherung der "price" Meta Box Daten
*
* @param int $post_id Die Post ID.
*/
function product_post_type_save_price_meta_boxes_data( $post_id ){
if ( !isset( $_POST['product_post_type_price_meta_box_nonce'] ) ||
!wp_verify_nonce(
$_POST['product_post_type_price_meta_box_nonce'],
basename( __FILE__ )
) ){
return;
}
if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE )
return;
if ( defined( 'DOING_AJAX' ) && DOING_AJAX )
return;
if ( !current_user_can( 'edit_post', $post_id ) )
return;
if ( isset( $_REQUEST['product_price'] ) ) {
update_post_meta(
$post_id,
'product_price',
sanitize_text_field( $_POST['product_price'] )
);
}
}
add_action( 'save_post_product', 'product_post_type_save_price_meta_boxes_data', 10, 2 );
Zu Beginn der Funktion werden einige Sicherheitsüberprüfungen durchgefüht. Es wird zuerst überprüft, ob die übergebene Nonce vorhanden und korrekt ist. Danach wird überprüft ob der aktuelle Funktionsaufruf durch eine automatische Speicherung oder durch Ajax geschieht. In beiden Fällen brechen wir den vorgang ab. Und zuletzt überprüft das Script, ob der Rang des aktuellen Nutzers über die Capability „edit_post“ verfügt.
Trifft all dies nicht zu, dann wird noch überprüft, ob „product_price“ übergeben wurde. Sollte das zutreffen, dann wird das Post Meta Feld über die Wordpress Funktion „update_post_meta“ (WordPress - Update post meta) hinzugefügt oder aktuallisiert.
Nun fehlt noch die Funktion zum Ausgeben des Preises im Frontend. Hierbei orientierst Du Dich am besten an der Benamung der andern Wordpress Funktionen wie „the_title“, „the_content“, oder „the_permalink“. Du könntest die Funktion bspw. „the_product_price“ nennen:
/**
* Ausgabe des "price" Post Meta Feldes
*
* @param object $post Das Post Objekt.
* @param bool $echo Sollen der Preis ausgegeben werden?
*/
function the_product_price($post = 0, $echo = true) {
$post = get_post( $post );
$id = isset( $post->ID ) ? $post->ID : 0;
$value = get_post_meta( $id, 'product_price', true );
if($echo) {
echo sprintf(
'<span class="product-detail--price">%s %s</span>',
esc_html($value),
__('€', TRANSLATION_CONST)
);
}
else
return $value;
}
In dieser letzten Funktion ermittelst Du zunächst das Post Objekt anhand der übergebenen Post Id, oder des übergeben Post Objektes. Danach wird der Wert des erfragten Post Meta Feldes ermittelt und je nach „echo“ Parameter ausgegeben oder zurückgegeben.
Schritt 4)
Dies ist der letzte Schritt, bevor Du vollends mit Deinem eigenen Post Type arbeiten kannst. In diesem Schritt wirst Du eine einfache Übersichtsseite und eine einfache Produkt Detailseite anlegen. Diese kannst Du später selber an Deine Bedürfnisse anpassen. Das Anlegen der Seite unterscheidet sich aber zwischen einem Theme und einem Plugin. In einem Theme werden diese beiden Dateien lediglich im Basisverzeichnis hinterlegt und in einem Plugin wirst Du sie im Verzeichnis „theme_files“ ablegen. Danach zeige ich dir, wie Du Dich in die notwendige Aktion einhängst, damit Deine Dateien erkannt werden.
Weiterhin ist es wichtig, dass Du Deine Permalink-Struktur in den Wordpress Einstellungen aktuallisierst, nachdem Du mit allen Schritten fertig bist. Ansonsten werden die Routen zu Deiner Archivseite und zur Einzelansicht nicht erkannt.
Du beginnst zunächst mit der einfachen Archivseite für Deine Produkte. Dazu legst Du wie oben bereits beschrieben die Datei „archive-product.php“ entweder im Basisverzeichnis Deines Themes, oder im Verzeichnis „theme_files“ Deines Plugins an.
archive-product.php | Für ein Theme
<?php
/**
* Template Name: Archive Product Template
*
* Description: Eine eigene Archivseite für meinen Post Type „product“
*/
get_header(); ?>
<div class="main-wrap" role="main">
<!-- Darstellung der Produkte -->
<section id="product-listing">
<?php if ( have_posts() ) : ?>
<div class="row">
<?php while ( have_posts() ) : the_post(); ?>
<div class="column"">
<?php get_template_part( 'template-parts/product-content', get_post_format() ); ?>
</div>
<? endwhile; ?>
</div>
<?php endif; ?>
</section>
</div>
<?php get_footer();
In dieser Datei wird der Wordpress Loop verwendet, um alle Produkte zu durchlaufen und auszugeben. Die genauen Inhalte werden mit der Wordpress Funktion „get_template_part“ aus der Datei „product-content.php“ im Verzeichnis „template-parts“ geladen (WordPress - Get template part). Diese Funktion kann lediglich in Themes verwendet werden.
Deshalb verwendest Du in einem Plugin einen anderen Code:
archive-product.php | Für ein Plugin
<?php
/**
* Template Name: Archive Product Template
*
* Description: Eine eigene Archivseite für meinen Post Type „product“
*/
get_header(); ?>
<div class="main-wrap" role="main">
<!-- Darstellung der Produkte -->
<section id="product-listing">
<?php if ( have_posts() ) : ?>
<div class="row">
<?php while ( have_posts() ) : the_post(); ?>
<div class="column"">
<?php require_once( 'product-content.php' ); ?>
</div>
<? endwhile; ?>
</div>
<?php endif; ?>
</section>
</div>
<?php get_footer();
Der Unterschied zu der Theme-Version ist, dass die Datei „product-content.php“ nun einfach mit im Verzeichnis „theme_files“ angelegt wird. Nun erstellst Du im jeweiligen Verzeichnis die Datei „product-content.php“:
<?php
/**
* Das Standard-Template zum Anzeigen eines Produktes
*/
?>
<div class="product-container" id="product-<?php the_ID(); ?>">
<?php if ( has_post_thumbnail() ) : ?>
<a href="<?php the_permalink(); ?>">
<img src="<?php the_post_thumbnail_url( ); ?>">
</a>
<?php endif; ?>
<div class="product-content-section">
<header>
<h3><a href="<?php the_permalink(); ?>"><?php the_title(); ?></a></h3>
</header>
<div class="entry-content">
<?php
the_product_price(get_the_ID());
echo '<br />';
the_excerpt();
?>
</div>
</div>
</div>
In der Datei kam nun auch unsere Funktion „the_product_price“ zum Einsatz. Diese gibt oberhalb des Auszugs den Preis für das Produkt aus.
Nun benötigst Du für Deine Produkte nur noch eine Detailseite. Hier gehst Du ähnlich vor wie bei der Archivseite. Die Einzelansicht / Deteilseite trägt den Dateinamen „single-product.php“ und wird bei der Integration in einem Theme in dessen Basisverzeichnis abgelegt und bei einem Plugin im Verzeichnis „theme_files“.
single-product.php
<?php get_header(); ?>
<?php while ( have_posts() ) : the_post(); ?>
<article <?php post_class() ?> id="product-<?php the_ID(); ?>">
<div class="row column" id="product-header">
<h1 class="entry-title"><?php the_title(); ?></h1>
</div>
<div class="main-wrap" role="main">
<div class="entry-content">
<div class="row">
<div class="medium-6 columns">
<?php if(has_post_thumbnail( )) : ?>
<a href="<?php the_post_thumbnail_url('large'); ?>">
<img src="<?php the_post_thumbnail_url('large'); ?>"/>
</a>
<?php endif; ?>
</div>
<div class="medium-6 columns">
<?php the_product_price(get_the_ID()); ?>
</div>
</div>
<div class="row columns">
<?php the_content(); ?>
</div>
</div>
<footer>
<?php
wp_link_pages(
array(
'before' => '<nav id="page-nav"><p>' . __( 'Produkte:', TRANSLATION_CONST ),
'after' => '</p></nav>',
)
);
?>
</footer>
<?php comments_template(); ?>
</div>
</article>
<?php endwhile;?>
<?php get_footer();
Somit gibt es nun auch eine Detailansicht für das Produkt. Hier wird ebenfalls die Funktion „the_product_price“ des Produkt Post Types verwendet. Nun zeige ich Dir noch wie Du die „theme_files“ in einem Plugin einbinden kannst.
Hinweis: Das Beispiel zur Integratien der Dateien ist nicht auf einem objektorientieren Plugin aufgebaut. In einem objektorientierten Plugin könntest Du diese Funktionen in der Plugin Klasse hinterlegen und im Konstruktor aufrufen.
Am besten erstellst Du in Deinem Plugin ein Verzeichnis für weiteren Code, z.B. „libs“, damit Deine Plugin Datei nicht zu unübersichtlich wird. Darin erstellst Du nun die PHP Datei „register_custom_theme_files.php“:
<?php
/**
* Verlinkt die Plugin SINGLE Post-Type Seiten
*/
function example_plugin_custom_single_theme_file_include($template) {
global $wp;
$requested_post_type = $wp->query_vars["post_type"];
if($requested_post_type == 'product')
return $template;
$file = EXAMPLE_PLUGIN_PATH.'/theme_files/single-'.$requested_post_type.'.php';
if(file_exists($file)) {
$template = $file;
}
return $template;
}
add_filter('single_template', 'example_plugin_custom_single_theme_file_include');
/**
* Verlinkt die Plugin ARCHIVE Post-Type Seiten
*/
function example_plugin_custom_archive_theme_file_include($template) {
global $wp;
$requested_post_type = $wp->query_vars["post_type"];
if($requested_post_type == 'product')
return $template;
$file = EXAMPLE_PLUGIN_PATH.'/theme_files/archive-'.$requested_post_type.'.php';
if(file_exists($file)) {
$template = $file;
}
return $template;
}
add_action('archive_template', 'example_plugin_custom_archive_theme_file_include');
Die Überprüfung, ob der angefragte Post Type gleich „product“ ist, kannst Du gerne im nachhinein refaktorisieren und mit einem Array vergleichen, in dem alle registrierten Post Types Deine Plugins eingetragen sind. Weiterhin wird die Konstante „EXAMPLE_PLUGIN_PATH“ verwendet, die in der Plugin Datei wie folgt initialisiert wird:
define('EXAMPLE_PLUGIN_PATH', plugin_dir_path( __FILE__ ));
Die Datei muss nun nurnoch in der Plugin Datei mit der PHP Funktion „require_once“ integriert werden.
require_once EXAMPLE_PLUGIN_PATH.'/libs/register_custom_theme_files.php';
In diesem Tutorial hast Du Deinen eigenen Post Type angelegt und Ihn um Komponenten wie Post Meta Felder, eigene Post Meta Boxen, sowie die "single-{post-type}.php", als auch die "archive-{post-type}.php" erweitert. Nun kannst Du Deine eigenen Wordpress Seiten flexibel um Inhalte erweitern und das Wordpress Backend dieser Inhalte an Deine Bedürfnisse anpassen.
Solltest Du Fragen oder Anregungen haben, so zögere nicht uns ein Kommentar zu hinterlassen.
Durch unsere langjährige Arbeit und über 100 erfolgreiche Projekte, konnten wir viele Erfahrungen sammeln. Dieses Know-How im Online-Marketing gaben wir u.a. bei Vorträgen von Google, der Industrie- und Handelskammer und der Handwerkskammer weiter.
Mit Know-How, Kreativität und Leidenschaft entwickeln wir auf unsere Kunden abgestimmte Marketing-Strategien, die Sie sicher und nachhaltig zum Erfolg führen. Gemeinsam setzen wir Ihr Online-Marketing so um, dass Sie langfristig Ihren Umsatz und Return-On-Investment steigern.
Jetzt kostenlosen Beratungstermin vereinbaren oder unter 0561 / 850 194 76 anrufen.
Paypal in Laravel 5.5 integrieren – so funktioniert's
Viele Internetseiten und Webshops bieten heutzutage Paypal als Bezahlungs-Gateway an, denn Paypal zählt zu den schnellsten und sichersten Bezahlsystemen weltweit. In diesem Artikel zeige ich Dir, wie Du die von Paypal auf Github bereitgestellte PHP SDK in Dein Laravel – Projekt integrieren kannst. Außerdem werden wir die Laravel Authentication aktivieren und über eine Datenbank Migration die Spalten „paypal_agreement_status“ und „paypal_agreement_id“ zur Benutzer Tabelle hinzufügen. Dadurch kannst Du Paypal Bezahl-Abonnements einem Benutzer zuordnen, den Status des Abonnements im Auge behalten und diesen gegebenenfalls aktuallisieren.
Die Paypal PHP SDK
Paypal bietet unter seinem Github Account eine eigene PHP SDK an (Github - Paypal php sdk). Durch die SDK kannst Du spielend leicht die Paypal Rest-API ansteuern, um bspw.:
- Einzelne Bezahlungen zu integrieren
- Den Status eines Bezahlvorgangs per Token abzurufen
- Bezahl-Abonnements zu planen (Billing plans)
- Abonnement-Verträge (agreements) nach Vorlage geplanter Abonnements anzulegen
- Den Status eines Abonnement-Vertrages abzurufen
- Einen Abonnement-Vertrag zu pausieren, oder zu re-aktivieren
Im Zuge dieses Artikels werde Ich Dir Schritt für Schritt zeigen wie Du die SDK in Dein Projekt einbindest, wie Du „billing plans“ anlegst und wie Du diese verwenden kannst um Abonnements zu erstellen. Zum Schluss zeige ich Dir noch, wie Du mit Hilfe der „agreement_id“ eines laufenden Abonnements dessen Status abfragen kannst (bspw.: aktiv, pausiert, beendet). Eine Dokumentation über die SDK von Paypal findest Du unter: Github - Paypal php sdk
Integration der Paypal PHP SDK – Paypal in Laravel integrieren
Schritt 1 - Einrichtung des Laravel Projektes
Dadurch, dass die PHP SDK als Repository auf Github bereitgestellt wird und das Repository über eine „composer.json“ verfügt, kannst Du es einfach als Abhängigkeit in die „composer.json“ Deiner Laravel Anwendung integrieren. Aber zunächst solltest Du mit Hilfe von Composer Dein Laravel Projekt über die Konsole/das Terminal anlegen, sofern das noch nicht geschehen ist. Füge bei „myApplication“ den Name Deiner Laravel Anwendung ein:
$ composer create-project --prefer-dist laravel/laravel myApplication
Solltest Du Composer noch nicht kennen, dann kannst Du unter Get Composer mehr über den PHP Paket-Manager erfahren. Dort findest Du eine Installationsanleitung, einen Artikel der Dich in die Benutzung von Composer einführt und auch eine Dokumentation.
Nachdem Du nun ein Projekt besitzt, kannst Du die benötigten Github Repositories mit Hilfe von Composer in Dein Projekt integrieren. Zunächst integrierst Du das Paket „paypal/rest-api-sdk-php“ per Konsolenbefehl:
$ composer require paypal/rest-api-sdk-php
Um auf einem einfachen Weg Http Anfragen von Deiner Laravel Anwendung zu versenden, kannst Du das Github Repository „Guzzle“ verwenden. Dadurch wird das Ansprechen der Rest-API erleichtert. Solltest Du „Guzzle“ also noch nicht verwenden, so kannst Du es mit Hilfe von Composer über die Konsole integrieren:
$ composer require guzzlehttp/guzzle
Schritt 2 – Vorbereitung der Datenbankanbindung
Nun bereitest Du die Anbindung von Laravel an die Datenbank vor. Zu aller erst legst Du auf Deinem Datenbankserver eine Datenbank für das Projekt an. Danach hinterlegst Du die DB Informationen in der „.env“ Datei. Wie die Datenbankanbindung im Detail abläuft, erfährst Du unter Laravel 5.5 - Database.
Wärend späterer Datenbank-Migrationen ist es auch nützlich eine Standartlänge für Datenbank „strings“ festzulegen. Dadurch ersparst Du Dir unnötige Fehler und Schreibarbeit. Um die Standartlänge festzulegen kannst Du unter „myApplication/app/Providers“ den Provider „AppServiceProvider“ öffnen und fügst dort den Namespace „IlluminateSupportFacadesSchema“ hinzu. Danach musst Du in der Funktion „boot“ nurnoch folgenden Funktionsaufruf intergerieren:
public function boot()
{
Schema::defaultStringLength(150);
}
Schritt 3 – Laravel User Authentication aktivieren
Um die von Laravel mitgelieferte User Authentication nutzen zu können, musst Du sie vorher „aktivieren“. Dabei hilft Dir das Laravel-Tool Artisan. Artisan ist eine PHP Anwendung, die Du parametrisiert in der Konsole ausführen kannst und die für Dich diverse Aufgaben rund um Deine Laravel Anwendung ausführt. Mit dem folgenden Befehl erstellt Artisan alle benötigten Komponenten wie Model, Views, Controller und Routen:
$ php artisan make:auth
Nun legst Du eine neue Migrationsdatei an, damit Deine Benutzer die zu Beginn erwähnten Spalten „paypal_agreement_status“ und „paypal_agreement_id“ erhalten. Eine Migration legst Du ebenfalls über Artisan an, wobei Du nach dem „make“ Befehl noch einen aussagekräftigen Namen für die Migrationsdatei angeben solltest:
$ php artisan make:migration add_paypal_agreement_to_user
Nun öffnest Du die gerade erstellte Migration. Diese findest Du unter „myApplication/database/migrations“. In dieser Datei gibt es nun zwei leere Methoden, „up“ und „down“. Füge nun folgenden Code in die „up“ – Funktion ein:
public function up() {
Schema::table('users', function($table) {
$table->integer('paypal_agreement_id');
$table->string('paypal_agreement_status');
}
}
Dem Status („paypal_agreement_status“) wird später der enum Wert der „state“ Eigenschaft vom Rest API Aufruf zugewiesen. Dazu verwendest Du später die Rest API „v1/payments/billing-agreements/agreement_id“ (Paypal Developer Docs - Billing agreements).
Um einen sauberen Rollback durchführen zu können muss noch die „down“ - Funktion implementiert werden. In dieser tauscht Du die Datentypen einfach gegen die Funktion „dropColumn“:
public function down() {
Schema::table('users', function($table) {
$table->dropColumn('paypal_agreement_id');
$table->dropColumn('paypal_agreement_status');
}
}
Damit die nun erstellte Migration und die vorherigen Migrationen in die Datenbank eingespielt werden, musst Du einen Artisan Konsolenbefehl ausführen:
$ php artisan migrate
Die Migrationen werden dadurch in die Datenbank überspielt und Du kannst die von Laravel mitgelieferte User Authentication nutzen.
Schritt 4 – Erstellen der Paypal Config Datei
Als nächstes erstellst Du eine Konfigurationsdatei in der alle Einstellungen und Schlüssel von Paypal hinterlegt werden. Die Datei erstellst Du unter „myApplication/config“ mit dem Dateinamen „paypal.php“:
<?php
return array(
/**
* Sandbox und Live credentials
*/
'credentials' => array(
'sandbox' => array(
'client_id' => env('PAYPAL_SANDBOX_CLIENT_ID', ''),
'secret' => env('PAYPAL_SANDBOX_SECRET', '')
),
'live' => array(
'client_id' => env('PAYPAL_LIVE_CLIENT_ID', ''),
'secret' => env('PAYPAL_LIVE_SECRET', '')
),
),
/**
* SDK Konfiguration
*/
'settings' => array(
/**
* Payment Mode
*
* Optionen: 'sandbox' oder 'live'
*/
'mode' => env('PAYPAL_MODE', 'sandbox'),
// Angabe in Sekunden
'http.ConnectionTimeOut' => 3000,
'log.LogEnabled' => true,
'log.FileName' => storage_path() . '/logs/paypal.log',
/**
* Log Level
*
* Optionen: 'DEBUG', 'INFO', 'WARN' oder 'ERROR'
*/
'log.LogLevel' => 'DEBUG'
),
);
In der Paypal Konfiguration werden die Schlüssel „client_id“ und „secret“ für den Sandbox und Live Modus mit der PHP Funktion „env()“ geladen. Diese müssen nun noch in der „.env“ Datei hinterlegt werden, die Werte folgen im Anschluss:
PAYPAL_MODE= PAYPAL_SANDBOX_CLIENT_ID= PAYPAL_SANDBOX_SECRET= PAYPAL_LIVE_CLIENT_ID= PAYPAL_LIVE_SECRET=
Schritt 5 – Registrieren der Anwendung in Paypal
Nun musst Du Deine Anwendung bei Paypal registrieren, um die „client_id“ sowie den „secret“ Code für den jeweiligen Modus zu erhalten. Dazu besuchst Du: Paypal Developer und meldest Dich mit Deinen Benutzerdaten unter „Log into Dashboard“ an.
Nach dem Login kommst Du auf Dein Paypal Development Dashboard. Wenn Du auf diesem runterscrollst bis zur Sektion „REST API apps“, dann kannst Du über den Knopf „Create App“ eine neue Rest API Anwendung zu Deinem Account hinzufügen.
Wenn Du auf „Create App“ geklickt hast, öffnet sich die Maske zur Erstellung einer neuen Anwendung. In dieser trägst Du den Namen Deiner Anwendung ein und wählst den „facilitator“ Sandbox Developer Account aus. Dieser sollte im Normalfall automatisch erstellt werden. Wenn die erste Erstellung des Accounts noch nicht geklappt hat, schlägt die Erstellung der Anwendung ebenfalls fehl. Versuche es einfach nach einer Weile erneut.
Nachdem die Anwendung erfolgreich erstellt wurde, gelangst Du auf die Detailansicht Deiner Anwendung. Auf dieser findest Du die benötigten Ids und secret Codes. Oberhalb der Box „Sandbox API Credentials“ findest Du zwei Knöpfe um zwischen dem Sandbox und dem Live Modus zu wechseln.
Wenn Du auf „Show“ unter „Secret“ klickst, erweitert sich das Panel und Du kannst den jeweiligen Secret Code sehen.
Nun kopierst Du die Schlüssel und fügst sie in die „.env“ Datei ein:
PAYPAL_MODE=sandbox PAYPAL_SANDBOX_CLIENT_ID=hier-steht-normalerweise-eine-lange-client-id PAYPAL_SANDBOX_SECRET=hier-steht-normalerweise-ein-langer-secret-code PAYPAL_LIVE_CLIENT_ID=hier-steht-normalerweise-eine-lange-client-id PAYPAL_LIVE_SECRET=hier-steht-normalerweise-ein-langer-secret-code
Schritt 6 – Erstellung des Paypal Controllers
Die „.env“ Datei ist gefüllt und Du kannst jetzt den Laravel Controller zur Verwaltung der Paypal PHP SDK erstellen. Dabei hilft Dir wieder Artisan:
$ php artisan make:controller PaypalController
Artisan hat nun für Dich einen leeren Controller mit dem Namen „PaypalController.php“ im Verzeichnis „myApplication/app/Http/Controllers“ angelegt. In diesen fügst Du nun folgende Namespaces hinzu:
use PayPalApiChargeModel; use PayPalApiCurrency; use PayPalApiMerchantPreferences; use PayPalApiPaymentDefinition; use PayPalApiPlan; use PayPalApiPatch; use PayPalApiPatchRequest; use PayPalCommonPayPalModel; use PayPalRestApiContext; use PayPalAuthOAuthTokenCredential;
Als nächstes benötigst Du noch einige Felder um die Paypal Daten auf der Konfiguration zwischenzuspeichern. Diese integriest Du am besten ganz oben in Deinem Controller:
private $apiContext; private $mode; private $client_id; private $secret;
Diese Felder füllst Du nun über den Konstruktor. Hier kommt auch die zuvor erstellte Konfiguration zum Einsatz:
public function __construct() {
// Zunächst wird geprüft, in welchem Modus sich die App befindet
if(config('paypal.settings.mode') == 'live'){
$this->client_id = config('paypal.credentials.live.client_id');
$this->secret = config('paypal.credentials.live.secret');
} else {
$this->client_id = config('paypal.credentials.sandbox.client_id');
$this->secret = config('paypal.credentials.sandbox.secret');
}
// Nun legst Du den Paypal API Context fest
$oAuthToken = new OAuthTokenCredential($this->client_id, $this->secret)
$this->apiContext = new ApiContext();
$this->apiContext->setConfig(config('paypal.settings'));
}
Durch das Setzen des Kontextes im Konstruktor können wir in den Funktionen des Controllers immer mit dem korrekten Kontext arbeiten. Wichtig ist, dass wir alle API Zugriffe in einem Try-Catch Block unterbringen, da gegebenenfalls ein Zugriffsfehler („PayPalConnectionException“) auftreten kann.
Schritt 7 – Erstellung einer Paypal Abonnement Vorlage
Nun kannst Du mit Hilfe Deiner Paypal Konfiguration und der Schlüssel eine Vorlage für ein Bezahl-Abonnement erstellen und aktivieren. Dies ist notwendig, damit die Benutzer später Abonnements abschließen zu können. Paypal benötigt für ein „Billing agreement“ zuerst einen „Billing plan“ als Vorlage.
In dem folgenden Beispiel benötigst Du die Funktion zum Anlegen der Vorlage nur ein mal, danach kann sie theoretisch entfernt werden. Du kannst natürlich einen komplexeren Workflow entwickeln, in dem administrative Nutzer Vorlagen für Abonnements mit Hilfe dieser Methode anlegen können. In diesem Fall müsstest Du die generierten Plan Ids in einer Datenbank speichern und nicht in der „.env“ Datei.
Erstelle nun unterhalb des Konstruktors die Funktion „create_billing_plan“:
public function create_billing_plan() {
// Erstelle einen neuen Plan
$plan = new Plan();
$plan->setName('myApplication - Monatsabonnement')
->setDescription('Monatlich abgerechnetes Abonnement der App myApplication')
->setType('infinite');
// Nun definierst Du die Bezahlparameter
$paymentDefinition = new PaymentDefinition();
$paymentDefinition->setName('Regelmäßige Bezahlung')
->setType('REGULAR')
->setFrequency('Month')
->setFrequencyInterval('1')
->setCycles('0')
->setAmount(new Currency(array('value' => 15, 'currency' => 'EUR')));
// An dieser Stelle definierst Du die Anbieter Einstellungen
$merchantPreferences = new MerchantPreferences();
$merchantPreferences->setReturnUrl('https://myapplication.dev/subscribe/paypal/return')
->setCancelUrl('https://myapplication.dev/subscribe/paypal/return')
->setAutoBillAmount('yes')
->setInitialFailAmountAction('CONTINUE')
->setMaxFailAttempts('0');
$plan->setPaymentDefinitions(array($paymentDefinition));
$plan->setMerchantPreferences($merchantPreferences);
// Jetzt ist alles vorbereitet, der Plan kann also bei Paypal erstellt werden
try {
$createdPlan = $plan->create($this->apiContext);
try {
$patch = new Patch();
$value = new PayPalModel('{"state":"ACTIVE"}');
$patch->setOp('replace')
->setPath('/')
->setValue($value);
$patchRequest = new PatchRequest();
$patchRequest->addPatch($patch);
$createdPlan->update($patchRequest, $this->apiContext);
$plan = Plan::get($createdPlan->getId(), $this->apiContext);
// Zum Schluss wird noch die ID ausgegeben, wenn alles geklappt hat
echo 'Vorlagen ID für das Abonnement: ' . $plan->getId();
} catch (PayPalExceptionPayPalConnectionException $ex) {
echo $ex->getCode();
echo $ex->getData();
die($ex);
} catch (Exception $ex) {
die($ex);
}
} catch (PayPalExceptionPayPalConnectionException $ex) {
echo $ex->getCode();
echo $ex->getData();
die($ex);
} catch (Exception $ex) {
die($ex);
}
}
}
In dieser Funktion ist nun einiges Integriert. Du kannst die Funktion selbstverständlich noch refaktorisieren, wenn Du sie in einen komplexeren Workflow einbetten willst.
Zu Beginn der Funktion legst Du einen neuen Plan an und initialisierst die benötigten Eigenschaften wie die Bezahlparameter und den Anbieter.
Deine definierte Vorlage hat folgenden Aufbau:
- Name: „myApplication – Monatsabonnement“
- Beschreibung: „Monatlich abgerechnetes Abonnement der App myApplication“
- Typ: „infinite“
- Bezahlparameter:
- Name: „Regelmäßige Bezahlung“
- Type: „REGULAR“
- Abbuchung: „Month“
- Abbuchungsintervall: „1“
- Anzahl der Zahlungen: „0“ (durch Type: „infinite“)
- Betrag: „15 Euro“
- Anbieter Einstellungen:
- URL zur Zahlungsbestätigung: „https://myapplication.dev/subscribe/paypal/return“
- URL zum Zahlungsabbruch: „https://myapplication.dev/subscribe/paypal/return“
- Sollen unbezahlte Beträge im nächsten Zyklus mitabgebucht werden: „yes“
- Aktion beim Fehlschlagen der ersten Abbuchung: „CONTINUE“ (Der Betrag wird bei der nächsten Abbuchung addiert)
- Maximale Fehler beim Abbuchen: „0“ (unbegrenzt)
Wichtig ist, dass Du die Domain im Beispiel gegen Deine Domain tauscht. Weiterhin kann es sein, dass es Probleme mit Links geben kann, in denen ein Port angegeben ist (bspw.: „http://localhost:8080/subscribe/paypal/return“). Am besten Du legst einen V-Host über Deinen lokalen Webserver an (bspw.: „http://local-app.dev/“), dann hast Du damit keine Probleme.
Nun öffnest Du die Routen-Datei „web.php“ unter „myApplication/routes“ und trägst folgende Route zu der „create_billing_plan“ Funktion ein:
Route::get('/create-plan', 'PaypalController@create_billing_plan');
Wenn Du nun im Browser die Url „http://myapplication.dev/create-plan“ aufrufst, wird die „billing plan id“ ausgegeben. Diese Id kannst du nun in der „.env“ Datei hinterlegen:
PAYPAL_SANDBOX_PLAN_ID=hier-steht-normalerweise-eine-plan-id PAYPAL_LIVE_PLAN_ID=hier-steht-normalerweise-eine-plan-id
Wenn Du mit Deiner Website in den Live Modus gehen willst, musst Du die Url noch mal im Live Modus aufrufen, damit die Vorlage auch für diesen Angelegt wird.Wichtig ist, dass Du daran denkst, die Route in der Routen Datei „web.php“ auszukommentieren, wenn Du sie nicht mehr benötigst.
// Route::get('/create-plan', 'PaypalController@create_billing_plan');
Schritt 8 – Eintragung der Paypal Routen
Trage nun die produktiven Routen für Deinen Paypal Controller in die „web.php“ Routen Datei ein:
Route::get('/subscribe/paypal', 'PaypalController@paypal_redirect')->name('paypal.redirect');
Route::get('/subscribe/paypal/return', 'PaypalController@paypal_return')->name('paypal.return');
Schritt 9 – Abonnements im Paypal Controller anlegen
Nun fügst Du noch die beiden vorher angelegten Routen als Funktionen zu Deinem Paypal Controller hinzu, davor musst Du aber noch die „billing plan“ Id aus der env Datei laden. Dies passiert im Konstruktor:
private $apiContext;
private $mode;
private $client_id;
private $secret;
private $plan_id;
public function __construct() {
// Zunächst wird geprüft, in welchem Modus sich die App befindet
if(config('paypal.settings.mode') == 'live'){
$this->client_id = config('paypal.credentials.live.client_id');
$this->secret = config('paypal.credentials.live.secret');
$this->plan_id = env('PAYPAL_LIVE_PLAN_ID', '');
} else {
$this->client_id = config('paypal.credentials.sandbox.client_id');
$this->secret = config('paypal.credentials.sandbox.secret');
$this->plan_id = env('PAYPAL_SANDBOX_PLAN_ID', '');
}
// Nun legst Du den Paypal API Context fest
$oAuthToken = new OAuthTokenCredential($this->client_id, $this->secret)
$this->apiContext = new ApiContext();
$this->apiContext->setConfig(config('paypal.settings'));
}
Selbstverständlich kannst Du auch noch ein Key-Value Paar in Deine Paypal Konfiguration aufnehmen, um bspw. mehrere vordefinierte Pläne bereitzustellen. Nun erstellst Du die erste Methode, in der ein Vertrag mit der Paypal PHP SDK angelegt wird. Danach wird der Benutzer nach Paypal weitergeleitet:
<?php
public function paypal_redirect(){
// Anlegen eines neuen Abonnements
$agreement = new Agreement();
$agreement->setName('myApplication - Abonnement')
->setDescription('Basisvertrag')
->setStartDate(CarbonCarbon::now()->addMinutes(5)->toIso8601String());
// Nun wird dem Vertrag die zuvor generierte „billing plan“ ID zugewiesen
$plan = new Plan();
$plan->setId($this->plan_id);
$agreement->setPlan($plan);
// Kundendetails – Hier reicht die Bezahlmethode. Kann aber erweitert werden
$payer = new Payer();
$payer->setPaymentMethod('paypal');
$agreement->setPayer($payer);
try {
// Nun kannst Du den Vertrag über die PHP SDK vorbereiten
$agreement = $agreement->create($this->apiContext);
// Nun wird noch die Bestätigungs-Url von Paypal ermittelt um den nutzer weiterzuleiten
$approvalUrl = $agreement->getApprovalLink();
return redirect($approvalUrl);
} catch (PayPalExceptionPayPalConnectionException $ex) {
echo $ex->getCode();
echo $ex->getData();
die($ex);
} catch (Exception $ex) {
die($ex);
}
}
Also nächstes brauchst Du noch die Funktion auf die Paypal den Benutzer zurückleitet. An dieser Stelle bekommst Du von Paypal auch einen Token übergeben mit dessen Hilfe Du bei der Vertragserstellung erkennen kannst, ob der Benutzer mit dem Vertrag einverstanden ist:
public function paypal_return(Request $request){
$token = $request->token;
$agreement = new PayPalApiAgreement();
try {
// Jetzt kannst Du versuchen mit dem Paypal Token den Vertrag anzulegen
$result = $agreement->execute($token, $this->apiContext);
$user = Auth::user();
$user->role = 'Abonnent';
$user->paypal = 1;
if(isset($result->id)){
$user->paypal_agreement_id = $result->id;
}
$user->save();
echo 'Ein neuer Abonnent wurde eingetragen!';
} catch (PayPalExceptionPayPalConnectionException $ex) {
echo 'Entweder hast Du die Bezahlung abgebrochen, oder Deine Sitzung ist abgelaufen.';
}
}
Ist alles gut gelaufen wird Dir eine „agreement Id“ zurückgegeben und diese kannst Du im Benutzer speichern. Deine Benutzer können über die Url „http://myapplication.dev/subscribe/paypal“ nun ein Abonnement abschließen. Wie Du die Url schlussendlich integrierst ist Dir überlassen. Hier folgt noch ein Beispiel für die Integration über eine einfachen Link:
<a href="{{ url('subscribe/paypal') }}" class=“paypal-btn“>Abonnement anlegen</a>
Schritt 10 – Abfragen des Status eines Abonnements
Im letzten Schritt wollte ich Dir noch zeigen, wie Du schnell den Status eines Abonnements abfragen kannst, um bspw. zu erkennen, ob dieses noch aktiv ist. Deine Nutzer können diese ja auch einstellen. Zunächst ergänzt Du Deine Routen um folgenden Eintrag:
Route::get('/agreement-status', 'PaypalController@paypal_agreement_status');
Im Paypal Controller solltest Du nun noch den folgenden Namespace hinzufügen:
use PaypalApiAgreement;
Den Paypal Controller erweiterst Du nun um folgende Funktion:
public function paypal_agreement_status(Request $request){
try {
$user = Auth::user();
$agreement = Agreement::get($user->paypal_agreement_id, $this->apiContext);
if(isset($agreement->id)){
$user->paypal_agreement_status = $agreement->state;
}
$user->save();
echo sprintf('Das Abonnement „%s“ hat aktuell den Status „%s“.', $agreement->id, $agreement->state);
} catch (PayPalExceptionPayPalConnectionException $ex) {
echo 'Beim Abrufen des Abonnement-Status kam es zum Fehler.';
echo $ex->getCode();
echo $ex->getData();
die($ex);
}
}
Dies ist ebenfalls nur eine Beispielfunktion, die Dir zeigen soll, wie einfach Du den Status abrufen kannst. Willst Du weitere Funktionen von Paypal in Deiner Anwendung integrieren, so wirf doch einen Blick in die Dokumentation. Paypal liefert in Deiner PHP SDK auch viele nützliche Beispiele an denen Du Dich bei der integration orientieren kannst Github - Paypal php sdk - Beispiel.
In diesem Artikel hast Du gelernt, wie Du die PHP SDK von Paypal in Laravel 5.5 integrierst. Du hast Composer eingesetzt um Laravel aufzusetzen und die PHP SDK zu integrieren. Weiterhin hast Du Projektkomponenten wie z.B. Controller und Migrationen mit dem Tool Artisan angelegt. Dein Paypal Controller wurde mit der PHP SDK verknüpft und Du hast mit Hilfe eines "billing plan" eine Möglichkeit geschaffen, um Abonnements einzurichten und deren Status abzufragen.
Solltest du Fragen oder Anregungen zum Thema haben, hinterlasse einfach ein Kommentar.
Durch unsere langjährige Arbeit und über 100 erfolgreiche Projekte, konnten wir viele Erfahrungen sammeln. Dieses Know-How im Online-Marketing gaben wir u.a. bei Vorträgen von Google, der Industrie- und Handelskammer und der Handwerkskammer weiter.
Mit Know-How, Kreativität und Leidenschaft entwickeln wir auf unsere Kunden abgestimmte Marketing-Strategien, die Sie sicher und nachhaltig zum Erfolg führen. Gemeinsam setzen wir Ihr Online-Marketing so um, dass Sie langfristig Ihren Umsatz und Return-On-Investment steigern.
Jetzt kostenlosen Beratungstermin vereinbaren oder unter 0561 / 850 194 76 anrufen.