larablog 建構日誌:依條件顯示文章

larablog 建構日誌:依條件顯示文章

有了部落格單一頁面的製作經驗後,接下來製作其他顯示頁面就更有把握:在 Controller 中建立方法,處理要顯示的資料;建立 Blade template 檔案處理 Controller 傳遞的變數,最後在 routes/web.php 定義存取的路由。

網站首頁:各項資訊匯集處

  首頁是訪客造訪的第一個頁面,從這個頁面了解網站提供的服務,再從此頁面前往網站其他地方。我在首頁部分的製作流程大致如下:

  1. PostController 新增首頁的顯示方法,我取名為:renderHomePage
  2. 按照 renderBlogPage 方法將需要顯示的項目指定為變數,顯示的文章資料是:已發佈、精選狀態為「是」、近期發佈的文章,還需要做分頁。
  3. 依照 blog_page.blade.php 的修改方式逐步將範例內容修改成程式輸出。
  4. 修改 routes/web.php 的首頁路由:從回傳 View 改成 renderHomePage 方法。

新增 renderHomePage 方法

  程式碼部分與 renderBlogPage 近似,文章變數從單篇的 $post 變為 $posts,按前述流程內容使用 Eloquent 語法查詢。

//部落格首頁
    public function renderHomePage(){
        $posts = Post::where('status', 'published')->where('featured', 'yes')->orderBy('created_at', 'desc')->paginate(5);
        $bindings = [
				//分類列表:列出狀態為發佈,精選為「是」的分類項目
            'categories' => Category::where('status', 'published')->where('featured', 'yes')->get(),
				//近期文章:列出狀態為發佈,精選為「是」的項目,以建立時間降冪排序後列出前五項
            'recent_articles' => Post::where('status', 'published')->where('featured', 'yes')->orderBy('created_at', 'desc')->limit(5)->get(),
				//標籤雲:列出狀態為發佈,精選為「是」的標籤項目
            'tags' => Tag::where('status', 'published')->where('featured', 'yes')->get(),
        ];
        return view('home', $bindings, compact('posts'));
    }

首頁 Blade template 修改概要

  首頁主要內容區域的顯示元素與文章全文大致相同,摘要文字部分顯示 introtext 欄位即可,除了原有的「作者名」、「建立時間」及「回應計數」外我增加了「分類名稱」項目,讓首頁顯示不同分類文章時協助訪客辨別。

  回應計數部分因為功能尚未實做維持原樣,連結會連到文章內文頁面中埋設的錨點。顯示文章項目的程式碼如下:

@foreach($posts as $post)
	<article class="entry">
		<div class="entry-img">
			<a href="/blog/{{ $post->slug }}">
				<img src="{{ Voyager::image($post->cover_image) }}" border="0" alt="{{ $post->title }}" class="img-fluid">
			</a>
		</div>

		<h2 class="entry-title">
			<a href="/blog/{{ $post->slug }}">{{ $post->title }}</a>
		</h2>

		<div class="entry-meta">
			<ul>
				<li class="d-flex align-items-center">
					<i class="bi bi-person"></i>{{ $post->user->name }}</a>
				</li>
				<li class="d-flex align-items-center"><i class="bi bi-clock"></i>
					<time datetime="{{ $post->created_at->toDateString() }}">{{ $post->created_at->toDateString() }}</time>
        </li>
				<li class="d-flex align-items-center"><i class="bi bi-folder"></i>
					<a href="/cagegory/{{ $post->category->slug }}">{{ $post->category->name }}</a>
				</li>
				<li class="d-flex align-items-center"><i class="bi bi-chat-dots"></i>
					<a href="/blog/{{ $post->slug }}#comments">12 回應</a>
				</li>
			</ul>
		</div>

		<div class="entry-content">
			<p>{!! $post->introtext !!}</p>
			<div class="read-more">
				<a href="/blog/{{ $post->slug }}">詳細閱讀</a>
			</div>
		</div>
	</article><!-- End blog entry -->
@endforeach

內容分頁

  隨著文章持續發佈,需要設定每頁顯示的文章項目數,並搭配分頁條切換分頁。

  發佈可客製化的分頁 blade 檔案,下列指令輸入後會建立 /resources/views/vendor/pagination 資料夾,裡頭存放的是既有的分頁 Blade template 檔案,可依需要修改。

php artisan vendor:publish --tag=laravel-pagination

  Controller 部分則在原有的查詢結果後加入要分頁顯示的參數,關於文章的查詢程式碼原本是:

$posts = Post::where('status', 'published')->where('featured', 'yes')->orderBy('created_at', 'desc')->get();

  將 get() 更改為 paginate(),其中 paginate 括弧中的數字是每頁的顯示筆數。

