X-Cart 5 Checkout 添加 Preferred Delivery Date 字段
结论
不要只在 Checkout 模板中添加一个 <input>。HTML 字段只能收集数据,X-Cart 不会自动把任意请求参数写入 Order。
完整实现需要处理四个部分:
- 扩展
Order模型并创建数据库字段。 - 把日期输入框加入 Checkout 的 Review 区域,确保它位于实际提交的表单内。
- 在执行
place_order前读取并校验请求参数,再写入当前 Cart/Order。 - 扩展 invoice 模板和后台订单列表,读取并显示这个属性。
购物车本身使用订单模型保存数据,所以日期必须在下单动作完成前写入当前 Cart。下单完成后,可以通过订单实体的 getter 读取。
以下代码适用于使用 Doctrine annotation 和 Twig 的 X-Cart 5。不同小版本的命名空间、模板目录和 View List 名称可能不同,请以当前安装版本中的 Customer notes、Checkout controller 和 Order ItemsList 实现为准。
1. 扩展 Order 模型
这项功能涉及模型、控制器和后台列表,不只是模板样式调整,因此更适合放在独立业务模块中,而不是仅通过 CustomSkin 覆盖模板。
<?php
namespace XLite\Module\YourVendor\DeliveryDate\Model;
/**
* @MappedSuperClass
*/
abstract class Order extends \XLite\Model\Order implements \XLite\Base\IDecorator
{
/**
* @Column(type="date", nullable=true)
*
* @var \DateTime|null
*/
protected $preferredDeliveryDate;
/**
* @return \DateTime|null
*/
public function getPreferredDeliveryDate()
{
return $this->preferredDeliveryDate;
}
/**
* @param \DateTime|null $date
*
* @return $this
*/
public function setPreferredDeliveryDate(\DateTime $date = null)
{
$this->preferredDeliveryDate = $date;
return $this;
}
}
如果只需要保存日期,不需要时间,数据库类型应使用 date,不要使用 datetime。这样可以避免时区转换造成日期前后偏移。
添加属性后,需要执行当前 X-Cart 版本对应的缓存重建和数据库 schema 更新流程。只清除模板缓存不会创建数据库列。
如果当前版本已经使用 YAML/XML 实体映射或新版模块结构,应按照该版本的实体扩展方式定义字段,不要同时使用两套映射机制。
2. 在 Checkout Review 区域加入输入框
字段必须放在 Place order 按钮所属的实际 <form> 中。示例模板如下:
<div class="preferred-delivery-date">
<label for="preferred_delivery_date">
{{ t('Preferred delivery date') }}
</label>
<input
type="date"
id="preferred_delivery_date"
name="preferred_delivery_date"
value="{% if this.getCart().getPreferredDeliveryDate() %}{{ this.getCart().getPreferredDeliveryDate()|date('Y-m-d') }}{% endif %}"
/>
</div>
应通过 View List 把模板加入 Review/Place order 区域,不要直接修改核心模板。不同 X-Cart 5 版本使用的 View List 名称可能不同,可以按下面的方式确认:
- 找到当前版本用于渲染
Customer notes的 widget 或模板。 - 查看其中使用的
@ListChild,或者模板里的list名称。 - 将日期字段注册到同一个列表,并调整
weight,让它出现在 Place order 按钮之前。
下面是示意代码,其中的列表名称必须替换为当前版本中实际存在的名称:
<?php
namespace XLite\Module\YourVendor\DeliveryDate\View\Checkout;
/**
* @ListChild(
* list="checkout.review.selected.placeOrder",
* zone="customer",
* weight="90"
* )
*/
class PreferredDeliveryDate extends \XLite\View\AView
{
protected function getDefaultTemplate()
{
return 'modules/YourVendor/DeliveryDate/checkout/preferred_delivery_date.twig';
}
}
字段的显示位置不是判断实现是否正确的唯一依据,还要确认 Place order 请求确实提交了它。可以在浏览器开发者工具的 Network 面板中查看下单请求,确认请求参数包含:
preferred_delivery_date=2026-09-15
如果请求中没有这个参数,通常说明字段不在提交表单内,或者当前版本的 Checkout JavaScript 只会序列化特定容器。此时需要把字段移入对应表单,或者使用与 Customer notes 相同的 AJAX 保存机制。
3. 在 Place order 前保存日期
扩展 Checkout controller,在父类真正创建订单之前读取并保存日期:
<?php
namespace XLite\Module\YourVendor\DeliveryDate\Controller\Customer;
abstract class Checkout extends \XLite\Controller\Customer\Checkout implements \XLite\Base\IDecorator
{
protected function doActionPlaceOrder()
{
$request = \XLite\Core\Request::getInstance();
$rawDate = trim((string) $request->preferred_delivery_date);
$date = $this->parsePreferredDeliveryDate($rawDate);
if ($rawDate !== '' && $date === null) {
\XLite\Core\TopMessage::addError(
static::t('Please enter a valid preferred delivery date')
);
return;
}
if ($date !== null) {
$today = new \DateTime('today');
if ($date < $today) {
\XLite\Core\TopMessage::addError(
static::t('Preferred delivery date cannot be in the past')
);
return;
}
}
$this->getCart()->setPreferredDeliveryDate($date);
parent::doActionPlaceOrder();
}
/**
* @param string $value
*
* @return \DateTime|null
*/
protected function parsePreferredDeliveryDate($value)
{
if ($value === '') {
return null;
}
$date = \DateTime::createFromFormat('!Y-m-d', $value);
$errors = \DateTime::getLastErrors();
if (
!$date
|| (
is_array($errors)
&& ($errors['warning_count'] > 0 || $errors['error_count'] > 0)
)
|| $date->format('Y-m-d') !== $value
) {
return null;
}
return $date;
}
}
这里要先调用 setter,再调用父类的 doActionPlaceOrder()。当前 Cart 通常会在父类流程中被确认并持久化为订单。如果等父方法执行结束后再写入,原来的购物车对象可能已经被清理或替换。
一般不需要单独执行 SQL。当前 Cart/Order 是由 Doctrine 管理的实体,正常的下单事务会保存它。不过,仍应根据当前版本的下单流程确认这一行为。如果该版本在调用父方法前不会自动 flush,可以在写入后显式调用:
\XLite\Core\Database::getEM()->persist($this->getCart());
不要为了确保保存而随意在下单事务中提前调用 flush(),否则可能破坏 X-Cart 原有的事务处理和错误回滚流程。
如果日期为必填项,还要在服务器端拒绝空值,不能只依赖 HTML 的 required:
if ($rawDate === '') {
\XLite\Core\TopMessage::addError(
static::t('Preferred delivery date is required')
);
return;
}
4. 下单后访问日期
取得订单实体后,可以直接调用:
$date = $order->getPreferredDeliveryDate();
if ($date !== null) {
$formattedDate = $date->format('Y-m-d');
}
不要直接访问:
$order->preferredDeliveryDate
这个属性通常是 protected,模板和其他模块应通过 getter 读取。
如果需要根据订单编号加载订单,可以这样写:
$order = \XLite\Core\Database::getRepo('XLite\Model\Order')
->findOneByOrderNumber($orderNumber);
if ($order && $order->getPreferredDeliveryDate()) {
$formattedDate = $order
->getPreferredDeliveryDate()
->format('Y-m-d');
}
5. 在 invoice 页面显示
通过 invoice 使用的 View List 添加模板片段,不要直接修改核心 invoice 模板。
Twig 示例:
{% if this.getOrder().getPreferredDeliveryDate() %}
<div class="preferred-delivery-date">
<strong>{{ t('Preferred delivery date') }}:</strong>
{{ this.getOrder().getPreferredDeliveryDate()|date('Y-m-d') }}
</div>
{% endif %}
部分版本会直接向 invoice 模板提供 order 变量,此时可以写成:
{% if order.getPreferredDeliveryDate() %}
<div class="preferred-delivery-date">
<strong>{{ t('Preferred delivery date') }}:</strong>
{{ order.getPreferredDeliveryDate()|date('Y-m-d') }}
</div>
{% endif %}
先查看附近模板如何访问订单,再使用相同的变量。还要确认 invoice 是否包含网页、邮件和 PDF 三条不同的渲染路径。如果它们使用不同的 View List,就需要分别注册模板。
6. 在后台 Order list 添加列
后台订单列表通常由 \XLite\View\ItemsList\Model\Order\Admin\Search 或当前版本对应的类生成。可以装饰该 ItemsList,并添加一列:
<?php
namespace XLite\Module\YourVendor\DeliveryDate\View\ItemsList\Model\Order\Admin;
abstract class Search extends \XLite\View\ItemsList\Model\Order\Admin\Search implements \XLite\Base\IDecorator
{
protected function defineColumns()
{
$columns = parent::defineColumns();
$columns['preferredDeliveryDate'] = [
static::COLUMN_NAME => static::t('Preferred delivery date'),
static::COLUMN_ORDERBY => 500,
static::COLUMN_TEMPLATE =>
'modules/YourVendor/DeliveryDate/order/preferred_delivery_date.twig',
];
return $columns;
}
}
列模板示例:
{% if this.entity.getPreferredDeliveryDate() %}
{{ this.entity.getPreferredDeliveryDate()|date('Y-m-d') }}
{% else %}
—
{% endif %}
模板上下文使用 this.entity、this.getEntity() 还是其他变量,取决于当前版本的 ItemsList。可以参考同一订单列表中已有的自定义模板列。
如果只需要在后台订单详情页显示日期,不必加入列表页。扩展订单详情页的 View List 更简单,也不会增加列表查询和渲染负担。
7. 常见问题
输入框显示了,但数据库中没有值
优先检查以下几项:
- Place order 请求是否真的包含
preferred_delivery_date。 - 扩展 controller 的
doActionPlaceOrder()是否被调用。 - getter 和 setter 是否属于实际生效的 Order decorator。
- 数据库是否已经创建对应的列。
- 模块缓存是否已经完成重建。
- 父类下单前是否发生了验证失败或页面重载。
日期在提交后丢失
Checkout 可能会在地址、运费或支付方式发生变化后重新渲染。如果希望字段在这些刷新过程中继续保留,就不能只在最终 Place order 时保存。可以增加一个 Checkout 更新动作,在字段变化或步骤刷新时先把日期写入当前 Cart,处理方式与 Customer notes 的保存流程类似。
无论是否使用 AJAX,最终执行 Place order 时仍建议重新读取并验证字段,以免客户端漏传参数或参数被篡改。
前端使用 type="date" 是否足够
不够。浏览器提交的仍然是字符串,服务器端必须验证:
- 是否严格符合
Y-m-d。 - 是否允许空值。
- 日期是否早于当天。
- 是否符合最短备货期。
- 是否属于可配送日期。
- 是否与所选配送方式匹配。
数据是否有效,应由服务器端校验决定,不能依赖 JavaScript 或 HTML 属性。
备注:内容仅供参考。
