Kommentar- System-Teil-1

Alternatives Kommentarsystem zu Laravel Beauty Teil 1

Posted on October 7, 2015 in laravel, php, tutorial

Inhalt

  • Einführung
  • Datenbank
  • Helper
  • Controller
  • Request
  • SpamPrevention
  • Views
  • CSS/JS

Einführung

Dieses Kommentarsystem wurde von mir als Alternative zu Laravel 5.1 Beauty - Adding Comments, RSS, and a Site Map geschrieben.

Als wichtigsten Grund sah ich den Datenschutz. Meine Daten, bzw. die Daten von Usern sollten nicht in die Hände dritter kommen, was über die Schnittstelle von Disqus zugelassen werden würde.

In diesem Teil wird das Kommentarsystem mit Antwortfunktion und einem Spam-Schutz aufgesetzt ohne weiterer Bearbeitung durch einen Adminbereich. Dieser erfolgt in einem separatem Teil. Die Avatare werden über Gravatareeingebunden. Im Anschluss wird alles über einen Markdown-Editor und etwas Jquery aufgehübscht.

Datenbank

Das Kommentarsystem benötigt 2 Tabellen. Eine für die Kommentare und eine für die Antworten auf die Kommentare. Ein wichtiger Punkt ist, dass es nur 2 Ebenen gibt. Eine Antwort auf eine Antwort ist zwar technisch möglich, wird aber als Antwort auf einen Kommentar und nicht als Antwort auf eine Antwort gesehen.

Durch folgende Artisan-Commands werden die Models, wie auch die Migrations automatisch angelegt und müssen dann nur noch bearbeitet werden.

php artisan make:model --migration Comment
php artisan make:model --migration Reply

Nach dem erfolgreichen Erstellen, bearbeiten wir die Migrations. Da es sich hier um Basics handelt, bedarf es keiner ausführlicheren Beschreibung.

//database/migrations/Datum_Uhrzeit_Create_Comments_Table
<?php

use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateCommentsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('comments', function (Blueprint $table) {
            $table->increments('id');
            $table->integer('user_id'); // UserId for Registered User
            $table->string('user'); // Username for unregistered User
            $table->boolean('approved')->default(0); // Check to Admin
            $table->integer('post_id')->index();
            $table->text('content');
            $table->text('content_html')->after('content');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::drop('comments');
    }
}

<?php
//database/migrations/Datum_Uhrzeit_Create_Replies_Table
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateRepliesTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('replies', function (Blueprint $table) {
            $table->increments('id');
            $table->integer('user_id'); // UserId for Registered User
            $table->string('user'); // Username for unregistered User
            $table->boolean('approved')->default(0); // Check to Admin
            $table->integer('comment_id')->index();
            $table->text('content');
            $table->text('content_html')->after('content');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::drop('replies');
    }
}

Wenn Fragen zu den Migrations sind, kann man diese hier nachlesen.

Um der Datenbank Testdaten zu geben müssen die Factory und die Seeds angepasst um im Anschluss ein Artisan-Command ausgeführt werden.

// edit database/factories/Modelfactory and add this lines to the end

$factory->define(App\Comment::class, function ($faker) {

    return [
        'user' => $faker->name,
        'content' => join("\n\n", $faker->paragraphs(mt_rand(3, 6))),
    ];
});

$factory->define(App\Reply::class, function ($faker) {

    return [
        'user' => $faker->name,
        'content' => join("\n\n", $faker->paragraphs(mt_rand(3, 6))),
    ];
});

Gleich weiter von der Factory zum Seeder.

php artisan make:seeder CommentTableSeeder 
php artisan make:seeder ReplyTableSeeder
//CommentTableSeeder
<?php

use App\Post;
use App\Comment;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;

class CommentTableSeeder extends Seeder
{
    /**
     * Seed the posts table
     */
    public function run()
    {
        $posts = Post::lists('id')->all();
        Comment::truncate();

        factory(Comment::class, 20)->create()->each(function ($c) use ($posts) {
            shuffle($posts);
            $c->post_id = head($posts);
            $c->save();

        });
    }
}

//ReplyTableSeeder
<?php

use App\Reply;
use App\Comment;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;

class ReplyTableSeeder extends Seeder
{
    /**
     * Seed the posts table
     */
    public function run()
    {
        $comments = Comment::lists('id')->all();
        Reply::truncate();

        factory(Reply::class, 20)->create()->each(function ($c) use ($comments) {
            shuffle($comments);
            $c->comment_id = head($comments);
            $c->save();

        });
    }
}

