Skip to content

View

Next, we will create a View to fetch data from the database.

Create View Model for List

Run:

shell
php windwalker g view Front/Article/ArticleListView

This will create the ArticleListView object.

INFO

If you enter the short name Front/Article, it will generate Front\Article\ArticleView.

And if you enter the full name Front/Article/ArticleListView, it will generate Front\Article\ArticleListView.

Since we want multiple Views to be managed in the same folder, we enter the full name here.

We will use the ORM object's query builder to fetch articles. Every view object is a ViewModel, that you can load or fetch data in it, and return as view data.

php
<?php // src/Module/Front/Article/ArticleListView.php

#[ViewModel(
    layout: 'article-list',
    js: 'article-list.js'
)]
class ArticleListView implements ViewModelInterface
{
    public function __construct(
        // Inject ORM
        protected ORM $orm
    ) {
        //
    }

    public function prepare(AppContext $app, View $view): array
    {
        $items = $this->orm->from(Article::class)
            ->where('state', 1) // Only fetch state = 1
            ->limit(15)
            ->all(Article::class);

        return compact('items');
    }
}

Here we add a class name: Article::class to all() method as first argument, that well make ORM hydrate every item to Article entity. If no argument provided, a Collection list will be returned.

If you don't want to read items from DB instantly, you may consider replace ->all(Article::class) with ->getIterator(Article::class), the ORM will return an iterator, before you loop it, the ORM will not send any request to DB.

In the views/article-list.blade.php template file, we use foreach to print the articles, wrapped in a bootstrap card.

blade
<?php // src/Module/Front/Article/views/article-list.blade.php
// ...

use App\Entity\Article;

// ...

/**
 * Annotate the type of $items
 * @var $items Article[]
 * @var $item  Article
 */
?>

@extends('global.body')

@section('content')
    <div class="container">
        <div class="row">
            <div class="col-lg-7">

                @foreach ($items as $item)
                    <div class="card mb-4">
                        <div class="card-body">
                            <h2 class="card-title">
                                {{ $item->getTitle() }}
                            </h2>

                            <div class="mb-2 small text-muted">
                                {{-- Convert DB UTC timezone to local timezone --}}
                                {{ $chronos->toLocalFormat($item->getCreated(), 'Y/m/d H:i:s') }}
                            </div>

                            <div>
                                {{-- Truncate the string for summary --}}
                                {!! \Windwalker\str($item->getContent())->stripHtmlTags()->truncate(100, '...') !!}
                            </div>
                        </div>
                    </div>
                @endforeach

            </div>
        </div>
    </div>
@stop

NOTE

Although the view file named blade, this is not render by Laravel Blade engine. Windwalker implemented a blade compatible engine called Edge, which is not depend on any Illumination package and dose not contains any Laravel built-in functions, and can be fully customize for any PHP projects.

Next, create the routing, run:

shell
php windwalker g route front/article

and edit the generated file to add article-list route:

blade
<?php // routes/front/article.route.php

// ...
use App\Module\Front\Article\ArticleListView; 

$router->group('article')
    ->register(function (RouteCreator $router) {
        $router->any('article_list', '/article/list') 
            ->view(ArticleListView::class); 
    });

Now, open the URL http://localhost:8000/article/list

You will see the article list.

sarticle list

Create Pagination

Pagination requires three numbers:

NumberPurposeExplanation
page or offsetCurrent page or offsetpage is the current page number, usually obtained from the URL. (page - 1) x limit is the offset
limitItems per pagelimit is directly set in our program, or can be controlled from the URL if needed
totalMaximum number of current querytotal is used to calculate the number of pages. It can be omitted, resulting in infinite next pages, usually calculated by the DB

We first modify the ArticleListView code as follows:

php
use App\Entity\Article;
use Windwalker\Core\Pagination\PaginationFactory;
use Windwalker\ORM\ORM;

use function Windwalker\filter;

// ...

class ArticleListView implements ViewModelInterface
{
    public function __construct(
        protected ORM $orm,
        protected PaginationFactory $paginationFactory
    ) {
        //
    }

