Agavi 透過 **View 類別命名慣例** 實現內容協商:
`SearchSuccessView` (HTML) → `SearchJsonView` (JSON) → `SearchXmlView` (XML) → `SearchRssView` (RSS)。
客戶端透過 `Accept` 標頭或 URL 後綴 (`.json`, `.xml`, `.rss`) 自動切換。
./bin/agavi action-wizard
# Module: Listing
# Action: Search
# Views: Error Success Json Xml Rss
產生:`SearchAction`、`SearchSuccessView`、`SearchErrorView`、`SearchJsonView`、`SearchXmlView`、`SearchRssView`。
<route name="listing" pattern="^/listing" module="Listing">
<route name=".search" pattern="^/search(\.(?Phtml|json|xml|rss))?$" action="Search" />
</route>
<?php
// app/modules/Listing/actions/SearchAction.class.php
class Listing_SearchAction extends AgaviAction
{
public function execute(AgaviRequestDataHolder $rd)
{
// 1. 取得搜尋參數
$criteria = [
'manufacturer' => $rd->getParameter('manufacturer'),
'model' => $rd->getParameter('model'),
'year_from' => $rd->getParameter('year_from'),
'year_to' => $rd->getParameter('year_to'),
'price_min' => $rd->getParameter('price_min'),
'price_max' => $rd->getParameter('price_max'),
'color' => $rd->getParameter('color'),
'certified' => $rd->getParameter('certified') ? true : null,
];
// 移除空值
$criteria = array_filter($criteria, fn($v) => $v !== '' && $v !== null);
// 2. 查詢資料 (Doctrine)
$em = DoctrineHelper::getEntityManager();
$qb = $em->createQueryBuilder()->select('l')->from('Listing', 'l')
->where('l.displayStatus = 1');
foreach ($criteria as $field => $value) {
$param = 'p_' . $field;
switch ($field) {
case 'year_from': $qb->andWhere("l.vehicleYear >= :$param"); break;
case 'year_to': $qb->andWhere("l.vehicleYear <= :$param"); break;
case 'price_min': $qb->andWhere("l.vehicleSalePriceMin >= :$param"); break;
case 'price_max': $qb->andWhere("l.vehicleSalePriceMax <= :$param"); break;
default: $qb->andWhere("l.$field = :$param");
}
$qb->setParameter($param, $value);
}
$qb->orderBy('l.recordDate', 'DESC');
$results = $qb->getQuery()->getResult();
// 3. 統一設定屬性,所有 View 共用
$this->setAttribute('results', $results);
$this->setAttribute('criteria', $criteria);
$this->setAttribute('count', count($results));
return 'Success'; // 對應 Success/Json/Xml/Rss View
}
public function validate(AgaviValidator $validator)
{
$validator->addRule('year_from', new AgaviIntegerValidator(['min' => 1900, 'max' => date('Y')+1]));
$validator->addRule('year_to', new AgaviIntegerValidator(['min' => 1900, 'max' => date('Y')+1]));
$validator->addRule('price_min', new AgaviNumericValidator(['min' => 0]));
$validator->addRule('price_max', new AgaviNumericValidator(['min' => 0]));
}
}
<?php
// app/modules/Listing/views/SearchSuccessView.class.php
class Listing_SearchSuccessView extends AgaviHtmlView
{
public function setupHtml()
{
$this->setLayout('Master');
}
}
<?php // app/modules/Listing/templates/SearchSuccess.php ?>
<h1>車輛搜尋結果 (<?= $t->getAttribute('count') ?> 筆)</h1>
<form method="GET" action="<?= $ro->gen('listing.search') ?>" class="search-form row g-3">
<div class="col-md-3"><input type="text" name="model" class="form-control" placeholder="車型" value="<?= $t->getAttribute('criteria.model', '') ?>"></div>
<div class="col-md-2"><input type="number" name="year_from" class="form-control" placeholder="年份從" value="<?= $t->getAttribute('criteria.year_from', '') ?>"></div>
<div class="col-md-2"><input type="number" name="year_to" class="form-control" placeholder="年份至" value="<?= $t->getAttribute('criteria.year_to', '') ?>"></div>
<div class="col-md-2"><input type="number" name="price_min" class="form-control" placeholder="最低價" value="<?= $t->getAttribute('criteria.price_min', '') ?>"></div>
<div class="col-md-2"><input type="number" name="price_max" class="form-control" placeholder="最高價" value="<?= $t->getAttribute('criteria.price_max', '') ?>"></div>
<div class="col-md-1"><button type="submit" class="btn btn-primary w-100">搜尋</button></div>
</form>
<div class="table-responsive mt-3">
<table class="table table-striped">
<thead><tr><th>車型</th><th>年份</th><th>顏色</th><th>里程</th><th>價格區間</th><th>認證</th><th>操作</th></tr></thead>
<tbody>
<?php foreach ($t->getAttribute('results') as $listing): ?>
<tr>
<td><?= $listing->getVehicleModel() ?></td>
<td><?= $listing->getVehicleYear() ?></td>
<td><?= $listing->getVehicleColor() ?></td>
<td><?= number_format($listing->getVehicleMileage()) ?> km</td>
<td><?= number_format($listing->getVehicleSalePriceMin()) ?> - <?= number_format($listing->getVehicleSalePriceMax()) ?></td>
<td><?= $listing->getVehicleIsCertified() ? '是' : '否' ?></td>
<td><a href="<?= $ro->gen('listing.display', ['id' => $listing->getRecordId()]) ?>" class="btn btn-sm btn-outline-primary">詳情</a></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php
// app/modules/Listing/views/SearchJsonView.class.php
class Listing_SearchJsonView extends AgaviJsonView
{
// AgaviJsonView 自動將 $this->getAttributes() 以 JSON 輸出
// 可覆寫 getDefaultOutputOptions() 自定義 json_encode 選項
public function getDefaultOutputOptions(): array
{
return JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT;
}
}
輸出範例:
{
"results": [
{
"recordId": 1,
"vehicleModel": "Boxster",
"vehicleYear": 2005,
"vehicleColor": "Yellow",
"vehicleMileage": 15457,
"vehicleSalePriceMin": 35000,
"vehicleSalePriceMax": 40000,
"vehicleIsCertified": true
}
],
"criteria": { "model": "Boxster" },
"count": 1
}
<?php
// app/modules/Listing/views/SearchXmlView.class.php
class Listing_SearchXmlView extends AgaviXmlView
{
public function getDefaultOutputOptions(): array
{
return ['format_output' => true, 'encoding' => 'UTF-8'];
}
// 可選:自定義 XML 結構
public function renderXml(AgaviDomDocument $dom)
{
$root = $dom->createElement('searchResults');
$root->setAttribute('count', $this->getAttribute('count'));
$dom->appendChild($root);
foreach ($this->getAttribute('results') as $listing) {
$item = $dom->createElement('listing');
$item->setAttribute('id', $listing->getRecordId());
foreach (['vehicleModel','vehicleYear','vehicleColor','vehicleMileage','vehicleSalePriceMin','vehicleSalePriceMax','vehicleIsCertified'] as $field) {
$el = $dom->createElement($field, $listing->{'get' . ucfirst($field)}());
$item->appendChild($el);
}
$root->appendChild($item);
}
}
}
<?php
// app/modules/Listing/views/SearchRssView.class.php
class Listing_SearchRssView extends AgaviRssView
{
public function setupRss()
{
$this->setChannelTitle('WASP 車輛搜尋訂閱');
$this->setChannelLink($this->getContext()->getController()->gen('listing.search', [], true));
$this->setChannelDescription('最新符合條件的車輛列表');
$this->setChannelLanguage('zh-TW');
}
public function getRssItems()
{
$items = [];
foreach ($this->getAttribute('results') as $listing) {
$items[] = [
'title' => $listing->getVehicleModel() . ' (' . $listing->getVehicleYear() . ')',
'link' => $this->getContext()->getController()->gen('listing.display', ['id' => $listing->getRecordId()], true),
'description' => "顏色: {$listing->getVehicleColor()}, 里程: {$listing->getVehicleMileage()}km, 價格: {$listing->getVehicleSalePriceMin()} - {$listing->getVehicleSalePriceMax()}",
'pubDate' => $listing->getRecordDate()->format('r'),
'guid' => 'listing-' . $listing->getRecordId(),
];
}
return $items;
}
}
curl -H "Accept: application/json" https://localhost/listing/search?model=Boxster
curl https://localhost/listing/search.json?model=Boxster
curl https://localhost/listing/search.xml?model=Boxster
curl https://localhost/listing/search.rss?model=Boxster
| View 類別 | 基類 | Content-Type | 適用場景 |
|---|---|---|---|
| SuccessView | AgaviHtmlView | text/html | 瀏覽器網頁 |
| JsonView | AgaviJsonView | application/json | AJAX、API、行動端 |
| XmlView | AgaviXmlView | application/xml | 企業整合、舊系統對接 |
| RssView | AgaviRssView | application/rss+xml | 訂閱、聚合器 |