// edit DatabaseSeeder before Model::requard()
$this->call('CommentTableSeeder');
$this->call('ReplyTableSeeder');

Mit den beiden Seeds werden pro Seed 20 Testeinträge für die weitere Bearbeitung erzeugt. Alle Kommentare und Antworten müssen erstmal manuell über die Datenbank freigeschaltet werden, da diese Funktion erst im Adminbereich hinterlegt wird.

Da nun die Tabellenstruktur steht, werden die Models angepasst.

//edit App/Comment

<?php

namespace App;

use App\Services\Markdowner;
use Illuminate\Database\Eloquent\Model;

class Comment extends Model
{
    protected $fillable = [
        'user_id', 'user', 'approved', 'post_id', 'content'
    ];

    /**
     * The has-many relationship between posts and comments.
     *
     * @return BelongsTo
     */
    public function posts()
    {
        return $this->belongsTo('App\Post', 'post_id');
    }

    /**
     * The has-many relationship between comments and replies.
     *
     * @return hasMany
     */
    public function replies()
    {
        return $this->hasMany('App\Reply', 'comment_id');
    }
    /**
     * Set the HTML content automatically when the raw content is set
     *
     * @param string $value
     */
    public function setContentAttribute($value)
    {
        $markdown = new Markdowner();

        $this->attributes['content'] = $value;
        $this->attributes['content_html'] = $markdown->toHTML($value);
    }

}

// Edit App/Reply
<?php

namespace App;

use App\Services\Markdowner;
use Illuminate\Database\Eloquent\Model;

class Reply extends Model
{
    protected $fillable = [
        'user_id', 'user', 'approved', 'comment_id', 'content'
    ];

    /**
     * The has-many relationship between comments and replies.
     *
     * @return BelongsTo
     */
    public function comments()
    {
        return $this->belongsTo('App\Comment');
    }

    /**
     * Set the HTML content automatically when the raw content is set
     *
     * @param string $value
     */
    public function setContentAttribute($value)
    {
        $markdown = new Markdowner();

        $this->attributes['content'] = $value;
        $this->attributes['content_html'] = $markdown->toHTML($value);
    }
}

// Edit App/Post and add this lines after the Tag Function

    /**
     * The has-many relationship between posts and comments.
     *
     * @return hasMany
     */
    public function comments()
    {
        return $this->hasMany('App\Comment');
    }

    /**
     * The has-many-trough relationship between posts, replies and comments.
     *
     * @return hasManyThrough
     */
    public function replies()
    {
        return $this->hasManyThrough('App\Reply', 'App\Comment', 'post_id', 'comment_id');
    }

Die Models Comment und Reply sind sehr ähnlich und einfach gehalten. Weiterführende Informationen sind im Quellcode auskommentiert bzw. beschrieben. Dem Postmodel, wird einfach nur Comment und Reply bekanntgemacht.

Um Beiträge von Comments und Replies sperren zu können, wird eine zusätzliche Funktion benötigt. Die Funktion basiert auf eine neue Spalte in der Post-Tabelle. Diese wird durch eine zusätzliche Migration bearbeitet.

php artisan make:migration add_closed_to_post_table
<?php
// //database/migrations
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class AddClosedToPostTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::table('posts', function (Blueprint $table) {
            $table->boolean('is_closed')->after('layout');
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::table('posts', function (Blueprint $table) {
            $table->dropColumn('is_closed');
        });
    }
}

Da die Migration alles aussagt, wird jetzt noch das Post Model angepasst.

// replace this lines in app/Post

protected $fillable = [
        'title', 'subtitle', 'content_raw', 'page_image', 'meta_description',
        'layout', 'is_draft', 'published_at', 'is_closed'
    ];

Jetzt noch 2 kleine Artisan-, sowie ein Composer-Command und die Datenbankerstellung ist soweit abgeschlossen.

composer dumpauto
php artisan migrate
php artisan db::seed

Helper

Helper sind kleine nützliche Funktionen, die die Arbeit etwas erleichtern sollen. Durch Laravel Beauty wurden bereits einige Funktionen in der App\helpers.php hinterlegt. Das Kommentarsystem benötigt weitere kleinere Helfer und daher wird die Datei angepasst.

// Ans Ende einfügen
/**
 * Give all True Comments / Replies
 * @param Collection $collection
 * @param boolean $bool
 * @return mixed
 */
function approved($collection, $bool)
{
     return $collection->filter(function($item) use ($bool) {
        return $item->approved == $bool;
    });
}

/**
 * Check the Comment of Replies
 * @param Collection $collection
 * @param int $comment_id
 * @param int $replies_id
 * @return bool
 */
function checkReplies($collection, $comment_id, $replies_id)
{
    return $collection->filter(function ($item) use($comment_id) {
        return $item->comment_id == $comment_id;
    })->last()->id == $replies_id;
}

/**
 * Count all Replies
 * @param Collection $collection
 * @param int $comment_id
 * @return mixed
 */

function countReplies($collection, $comment_id)
{
    return $collection->filter(function ($item) use($comment_id) {
        return $item->comment_id == $comment_id;
    })->count();

Die Funktion “approved” gibt uns alle freigeschalteten Kommentare, sowie Antworten auf die Kommentare zurück. checkReplies wird aufgerufen um zu prüfen, ob auf einen Kommentar, eine Antwort vorhanden ist. countReplies gibt die Anzahl an Antworten auf die Kommentare zurück.

Controller

Die Grundlagen sind mit der Tabelle und den Helfern vorhanden. Jetzt benötigt der BlogController eine Anpassung, damit die Kommentare, sowie die Antworten auch angesteuert und für die weitere Bearbeitung genutzt werden können.

// app\Http\Controllers\BlogController.php
// Füge in der Funktion showPost 
//      nach ->whereSlug($slug)
//           ->firstOrFail(); folgendes ein

// Hole alle freigegebenen Kommentare und Antworten zum ausgewählten Artikel
            $post->comments = approved($post->comments, true);
            $post->replies = approved($post->replies, true);

// folgendes ans Ende der Datei einfügen

    /**
     * Insert Comments to Database
     * @param Requests\CommentCreateRequest $request
     * @param Comment $comment
     * @return \Illuminate\Http\RedirectResponse
     */
    public function insertComment(Requests\CommentCreateRequest $request, Comment $comment)
    {
        if($this->dispatch(new SpamPrevention($request)) === false)
        {
            $comment->create($request->input());
        }
        return redirect()->back()->with('success', 'Your comment was created and is waiting to be approved.');
    }

    /**
     * Insert Replies to Database
     * @param Requests\ReplyCreateRequest $request
     * @param Reply $reply
     * @return \Illuminate\Http\RedirectResponse
     */
    public function insertReply(Requests\ReplyCreateRequest $request, Reply $reply)
    {
        if($this->dispatch(new SpamPrevention($request)) === false)
        {
            $reply->create($request->input());
        }
        return redirect()->back()->with('success', 'Your reply was created and is waiting to be approved.');
    }

// folgende uses müssen im Kopfbereich zusätzlich hinterlegt sein

use App\Comment;
use App\Jobs\SpamPrevention;
use App\Reply;

Die Methoden bedürfen keiner weiteren Erklärung, da die Kommentare hier alles wichtige aussagen. Damit dem Blog auch die Actions bekannt gemacht werden, muss die routes.php angepasst werden, da es sonst einen 404 gibt.

//app\Http\routes.php
// nach get('blog/{slug}', 'BlogController@showPost'); folgendes einfügen

post('blog/comment', 'BlogController@insertComment');
post('blog/reply/{comment_id}', 'BlogController@insertReply');

Request

Im Controller werden Userdaten weitergereicht, diese müssen noch zur weiteren Bearbeitung validiert werden. Die Requestdateien übernehmen hier die Arbeit. Als erstes wird artisan zur Erstellung von den Requestdateien genutzt. Im Anschluss werden dann die Dateien angepasst.

php artisan make:request CommentCreateRequest
php artisan make:request ReplyCreateRequest
// app\Http\CommentCreateRequest 
<?php
namespace App\Http\Requests;

class CommentCreateRequest extends Request
{
    /**
     * Determine if the user is authorized to make this request.
     */
    public function authorize()
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     */
    public function rules()
    {
        return [
            'user' => 'required',
            'post_id' => 'required|exists:posts,id',
            'content' => 'required',
        ];
    }
}

CommentCreateRequest benötigt zwingend einen User, sowie auch Inhalt. Die post_id muss zwingend in der Datenbank vorhanden sein.

// app\Http\ReplyCreateRequest
<?php
namespace App\Http\Requests;

class ReplyCreateRequest extends Request
{
    /**
     * Determine if the user is authorized to make this request.
     */
    public function authorize()
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     */
    public function rules()
    {
        return [
            'user' => 'required',
            'comment_id' => 'required|exists:comments,id',
            'content' => 'required',
        ];
    }
}

Ähnlich dem CommentCreateRequest ist auch dem ReplyCreateRequest. Hier wird lediglich statt der post_id die comment_id auf Existenz geprüft, sonst bleibt alles wie beim CommentCreateRequest.

SpamPrevention

Ein häufiges Problem ist, was auch bei Laravel Beauty angesprochen wird, dass User / Robots viel Spam verbreiten. Das soll natürlich auf ein Minimum reduziert werden. Einen 100%igen Schutz gibt es natürlich nicht. Die ersten Maßnahmen sind bereits im Controller hinterlegt worden. Damit der Spamschutz funktionieren kann, wird eine Config- und ein Job benötigt. Diese werden nun angelegt und erörtert.

php artisan make:job SpamPrevention
<?php

namespace App\Jobs;

use App\Http\Requests\Request;
use App\Jobs\Job;
use Carbon\Carbon;
use Illuminate\Contracts\Bus\SelfHandling;

class SpamPrevention extends Job implements SelfHandling
{
    private $input;
    private $factor    = 0;
    private $functions = [];
    /**
     * Create a new job instance.
     *
     * @return void
     */
    public function __construct(Request $request)
    {
        $this->input     = $request;
        $this->functions = config('blog.spamPreventionFunctions');
    }

    /**
     * Execute the job.
     *
     * @return void
     */
    public function handle()
    {
        if(config('blog.enable') === true)
        {
            foreach($this->functions as $value)
            {
                if(call_user_func([$this, 'check' . $value]) === true)
                {
                    $this->_buildFactor($value);
                }
            }
        }
        return ($this->_checkFactor() >= config('blog.spamfactor'));
    }

    /**
     * Check EmptyValue
     * if the Hiddenfield is not empty than true -> Spam
     * @return bool
     */

    public function checkEmptyFiled()
    {
        if($this->input->{config('blog.emptyField')} == '')
        {
            return false;
        }
        return true;
    }

    /**
     * Check Session Values
     * Is Session Empty or the Sessionvalue time is not set, than true --> Spam
     * FloodProtect --> Check Formtime with Config Values --> to fast --> true or to slow --> spam
     * @return bool
     */

    public function checkSessionTime()
    {

        if(session()->has('time') === false)
        {
            return true;
        }

        $diff = $this->input->session()->get('time')->diffInSeconds(Carbon::now());
        if($diff < config('blog.expireMin') && $this->input->session()->get('flood') === true)
        {
            return true;
        }
        else if($diff > config('blog.expireMax'))
        {
            return true;
        }
        else
        {
            $this->input->session()->put('time', Carbon::now());
            $this->input->session()->put('flood', true);
            return false;
        }
    }

    /**
     * Check Links in Content
     * Linkscount higher param in Config than true --> Spam
     * @return bool
     */

    public function checkLinkCounter()
    {
        $count = 0;
        foreach(config('blog.linkCounterParams') as $value)
        {
            $count += substr_count(strtolower($this->input->get('content')), $value);
        }

        return ($count > config('blog.linkCounterMax'));
    }

    /**
     * Check Fieldname with Content
     * Same Content and Fieldname than true --> Spam
     * @return bool
     */
    public function checkFieldContent()
    {
        foreach($this->input->input() as $key => $value)
        {
            if($key == trim(strtolower($value)))
            {
                return true;
            }
        }
        return false;
    }

    /**
     * Same Content on every Field, than true --> Spam
     * @return bool
     */
    public function checkUniqueContent()
    {
        $array = [];
        foreach($this->input->input() as $key => $value)
        {

            if(in_array(trim(strtolower($value)), $array))
            {
                return true;
            }
            $array[] = trim(strtolower($value));
        }
        return false;
    }

    /**
     * Check the fields of blacklistvalues
     * @return bool
     */
    public function checkBlackListString()
    {
        $stringBuild = implode(' ', $this->input->input());

        foreach(config('blog.blackListValueString') as $value)
        {
            preg_match("/".$value."/", strtolower($stringBuild), $matches);

            if(count($matches) > 0)
            {
                return true;
            }
        }
        return false;
    }

    /**
     * check the ip for blacklist
     * @return bool
     */

    public function checkBlackListIp()
    {
        $ip = $this->input->instance()->getClientIp();


        if(config('blog.blackListIpScanMethod') == 'i' || config('blog.blackListIpScanMethod') == 'd')
        {
            $spam = $this->_appBlackList($ip);
            if($spam === true)
            {
                return true;
            }
        }
        if(config('blog.blackListIpScanMethod') == 'e' || config('blog.blackListIpScanMethod') == 'd')
        {
            $spam = $this->_dnsLookUp($ip);
            if($spam === true)
            {
                return true;
            }
        }
        return false;
    }

    /**
     * check the ip vs extern blacklistings
     * @param $ip
     * @return bool
     */

    private function _dnsLookUp($ip)
    {
        $reverse_ip = implode(".",array_reverse(explode(".",$ip)));
        foreach(config('blog.blackListLookUp') as $value)
        {
            if(checkdnsrr($reverse_ip.".".$value.".","A"))
            {
                return true;
            }
        }
        return false;
    }

    /**
     * solit the range / wildcard and build the ip check intern
     * @param $ip
     * @return bool
     */

    private function _appBlackList($ip)
    {
        foreach(config('blog.blackListValueIp') as $value)
        {

            if(strpos($value, '-'))
            {
                $range = explode('-', $value);

                if(trim($range[0]) < $ip && trim($range[1]) > $ip)
                {
                    return true;
                }
            }
            if(strpos($value, '*'))
            {
                if(stristr($ip, substr($value, 0, strpos($value, '*')), true) !== false)
                {
                    return true;
                }
            }
            if($value == $ip)
            {
                return true;
            }
            return false;
        }
    }

    /**
     * build the factorvalue
     * @param $name
     */
    private function _buildFactor($name)
    {
        $this->factor += config('blog.factor' . $name);
    }

    /**
     * check the factor
     * @return float
     */

    private function _checkFactor()
    {
        return ((-1 / ($this->factor + 1)) *100 >= config('blog.spamfactor'));
    }
}

Durch diesen Schutz werden die Kommentare / Antworten nicht eingestellt und sofort abgefangen. Sie werden auch nicht gespeichert. Der User / Robot bekommt dennoch eine Success Meldung. Mit ein paar kleineren Handgriffen kann dieses auch geändert werden, so das der Beitrag trotzdem erstellt wird und manuell nachgearbeitet werden kann. Dazu müssen im BlogController kleinere Änderungen vorgenommen werden, worauf aber in diesen beiden Teilen erstmal nicht eingegangen wird.

Zur Erläuterung:

    private $input;
    private $factor    = 0;
    private $functions = [];

Die Properties speichern einmal den Inhalt des Kommentars, sowie der Antwort. Der Factor speichert den Spamwert des Kommentars. Die Functions-Property erhält alle Funktionen, die zur Spamerkennung genutzt werden sollen, ausgelesen aus der Config.

Die handle Methode ist das Kennstück. Hier wird geprüft, ob die Bedinung erfüllt ist und im Erfolgsfall der Factor addiert. Im Anschluss wird dann geprüft ob der errechnete Wert den maximalen Wert übersteigt.

In der checkEmptyFiled, wird ein Feld geprüft, dass immer leer sein muss. In der Configdatei kann dieses angepasst werden.

Bei checkSessionTime wird auf Flood geprüft. Ist die Session abgelaufen, gibts einen Fehler, genauso wie bei zu schnellem absenden des Formulares.

checkLinkCounter zählt alle Links im Kommentar und vergleicht diese mit der Configdatei.

In checkFieldContent wird der Inhalt mit der Feldnamen geprüft, sollte diese Inhalt gleich sein, wirds als Spam gewertet und der Faktor erhöht sich.

Bei checkUniqueContent wird geprüft, ob der Inhalt verschieden ist und nicht in jedem Feld der gleiche Wert hinterlegt ist.

Bei den Blacklistfunktionen ist es so, dass die Inhalte auf die Werte in der Configdatei geprüft werden. Bei checkBlackListString wird sobald ein Wort vorhanden ist, sofort mit Spamverdacht abgebrochen. BlackListIp ist eine sehr komplexe und gefährliche Methode und sollte auch nur bedacht eingesetzt werden. Es gibt drei Möglichkeiten hier zu prüfen. Intern, Extern, Double. Bei der Internen Methode wird auf Ip’s geprüft, die in der Configdatei hinterlegt sind. Die Funktion _appBlackList prüft hier auch auf Wildcards () Extern prüft die vorhandene Ip gegen die Server in der Configmethode ab, sollte dort eine Ip gelistet sein, wird es sofort abgelehnt. Dafür ist die Funktion *_dnsLookUp zuständig. Bei double, kommen beide Prüfungen zustande.

Die restlichen beiden Funktionen _buildFactor und _checkFactor berechnen und prüfen auf Spam.

// config/blog.php
// ans Ende , aber vor ]; einfügen

    // SpamPrevention

    'enable'                    => true,

    // Ab wann ist der Kommentar als Spam zu werten
    'spamfactor'                => '50',

    // FieldProtection --> EmptyField
    'factorEmptyFiled'          => '4',
    'emptyField'                => 'url',

    // SessionCheck
    'factorSessionTime'         => 5,
    'expireMax'                 => 86400,
    'expireMin'                 => 5,

    // LinkCheck
    'factorLinkCounter'         => 3,
    'linkCounterMax'            => 1,
    'linkCounterParams'         => [
        'http', 'ftp',
    ],

    // Unickeck
    'factorUniqueContent'       => 1,

    // FieldCheck
    'factorFieldContent'        => 4,

    // Blacklisting String
    'factorBlackListString'     => 8,
    'blackListValueString'      => [
      'sex', 'porn', 'viagra', 'Krankenkasse'
    ],

    // Blacklisting Ips
    'factorBlackListIp'         => 8,
    'blackListIpScanMethod'     => 'i', // i = intern , e = extern , d = double

    // Attention -> too many Servers, can make a false alert
    'blackListLookUp'           => [
        'zen.spamhaus.org',
        'dnsbl-1.uceprotect.net',
    ],
    'blackListValueIp'          => [
        '128.*'
    ],


    // All Spam Prevention Functions
    'spamPreventionFunctions'   => [
         'SessionTime',
         'EmptyFiled',
         'LinkCounter',
         'FieldContent',
         'UniqueContent',
         'BlackListString',
         'BlackListIp'
    ],

Die vorgeschlagenenen Werte sind Richtwerte und müssen nicht übernommen werden. Welche und wie Funktionen genutzt werden, kann selber entschieden werden. Die Besten Ergebnisse bekommt man, wenn man es auf “Herz und Nieren” prüft.

Views

In den letzten Schritten müssen die views und das Design angepasst werden. Dafür wird folgende Ordnerstruktur angelegt.

resources/views/blog/comments
--> _form.blade.php
--> show.blade.php
resources/views/blog/reply
--> _form.blade.php

Die Views, werden nicht weiter kommentiert, da dieses selbsterklärend sein sollte.

//comments/show
@if ($post->comments->count())
    @foreach($post->comments as $key => $value)
            <div class="col-lg-10 col-lg-offset-1 col-md-10 col-md-offset-1">
                <div class="comments">
                    <div class="active item">

                        <div class="carousel-info">
                            <img alt="avatar" src="https://www.gravatar.com/avatar/@if(Auth::check() && $value->user == Auth::user()->name ){!! md5(strtolower(Auth::user()->email)) !!}"
                            @else " @endif class="pull-left">
                            <div class="pull-left">
                                <span class="comments-name">{{ $value->user }}</span>
                                <span class="comments-post">{{ Carbon\Carbon::parse($value->created_at)->format('l, F j, Y') }}</span>
                            </div>
                        </div>
                        <blockquote><p>{!! $value->content_html !!}</p></blockquote>
                    </div>
                    @if(countReplies($post->replies, $value->id) == 0 && $post->is_closed == 0)
                        <button class="btn btn-sm" data-toggle="btn_reply"> Reply </button>
                        @include('blog.reply._form')
                    @endif
                    @if($post->comments->count() == $key + 1)
                        @if($post->is_closed == 0)
                            <button class="btn btn-sm" data-toggle="btn_comment"> Comment </button>
                            @include('blog.comments._form')
                        @else
                            <blockquote>Comments are blocked.</blockquote>
                        @endif
                    @endif
                </div>
            </div>
        @if($post->replies->count() > 0)
            @foreach($post->replies as $k => $val)
                @if($val->comment_id === $value->id)
                    <div class="col-lg-10 col-lg-offset-2 col-md-10 col-md-offset-2">

                        <div class="comments">
                            <div class="active item">
                                <div class="carousel-info">
                                    <img alt="avatar" src="https://www.gravatar.com/avatar/@if(Auth::check() && $value->user == Auth::user()->name){!! md5(strtolower(Auth::user()->email)) !!}"
                                    @else "@endif class="pull-left">
                                    <div class="pull-left">
                                        <span class="comments-name">{{ $val->user }}</span>
                                        <span class="comments-post">{{ $val->created_at->format('l, F j, Y') }}</span>
                                    </div>
                                </div>
                                <blockquote><p>{!! $val->content_html !!}</p></blockquote>
                            </div>
                            @if(checkReplies($post->replies, $val->comment_id, $val->id) )
                                @if($post->is_closed == 0)
                                    <button class="btn btn-sm" data-toggle="btn_reply"> Reply </button>
                                    @include('blog.reply._form')
                                @else
                                    <blockquote>Replies are blocked.</blockquote>
                                @endif
                            @endif
                        </div>

                    </div>
                @endif
            @endforeach
        @endif
    @endforeach
@else
    <div class="col-lg-10 col-lg-offset-1 col-md-10 col-md-offset-1">
        <div class="comments">

            @if($post->is_closed == 0)
                <div class="active item">
                    <blockquote>This article has no comments. You can leave the first comment.</blockquote>
                </div>
                <button class="btn btn-sm" data-toggle="btn_comment"> Comment </button>
                @include('blog.comments._form')
            @else
                <div class="active item">
                    <blockquote>This article has no comments. Comments are blocked.</blockquote>
                </div>
            @endif

        </div>

    </div>
@endif

//comments/_form <form class="form-horizontal" role="form" method="POST" action="/blog/comment" data-toggle="form-comment"> <input type="hidden" name="_token" value="{{ csrf_token() }}"> <input type="hidden" name="post_id" value="{{ $post->id }}"> @if(Auth::check()) <input type="hidden" name="user_id" value="{{ Auth::user()->id }}"> @endif <div id="comment_form"> <div> <input type="text" name="{{ config('blog.emptyField') }}" id="{{ config('blog.emptyField') }}" value=""> </div> <div> <input type="text" name="user" id="name" value=@if(Auth::check()) "{{ Auth::user()->name }}" readonly @else "" @endif placeholder="Name"> </div> <div> <textarea rows="4" name="content" id="comment" placeholder="Comment" data-provide="markdown" data-hidden-buttons="cmdPreview" ></textarea> </div> <div> <input type="submit" name="submit" value="Add Comment"> </div> </div> </form>

//reply/_form <form class="form-horizontal" role="form" method="POST" action="/blog/reply/{{ $value->id }}" data-toggle="form-reply" > <input type="hidden" name="_token" value="{{ csrf_token() }}"> <input type="hidden" name="comment_id" value="{{ $value->id }}"> @if(Auth::check()) <input type="hidden" name="user_id" value="{{ Auth::user()->id }}"> @endif <div id="comment_form"> <div> <input type="text" name="user" id="name" value=@if(Auth::check()) "{{ Auth::user()->name }}" readonly @else "" @endif placeholder="Name"> </div> <div> <textarea rows="4" name="content" id="reply" placeholder="Reply" data-provide="markdown" data-hidden-buttons="cmdPreview"></textarea> </div> <div> <input type="submit" name="submit" value="Add Reply"> </div> </div> </form>

Durch die Struktur und Dateien ist nun die Basis für die Kommentare und die Antwortebene geschaffen. Zum jetztigen Zeitpunkt wird aber noch kein Kommentar angelegt. Dafür muss noch eine Datei leicht angepasst werden.

// view/blog/layouts/post.blade.php
// Nach folgendem Block
// </span>
//                        {!! $post->content_html !!}
//                    </div>
// folgendes in einer separaten Zeile einfügen

@include('blog.comments.show')

CSS/JS

Die freigeschalteten Kommentare werden nun dargestellt. Allerdings sehr unfreundlich. Das wird durch ein bisschen LESS geändert.

// resources/assets/less/blog.less
// ans Ende einfügen

#comment_form input, #comment_form textarea {
  font-size: 10px;
  border: 2px solid rgba(0,0,0,0.1);
  padding: 2px 4px;

  -webkit-border-radius: 5px;
  -moz-border-radius: 5px;
  border-radius: 5px;

  outline: 0;
}