$posts = Post::where('status', 'published')->where('featured', 'yes')->orderBy('created_at', 'desc')->paginate(5);

  接著到 app\Providers\AppServiceProvider.php 增加程式碼 use Illuminate\Pagination\Paginator;,在boot() 方法中加入指定分頁要使用的 Blade template 名稱,整個程式碼如下:

// app\Providers\AppServiceProvider.php
<?php

namespace App\Providers;

use App\FormFields\EditorMDHandler;
use Illuminate\Support\ServiceProvider;
use Illuminate\Pagination\Paginator;
use TCG\Voyager\Facades\Voyager;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Register any application services.
     *
     * @return void
     */
    public function register()
    {
        Voyager::addFormField(EditorMDHandler::class);
    }

    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot()
    {
        Paginator::defaultView('pagination::bootstrap-4');
    }
}

  Blade template 中關於分頁的 HTML 碼範例是:

<div class="blog-pagination">
	<ul class="justify-content-center">
			<li><a href="#">1</a></li>
			<li class="active"><a href="#">2</a></li>
			<li><a href="#">3</a></li>
		</ul>
</div>

  現在改成:

<div class="blog-pagination">
	<ul class="justify-content-center">
		{{ $posts->links() }}
	</ul>
</div>

首頁尾端的分頁條

修改 routes/web.php

  原本前往首頁的路由是回傳 View 檔案,現在因應 Controller 的新增改成呼叫 PostControllerrenderHomePage 方法。

Route::get('/', 'App\Http\Controllers\PostController@renderHomePage')->name('home');

Route::get('/blog/{slug}', 'App\Http\Controllers\PostController@renderBlogPage')->name('post');

依分類顯示:單一分類內所有文章

  「依分類顯示」文章列出屬於相同分類的所有文章,評估顯示項目後的頁面長度設定分頁。程式碼與首頁的情況大致相同,會在在網頁路徑處顯示分類描述資訊,文章資訊區塊取消分類名稱。

建立方法

  在 PostController 內建立 renderBlogByCategory 方法,透過參數 $slug 接收類別別名,取得別名後以此找出分類 ID,並以此找尋文章資料表中 category_id 相同的文章項目,以建立時間倒序排列後分頁輸出:每頁五個項目。

public function renderBlogByCategory($slug){
        $category = Category::where('slug', $slug)->first();
        $posts = Post::where('category_id', $category->id)->orderBy('created_at', 'desc')->paginate(5);
        $binding = [
            'categories' => Category::where('status', 'published')->where('featured', 'yes')->get(),
            'tags' => Tag::where('status', 'published')->where('featured', 'yes')->get(),
            'recent_articles' => Post::where('status', 'published')->where('featured', 'yes')->orderBy('created_at', 'desc')->limit(5)->get(),
        ];
        return view('blog_category', $binding, compact('posts'));
    }

建立 Blade template 檔案

  製作呈現 renderBlogByCategory 方法回傳變數的 Blade template 檔案:resources/views/blog_category.blade.php

// resources/views/blog_category.blade.php

@extends('layouts.masterd')
@section('meta_description', $posts->first()->category->description)
@section('title', $posts->first()->category->name)
@section('cover_image', Voyager::image('articles/by_category.png'))
@section('url', url()->full())
@section('content')

<!-- 網頁路徑 -->
<section class="breadcrumbs">
    <div class="container">
        <div class="d-flex justify-content-between align-items-center">
            {{ $posts->first()->category->description }}的文章分類
        </div>
    </div>
</section>
<!-- 網頁路徑 -->

<!-- ======= Blog Single Section ======= -->
<section id="blog" class="blog">
    <div class="container" data-aos="fade-up">
        <div class="row">
            <div class="col-lg-8 entries">
                @foreach($posts as $post)
                <article class="entry">
                    <div class="entry-img">
                        <a href="/blog/{{ $post->slug }}">
                            <img src="{{ Voyager::image($post->cover_image) }}" border="0" alt="{{ $post->title }}" class="img-fluid">
                        </a>
                    </div>

                    <h2 class="entry-title">
                        <a href="/blog/{{ $post->slug }}">{{ $post->title }}</a>
                    </h2>

                    <div class="entry-meta">
                        <ul>
                            <li class="d-flex align-items-center"><i class="bi bi-person"></i> <a
                                    href="blog-single.html">{{ $post->user->name }}</a></li>
                            <li class="d-flex align-items-center"><i class="bi bi-clock"></i> <time
                                    datetime="{{ $post->created_at->toDateString() }}">{{ $post->created_at->toDateString() }}</time>
                            </li>
                            <li class="d-flex align-items-center"><i class="bi bi-chat-dots"></i> <a
                                    href="/blog/{{ $post->slug }}#comments">00 回應</a></li>
                        </ul>
                    </div>

                    <div class="entry-content">
                        <p>
                            {!! $post->introtext !!}
                        </p>
                        <div class="read-more">
                            <a href="/blog/{{ $post->slug }}">詳細閱讀</a>
                        </div>
                    </div>
                </article><!-- End blog entry -->
                @endforeach

                <div class="blog-pagination">
                    <ul class="justify-content-center">
                        {{ $posts->links() }}
                    </ul>
                </div>

            </div><!-- End blog entries list -->

            <!-- 側邊欄 -->
            <div class="col-lg-4">
                <div class="sidebar">
                    <!-- 搜尋表單 -->
                    @include('widgets.search')
                    <!-- 搜尋表單 -->

                    <!-- 文章分類 -->
                    @include('widgets.categories')
                    <!-- 文章分類 -->

                    <!-- 近期文章 -->
                    @include('widgets.recent_posts')
                    <!-- 近期文章 -->

                    <!-- 標籤雲 -->
                    @include('widgets.tags')
                    <!-- 標籤雲 -->
                </div><!-- End sidebar -->
            </div><!-- End blog sidebar -->
            <!-- 側邊欄 -->

        </div><!-- End blog sidebar -->
    </div>