    public function prepare(AppContext $app, View $view): array
    {
        $page = (int) $app->input('page');
        $limit = 5;

        // Restrict the minimum value of page to 1
        $page = filter($page, 'int|range(min=1)');

        $query = $this->orm->from(Article::class)
            ->where('state', 1)
            ->offset(($page - 1) * $limit) // Calculate offset
            ->limit($limit);

        // Create pagination object
        $pagination = $this->paginationFactory->create($page, $limit)
            // Calculate total using ORM::countWith()
            ->total(fn () => $this->orm->countWith($query));

        $items = $query->getIterator(Article::class);

        return compact('items', 'pagination');
    }
}

Then print the pagination in the blade template.

blade
    ...
                            </div>
                        </div>
                    </div>
                @endforeach

                <div class="my-4">
                    <x-pagination :pagination="$pagination"></x-pagination>
                </div>

            </div>
        </div>
    ...

The result:

pagination

We use x-pagination component to print the pagination HTML, you can modify the HTML by editing views/layout/pagination/basic-pagination.blade.php. And you can replace the template file path by editing etc/packages/renderer.php

php
    // ...

        'aliases' => [
            '@pagination' => 'layout.pagination.basic-pagination', 
            '@messages' => 'layout.messages.bs5-messages',
            '@csrf' => 'layout.security.csrf',
        ],

    // ...

Create View Model for Item

Similar to the previous steps, run this command:

php
php windwalker g view Front/Article/ArticleItemView

to create ArticleItemView, and then write the code as follows:

php
// src/Module/Front/Article/ArticleItemView.php

// ...
use App\Entity\Article;
use Windwalker\Core\Router\Exception\RouteNotFoundException;
use Windwalker\ORM\ORM;

// ...

#[ViewModel(
    layout: 'article-item',
    js: 'article-item.js'
)]
class ArticleItemView implements ViewModelInterface
{
    public function __construct(
        protected ORM $orm
    ) {
        //
    }

    public function prepare(AppContext $app, View $view): array
    {
        $id = $app->input('id');

        $item = $this->orm->findOne(Article::class, $id);

        if (!$item) {
            throw new RouteNotFoundException();
        }

        return compact('item');
    }
}

Here we get ID from URL params and find record from DB. If there are no any item found, throw a 404 exception. You may simply replace to mustFinOne() to done this by one line.

php
        $item = $this->orm->mustFindOne(Article::class, $id); 
        $item = $this->orm->findOne(Article::class, $id); 

        if (!$item) { 
            throw new RouteNotFoundException(); 
        } 

Next, write the blade template.

blade
<?php
// src/Module/Front/Article/views/article-item.blade.php

/**
 * @var $item Article
 */
?>

@extends('global.body')

@section('content')
    <div class="container">
        <div class="row justify-content-center">
            <div class="col-lg-8 col-md-10">
                <article class="c-article">
                    <header>
                        <h2>{{ $item->getTitle() }}</h2>
                    </header>

                    <div class="my-2 text-muted">
                        {{ $chronos->toLocalFormat($item->getCreated(), 'Y/m/d H:i:s') }}
                    </div>

                    <div class="c-article__content">
                        {!! $item->getContent() !!}
                    </div>
                </article>
            </div>
        </div>
    </div>
@stop

Then add this view to the route.

php
// ...

use App\Module\Front\Article\ArticleItemView; 
use App\Module\Front\Article\ArticleListView;
use Windwalker\Core\Router\RouteCreator;

/** @var RouteCreator $router */

$router->group('article')
    ->register(function (RouteCreator $router) {
        $router->any('article_list', '/article/list')
            ->view(ArticleListView::class);

        $router->any('article_item', '/article/item/{id}') 
            ->view(ArticleItemView::class); 
    });

Finally, go back to article-list.blade.php and add links to each card to article_item.

blade
@foreach ($items as $item)
    <div class="card mb-4">
        <div class="card-body">
            ...
            
            <div>
                {{-- Truncate the string for summary --}}
                {!! \Windwalker\str($item->getContent())->stripHtmlTags()->truncate(100, '...') !!}
            </div>

            <div class="mt-2">
                <a href="{{ $nav->to('article_item', ['id' => $item->getId()]) }}" class="btn btn-primary">
                    Read More
                </a>
            </div>
        </div>
    </div>
@endforeach

The final result:

list

item

Released under the MIT License.