#comment_form input {
  margin: 8px 0;
}

#comment_form #url {
  display: none;
}

#comment_form textarea {
  margin-top: 8px;
  width: 350px;
  height: 150px;
}

#comment_form input[type="submit"] {
  cursor: pointer;
  background: -webkit-linear-gradient(top, #efefef, #ddd);
  background: -moz-linear-gradient(top, #efefef, #ddd);
  background: -ms-linear-gradient(top, #efefef, #ddd);
  background: -o-linear-gradient(top, #efefef, #ddd);
  background: linear-gradient(top, #efefef, #ddd);
  color: #333;
  text-shadow: 0px 1px 1px rgba(255,255,255,1);
  border: 1px solid #ccc;
}

#comment_form input[type="submit"]:hover {
  background: -webkit-linear-gradient(top, #eee, #ccc);
  background: -moz-linear-gradient(top, #eee, #ccc);
  background: -ms-linear-gradient(top, #eee, #ccc);
  background: -o-linear-gradient(top, #eee, #ccc);
  background: linear-gradient(top, #eee, #ccc);
  border: 1px solid #bbb;
}

#comment_form input[type="submit"]:active {
  background: -webkit-linear-gradient(top, #ddd, #aaa);
  background: -moz-linear-gradient(top, #ddd, #aaa);
  background: -ms-linear-gradient(top, #ddd, #aaa);
  background: -o-linear-gradient(top, #ddd, #aaa);
  background: linear-gradient(top, #ddd, #aaa);
  border: 1px solid #999;
}

.sidebar {
  .box-shadow(1px 1px 1px);
  padding: 0 1% 1% 5%;
  background-color: #f1f1f1;
  > header > h3 {
    text-align: center;
  }
  .tags {
    text-align: center;
    font-weight: bold;
    padding: 0 0 10px 0;
    span {
      .font-size(1.2);
    }
    .weight-1 {
      .font-size(1.5)
    }
    .weight-2 {
      .font-size(1.8)
    }
    .weight-3 {
      .font-size(2.1)
    }
    .weight-4 {
      .font-size(2.4)
    }
    .weight-5 {
      .font-size(2.7)
    }
  }
  .lastcomment {
    height: 100px;
    padding: 0 4% 0 0;
    margin-top: 1px;
    p {
      margin: 0;
      .font-size(1.4);
      .text-justify;
    }
    span {
      display: block;
      .font-size(1.2);
    }
    .small {
      padding: 0;
      .font-size(1.2);
      span {
        .text-danger
      }
    }
    &:last-child {
      border-bottom: none;
    }
  }
}

// Markdown Editor
.md-editor {

  .md-header {
    width: 350px;
  }
  .btn-group {
    float: none;
  }

  .btn {
    border: 0;
    background: none;
    color: #b3b3b3;
    padding: 2px 8.5px;

    &:hover,
    &:focus,
    &.active,
    &:active {
      box-shadow: none;
      color: #333;
    }
  }
}

Auf das less, folgt nun noch ein bisschen jquery um alles etwas dynamisch zu machen.

// resources/assets/js/blog.js
// ans Ende einfügen vor });

    // Initialize tooltips
    $('[data-toggle="tooltip"]').tooltip();

    // Comments
    $('[data-toggle^="form"]').hide();

    $('[data-toggle="btn_comment"]').click(function()
    {
        $(this).parent().children('[data-toggle="form-comment"]').toggle('slow');
    });

    $('[data-toggle="btn_reply"]').click(function()
    {
        $(this).parent().children('[data-toggle="form-reply"]').toggle('slow');
    });

Durch die Änderungen an der blog.less und blog.js ist nun ein schönes schlichtes Design entstanden, was durch den folgenden Command ausgelöst wird.

gulp

Als kleineren Abschluss wird nun der Markdown-Editor eingefügt. Dieser wird über Direct-Links eingebunden. Einen für CSS und den anderen für JS:

// resources/views/layouts/master.blade.php
// Nach {{-- Styles --}}
// Folgendes einfügen
<link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-markdown/2.9.0/css/bootstrap-markdown.min.css" rel="stylesheet">

// Vor @yield('scripts')
// Folgendes einfügen

<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-markdown/2.9.0/js/bootstrap-markdown.min.js"></script>

Das Kommentar- und Antwortsystem ist nun soweit, dass es ordentlich genutzt werden kann. Allerdings muss alles noch manuell über die Datenbank freigeschaltet werden. Dafür wird in den Spalten der “approved” Wert von 0 auf 1 gesetzt und schon werden die entsprechenen Einträge angezeigt. Durch den 2. Teil wird ein Adminbereich hinterlegt, womit die Freischaltung und Löschung der Kommentare / Antworten vereinfacht wird.

Für Anregungen oder Kritik schreibt einfach mir einfach einen Kommentar.

Viele Grüße

_ Cyrix _

This article has no comments. You can leave the first comment.