</div>
</section><!-- End Blog Single Section -->
@stop

新增路由

  在 /route/web.php 新增分類瀏覽的路由。

Route::get('/category/{slug}', 'App\Http\Controllers\PostController@renderBlogByCategory');

  測試前端顯示

分類顯示網址:以別名及分類顯示畫面

依標籤顯示:跨分類的文章集合

  如果「列出分類下所有文章」像是「上—下」的縱向結構,那麼「擁有相同標籤的文章項目」就像是「左—」的橫向結構,透過篩選出使用相同標籤的文章項目可以做到跨文章分類的內容顯示。

  在實做這項功能時我卡關卡了很久,因為我不是很清楚 Laravel Eloquent 的語法作業順序。

Builder 與 Collection

  關於這個功能的實做,我的想法是這樣:

  1. 從網址獲得要過濾的標籤別名(slug)。
  2. 以此為關鍵字去找尋有哪些文章有使用這個標籤。
  3. 上述的查詢結果以分頁顯示。

  基於上述內容我在 PostController 建立的 renderBlogByTag 方法程式碼如下:

public function renderBlogByTag($slug){
        $tag = Tag::where('slug', $slug)->first();
        $posts = $tag->posts->sortByDesc('id');
    }

  當我在 sortByDesc('id') 後面加上 →paginate(5) 之後,Laravel 的除錯功能告訴我:不能使用 paginate,用 dd 函式檢視後發現 $posts 的輸出是 Collection(集合)。

  向哥布林老師請教之後才知道 Eloquent 語法查詢分成兩個階段:

  1. 以建構器(Builder)進行查詢,在這個階段才能使用 paginate 函式分頁。
  2. 將查詢結果轉成 Collection 陣列,然後就可以在 Blade template 用 @foreach - @endforeach 迴圈輸出。

  在上面程式碼區塊我把 $posts 指定為 $tags→posts 時就已經成為 Collection,自然無法使用 paginate 函式。所以要怎麼寫才能使查詢語法停留在建構器階段呢?答案是—資料看得不夠多,慚愧—將 →posts 改成 →posts()

  在還不熟悉 PHP 與 Laravel 的使用時常常會遇到問題,以我的情況幾乎是每實做一項功能,就會撞一次牆。思考問題原因後想辦法突破,然後繼續做下一個功能,又撞一次牆...大部分的情況是自己沒弄清楚運作邏輯,有的時候是不清楚有特定函式可以解決,有經驗者能在旁協助真的幫助很大。

  解決問題後的 renderBlogByTag 方法程式碼如下:

public function renderBlogByTag($slug){
        $tag = Tag::where('slug', $slug)->first();
        $posts = $tag->posts()->paginate(5);
        $binding = [
            'filtered_tag' => $tag->name,
            'comments_count' => Comment::where('post_id','$post->id')->count(),
            'categories' => Category::where('status', 'published')->where('featured', 'yes')->get(),
            'tags' => Tag::where('status', 'published')->where('featured', 'yes')->get(),
            'recent_articles' => Post::where('status', 'published')->where('featured', 'yes')->orderBy('created_at', 'desc')->limit(5)->get(),
        ];
        return view('blog_tag', $binding, compact('posts'));
    }

  顯示 renderBlogByTag 傳遞變數的 Blade template 檔案:resoureces/views/blog_tag.blade.php 內容跟首頁 Blade template 檔案:home.blade.php 內容幾乎相同,在頁頭部分則有更多認識:

@extends('layouts.master')
@section('meta_description', '使用標籤:' . $filtered_tag . '的文章')
@section('title', '以標籤篩選:' . $filtered_tag)
@section('cover_image', Voyager::image('articles/by_tag.png'))
@section('url', url()->full())
@section('nav_blog', 'active')
@section('content')

  @section 中的帶入的變數字串值可以用連接運算子連接起來,這樣我就能做出更多變化。(雖然對 Laravel 高手們來說應是不值一提)

  最後在 routes/web.php 加上路由完成整個功能。

Route::get('/tag/{slug}', 'App\Http\Controllers\PostController@renderBlogByTag');

以標籤篩選文章顯示畫面

無側邊欄,僅顯示網站文件的單一頁面

  在部落格文章之外,網站還需要向訪客顯示與網站運作有關的資訊:如服務條款、隱私權規定、會員需知...等制式文件。就我的認知這類文件在畫面呈現上盡可能單一,所以也為此建立單獨的方法及 Blade template 檔案、路由。

  在 Voyager 畫面建立制式文件內容後,接著在 PostController 建立 renderSinglePage 方法。

public function renderSinglePage($slug){
        $post = Post::where('slug', $slug)->first();
        return view('single_article', compact('post'));
    }

  與其相呼應的 Blade template 檔案:resources/views/single_article.blade.php,在頁面呈現上僅顯示文章內容而沒有顯示側邊欄,讓訪客可以專心閱讀頁面...雖然這也表示閱讀體驗會平淡無味。

@extends('layouts.master')
@section('meta_description', $post->introtext)
@section('title', $post->title)
@section('cover_image', Voyager::image($post->cover_image))
@section('url', url()->full())
@section('nav_home', 'active')
@section('content')

<!-- ======= Blog Single Section ======= -->

<section id="blog" class="blog">
    <div class="container" data-aos="fade-up">
        <div class="row">
            <div class="col-lg-12 entries">
                <article class="entry entry-single">
                    <div class="entry-content">
                        {!! \Illuminate\Support\Str::markdown($post->content) !!}
                    </div>
                </article><!-- End blog entry -->
            </div><!-- End blog entries list -->
        </div><!-- End blog sidebar -->
		</div>
</section><!-- End Blog Single Section -->
@stop

  為了讓文件網址容易判讀,路由部分也以網站根目錄的方式輸出

Route::get('/{slug}', 'App\Http\Controllers\PostController@renderSinglePage')->name('single');

顯示網站文件的專屬版型

  到此為止我已經完成網站各主要頁面運作的 Controller 方法,以及呈現的 Blade template 檔案,前往各頁面的路由也設置完成,但是也就是在同一時間,我發現有些頁面去不了...

注意路由順序

  將單一頁面路由加入 routes/web.php 之後,我要前往 Voyager 繼續新增文章,而就在我輸入:網站網址/admin 後看到了以下畫面:

Laravel 提示:方法不存在

  我是要前往 Voyager 畫面,跟 renderSinglePage 方法有什麼關係?就在我納悶兩個不相關的項目為何會湊在一起,而且還呈現除錯畫面時,猛然想到了 PHP 語言的特性:直譯式。

  有讀過計算機概論的人大概都讀過編譯式語言及直譯式語言的介紹,瞭解這兩種語言的特性及優缺點。直譯式語言的優點是更改內容後不需重新編譯,特性是從第一行開始依照順序一行一行執行...

  就是「從第一行開始依照順序一行一行執行」的特性,讓除錯畫面跑出來:我讓單一頁面的路由放在 Voyager 路由的前面,導致當我在網址列輸入 /admin 的時候會先執行單一頁面路由,而忽略之後的 Voyager 路由。

  解決方法也很簡單:將單一頁面路由移到較後面的段落就可以了。以上的例子提醒我在設定路由時要注意先後順序,同時透過設定前綴字區分專案各項功能輸出。

結語

  如果把學習程式的過程比喻為從 0(完全不懂)到 100(大師),我認為從 0 到 1:做出小 side project 會是初學者最大的難關。如果從學校學習的時間開始算,自學程式設計遇到大小問題跨不過去最後放棄,到現在已經過了超過二十年,也就是說我卡關卡了很久...

  現在能使用 Laravel 一步步實做功能出來,除了因為網路發達讓取得資訊的方式遠比過去方便之外;網路拉近了人與人的距離讓我能和更多的人接觸,在困難時有人拉一把,跨越問題的鴻溝。

  現在我已經做到「從 0 到 1」,接下來就是透過不斷學習與實做,讓表示程度的值越來越多。

作者照片

A-Bo Lee

居住在臺灣的 Joomler,期望以程式設計、開放原碼推廣活動收入養活一家老小。
35 歲後改姓李,id 作為曾為郭姓的證明。
FFXI:Abokuo@Sylph鯖、よろしくです。