EC-CUBE 4.1系で商品の販売期間を限定するカスタム

# 販売期間を限定するカスタム
dtb_productに販売開始日時と販売終了日時を追加し、商品一覧ページと商品詳細ページにてカートボタンの表示を分岐させることで販売期間中のみに購入が可能になるカスタマイズを実装したい。なお、価格表示もその期間中とする。
カスタマイズを実装するにあたり、コミュニティにてアドバイスをいただくことができました。
ありがとうございます。 

当初は商品の公開/非公開自体の予約機能を実装しようと考えていたため、その分は別記事を投稿する。

## 環境
* Docker Compose version v2.0.0-rc.1
* PHP 7.4
* mysql 5.7
* EC-CUBE 4.1.1

## 参考にしたドキュメント

https://doc4.ec-cube.net/customize_entity

https://qiita.com/JunjiUmemoto/items/8617dfe1e8109561caca

http://a-zumi.net/eccube4-shedule-posts/

## Entityカスタマイズ
dtb_productに開始日時と終了日時のカラムを追加する。
Entity からフォームを自動生成したいため、``@EntityExtension`` アノテーションで拡張したフィールドに ``@FormAppend`` アノテーションを追加する。
今回は時間まで指定したいため``DateTimeType``を使用し``"with_seconds": true``とする。
常時公開する商品もあるため、``"required":false``にしておく。

```php:Customize\Entity\ProductTrait.php
<?php

namespace Customize\Entity;

use Doctrine\ORM\Mapping as ORM;
use Eccube\Annotation as Eccube;
use Eccube\Annotation\EntityExtension;
use Symfony\Component\Form\Extension\Core\Type\DateTimeType;

/**
 * @Eccube\EntityExtension("Eccube\Entity\Product")
 */
trait ProductTrait
{
  /**
   * @var \DateTime
   * @ORM\Column(name="publish_date", type="datetimetz", nullable=true)
   * @Eccube\FormAppend(
   *  auto_render=true,
   *  type="Symfony\Component\Form\Extension\Core\Type\DateTimeType",
   *  options={
   *    "required":false,
   *    "label":"公開予定日時",
   *    "input": "datetime",
   *    "placeholder": {"year":"----", "month": "--", "day": "--", "hour": "--", "minute": "--", "second": "--"},
   *    "with_seconds": true
   *  }
   * )
   */
  private $publish_date;

  /**
   * @var \DateTime
   * @ORM\Column(name="private_date", type="datetimetz", nullable=true)
   * @Eccube\FormAppend(
   *  auto_render=true,
   *  type="Symfony\Component\Form\Extension\Core\Type\DateTimeType",
   *  options={
   *    "required":false,
   *    "label":"非公開予定日時",
   *    "input": "datetime",
   *    "placeholder": {"year":"----", "month": "--", "day": "--", "hour": "--", "minute": "--", "second": "--"},
   *    "with_seconds": true
   *  }
   * )
   */
  private $private_date;

  public function getPublishDate(): ?\DateTimeInterface
  {
    return $this->publish_date;
  }

  public function setPublishDate(?\DateTimeInterface $publish_date): self
  {
    $this->publish_date = $publish_date;

    return $this;
  }

  public function getPrivateDate(): ?\DateTimeInterface
  {
    return $this->private_date;
  }

  public function setPrivateDate(?\DateTimeInterface $private_date): self
  {
    $this->private_date = $private_date;

    return $this;
  }
}
```

### Proxyクラスを生成
```
bin/console eccube:generate:proxies
```

### キャッシュ削除
```
bin/console cache:clear --no-warmup
```

### 実行するSQLを確認
```
bin/console doctrine:schema:update --dump-sql
```

### スキーマを更新
```
bin/console doctrine:schema:update --dump-sql --force
```

## 商品一覧ページ
Twigのdateフィルタで現時刻と販売期間を比較し、期間内であれば従来のカートボタンを、販売期間外の場合は販売期間の日時を表示しbuttonはdisabledにする。

```twig:app/template/default/Product/list.twig
<p class="price02-default">
    {% if date(Product.publish_date) < date() and date(Product.private_date) > date()%}
        {% if Product.hasProductClass %}
            {% if Product.getPrice02Min == Product.getPrice02Max %}
                {{ Product.getPrice02IncTaxMin|price }}
            {% else %}
                {{ Product.getPrice02IncTaxMin|price }} ～ {{ Product.getPrice02IncTaxMax|price }}
            {% endif %}
        {% else %}
            {{ Product.getPrice02IncTaxMin|price }}
        {% endif %}
    {% endif %}
</p>

~~~

{% if Product.stock_find  and Product.publish_date < date() < Product.private_date %}
{% set form = forms[Product.id] %}
<form name="form{{ Product.id }}" id="productForm{{ Product.id }}" action="{{ url('product_add_cart', {id:Product.id}) }}" method="post">
    <div class="ec-productRole__actions">
        {% if form.classcategory_id1 is defined %}
            <div class="ec-select">
                {{ form_widget(form.classcategory_id1) }}
                {{ form_errors(form.classcategory_id1) }}
            </div>
            {% if form.classcategory_id2 is defined %}
                <div class="ec-select">
                    {{ form_widget(form.classcategory_id2) }}
                    {{ form_errors(form.classcategory_id2) }}
                </div>
            {% endif %}
        {% endif %}
        <div class="ec-numberInput"><span>{{ 'common.quantity'|trans }}</span>
            {{ form_widget(form.quantity, {'attr': {'class': 'quantity'}}) }}
            {{ form_errors(form.quantity) }}
        </div>
    </div>
    {{ form_rest(form) }}
</form>
<div class="ec-productRole__btn">
    <button type="submit" class="ec-blockBtn--action add-cart" data-cartid="{{ Product.id }}" form="productForm{{ Product.id }}">
        {{ 'front.product.add_cart'|trans }}
    </button>
</div>
{% elseif Product.publish_date > date() or Product.private_date < date() %}
<div class="ec-productRole__btn">
    <button type="button" class="ec-blockBtn--action" disabled="disabled">
        <p>
            {{ Product.publish_date|date('m/d h:m') }} ~ {{ Product.private_date|date('m/d h:m') }}
        </p>
    </button>
</div>
{% else %}
<div class="ec-productRole__btn">
    <button type="button" class="ec-blockBtn--action" disabled="disabled">
        {{ 'front.product.out_of_stock'|trans }}
    </button>
</div>
{% endif %}
```
## 商品詳細ページ
商品詳細ページのボタンは``{{ form_rest(form) }}``で生成されているため、formごとifで囲うことでFormTypeを編集することなく条件分岐させる。

```twig:app/template/default/Product/detail.twig
{% if Product.private_date > date() >Product.publish_date %}
{# 通常価格 #}
{% if Product.hasProductClass -%}
    <div class="ec-productRole__priceRegular">
        {% if Product.getPrice01Min is not null and Product.getPrice01IncTaxMin == Product.getPrice01IncTaxMax %}
            <span class="ec-productRole__priceRegularPrice">{{ 'front.product.normal_price'|trans }}：<span class="price01-default">{{ Product.getPrice01IncTaxMin|price }}</span></span>
            <span class="ec-productRole__priceRegularTax">{{ 'common.tax_include'|trans }}</span>
        {% elseif Product.getPrice01Min is not null and Product.getPrice01Max is not null %}
            <span class="ec-productRole__priceRegularPrice">{{ 'front.product.normal_price'|trans }}：<span class="price01-default">{{ Product.getPrice01IncTaxMin|price }}～ {{ Product.getPrice01IncTaxMax|price }}</span></span>
            <span class="ec-productRole__priceRegularTax">{{ 'common.tax_include'|trans }}</span>
        {% endif %}
    </div>
{% else %}
    {% if Product.getPrice01Max is not null %}
        <span class="ec-productRole__priceRegularPrice">{{ 'front.product.normal_price'|trans }}：{{ Product.getPrice01IncTaxMin|price }}</span>
        <span class="ec-productRole__priceRegularTax">{{ 'common.tax_include'|trans }}</span>
    {% endif %}
{% endif %}
{# 販売価格 #}
<div class="ec-productRole__price">
    {% if Product.hasProductClass -%}
        {% if Product.getPrice02IncTaxMin == Product.getPrice02IncTaxMax %}
            <div class="ec-price">
                <span class="ec-price__price price02-default">{{ Product.getPrice02IncTaxMin|price }}</span>
                <span class="ec-price__tax">{{ 'common.tax_include'|trans }}</span>
            </div>
        {% else %}
            <div class="ec-price">
                <span class="ec-price__price price02-default">{{ Product.getPrice02IncTaxMin|price }} ～ {{ Product.getPrice02IncTaxMax|price }}</span>
                <span class="ec-price__tax">{{ 'common.tax_include'|trans }}</span>
            </div>
        {% endif %}
    {% else %}
        <div class="ec-price">
            <span class="ec-price__price">{{ Product.getPrice02IncTaxMin|price }}</span>
            <span class="ec-price__tax">{{ 'common.tax_include'|trans }}</span>
        </div>
    {% endif %}
</div>
{% endif %}

~~~

{% if date( Product.publish_date ) < date() < date( Product.private_date ) %}
<form action="{{ url('product_add_cart', {id:Product.id}) }}" method="post" id="form1" name="form1">
    {% if Product.stock_find %}
        <div class="ec-productRole__actions">
            {% if form.classcategory_id1 is defined %}
                <div class="ec-select">
                    {{ form_widget(form.classcategory_id1) }}
                    {{ form_errors(form.classcategory_id1) }}
                </div>
                {% if form.classcategory_id2 is defined %}
                    <div class="ec-select">
                        {{ form_widget(form.classcategory_id2) }}
                        {{ form_errors(form.classcategory_id2) }}
                    </div>
                {% endif %}
            {% endif %}
            <div class="ec-numberInput"><span>{{ 'common.quantity'|trans }}</span>
                {{ form_widget(form.quantity) }}
                {{ form_errors(form.quantity) }}
            </div>
        </div>
        <div class="ec-productRole__btn">
            <button type="submit" class="ec-blockBtn--action add-cart">
                {{ 'front.product.add_cart'|trans }}
            </button>
        </div>
    {% else %}
        <div class="ec-productRole__btn">
            <button type="button" class="ec-blockBtn--action" disabled="disabled">
                {{ 'front.product.out_of_stock'|trans }}
            </button>
        </div>
    {% endif %}
        {{ form_rest(form) }}
</form>
{% else %}
        <div class="ec-productRole__btn">
        <button type="button" class="ec-blockBtn--action" disabled="disabled">
            {{ Product.publish_date|date('Y/m/d h:m') }} ~ {{ Product.private_date|date('Y/m/d h:m') }}
        </button>
    </div>
{% endif %}

```

## あとがき
初心者ながら調べつつコミュニティの参加者さんにも助けられながらカスタマイズすることができました。
もし、もっとスマートな方法をご存知の方がいらっしゃいましたらぜひご教授いただけますと幸いです。

ブログ機能も開発したいところですが、コストが大変そうなので検討中。