View
Next, we will create a View to fetch data from the database.
Create View Model for List
Run:
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 // 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.
<?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:
php windwalker g route front/article
and edit the generated file to add article-list route:
<?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.
Create Pagination
Pagination requires three numbers:
Number | Purpose | Explanation |
---|---|---|
page or offset | Current page or offset | page is the current page number, usually obtained from the URL. (page - 1) x limit is the offset |
limit | Items per page | limit is directly set in our program, or can be controlled from the URL if needed |
total | Maximum number of current query | total 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:
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.
...
</div>
</div>
</div>
@endforeach
<div class="my-4">
<x-pagination :pagination="$pagination"></x-pagination>
</div>
</div>
</div>
...
The result:
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
// ...
'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 windwalker g view Front/Article/ArticleItemView
to create ArticleItemView
, and then write the code as follows:
// 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.
$item = $this->orm->mustFindOne(Article::class, $id);
$item = $this->orm->findOne(Article::class, $id);
if (!$item) {
throw new RouteNotFoundException();
}
Next, write the blade template.
<?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.
// ...
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
.
@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: