wiki:EC-CUBE標準規約/開発効率向上のためのTips

開発効率向上のための Tips

概要

この Tips は, EC-CUBE標準規約には含まれないが, 開発や保守の効率を向上するための秘訣を記載します. なお, この Tips に沿っていない既存のソースコードが多々ありますが, すべてを修正するのは現実的ではありません. 新規にカスタマイズを加える箇所のみ適用すると良いでしょう.

要件

主に, PHP5.3.2 以降の環境で稼働するものを対象とします.

PHP5 の構文を使用する

EC-CUBE 2.12.0 以降, PHP4 はサポートしていないため, PHP5 の構文を使用します. これらを適切に使用することで, 開発・保守効率の向上が望めます.

Property Visibility (アクセス権)

クラスのメンバーには, public, protected, private のアクセス権を付与します. これにより, 外部クラスからの意図せぬメンバ変数アクセスを防ぐことができ, 堅牢かつ保守しやすいコードになります.

  <?php

  /**
   * NG クラスのメンバ変数で振舞いを変更できてしまう
   */
  class MyBadClass {

      var $order_id;

      function doSendMail() {
          $objPurchase = new SC_Purchase_Ex();
          $objPurchase->sendOrderMail($this->order_id);
      }
  }

  /**
   * OK 引数やアクセサメソッドを経由することにより, 意図せぬ振舞い変更を防ぐ
   */
  class MyGoodClass {

      private $order_id;

      public function setOrderId($order_id) {
          $this->order_id = $order_id;
      }

      public function getOrderId() {
          return $this->order_id;
      }

      public function doSendMail() {
          $objPurchase = new SC_Purchase_Ex();
          $objPurchase->sendOrderMail($this->getOrderId());
      }
  }

private または protected 修飾子により, 外部からのアクセスを防止することは重要ですが, EC-CUBEは継承によるカスタマイズを多用するため, 特に理由の無い限りは protected 修飾子を利用します.

  <?php
  /**
   * NG 内部用メソッドを外部からアクセスされてしまう
   */
  class MyBadClass {

      function lfLocalFunction($order_id) {
          $objPurchase = new SC_Purchase_Ex();
          $objPurchase->sendOrderMail($order_id);
      }
  }

  /**
   * OK 内部用クラスは, 継承によるカスタマイズの余地を残すため, protected を使用する
   */
  class MyGoodClass {

      protected function localFunction($order_id) {
          $objPurchase = new SC_Purchase_Ex();
          $objPurchase->sendOrderMail($order_id);
      }
  }

Exception (例外)

適切な Exception を使用することにより, エラーハンドリングを簡潔に記述できます.

  <?php

  /**
   * NG Exception を使用しない場合は, 返り値が煩雑になります.
   * このクラスの場合は, mixed なので, ソースを見ないと何が返ってくるか解りません!
   */
  class MyBadClass {

      /**
       * @return mixed 処理が成功した場合は計算後の値を返す;
       * 失敗した場合は false を返す.
       */
      public function customFunction($number) {
          // 失敗した場合は false を返す
          if ($number > 10) {
              return false;
          }
          return $number + 10;
      }
  }

  /**
   * OK Exception を使用すると, 返り値が簡潔になり, エラーハンドリングもしやすくなります.
   * Exception クラスを拡張し, 独自の Exception を定義しています.
   * customFunction() は, 必ず数値が返り, エラーが発生した場合は MyException をスローします.
   */
  class MyGoodClass {

      /**
       * @return integer 計算後の値を返す.
       * @throw MyException 処理が失敗した場合
       */
      public function customFunction($number) {
          if ($number > 10) {
              throw new MyException('error message!');
          }
          return $number + 10;
      }
  }

  class MyException extends Exception {

      public function __construct($message, $code, $previous) {
          parent::__construct($message, $code, $previous);
      }
  }

Enum (列挙型)

PHP の標準構文ではありませんが, interface を使用することにより Enum(列挙型)を擬似的に表現できます. 定数をたくさん定義するより, 直感的に記述できます.

  <?php
  /**
   * NG 定数を羅列すると, 煩雑になる
   */
  define('OPENID_SCOPE_OPENID', 'openid');
  define('OPENID_SCOPE_PROFILE', 'profile');
  define('OPENID_SCOPE_EMAIL', 'email');
  define('OPENID_SCOPE_ADDRESS', 'address');
  define('OPENID_SCOPE_PHONE', 'phone');

  // 他の定数と混同しやすい
  echo sprintf('%s %s',
               OPENID_SCOPE_OPENID,
               OPENID_SCOPE_PROFILE);


  /**
   * OK Enum(列挙型)を表現することにより, 直感的にアクセス可能.
   * 継承もできるため, 拡張しやすい.
   */

  /**
   * OpenID Connect の Scope を表す列挙型
   */
  interface OIDConnect_Scope {
      const OPENID = 'openid';
      const PROFILE = 'profile';
      const EMAIL = 'email';
      const ADDRESS = 'address';
      const PHONE = 'phone';
  }

  // OpenID Connect 関連の定義ということが直感的に判断できる
  echo sprintf('%s %s',
               OIDConnect_Scope::OPENID,
               OIDConnect_Scope::PROFILE);

Design Pattern (デザインパターン)

PHP5 の構文を活用することにより, 各種デザインパターンを利用しやすくなりました. 特に Singleton, Chain of Responsibility, Factory, Strategy などをよく利用します.

 http://www.ibm.com/developerworks/jp/opensource/library/os-php-designptrns/

エンティティクラスを作成する

標準の EC-CUBE では, データベースから連想配列で値を取得します.

この連想配列を使用して, さまざまなビジネスロジックを制御したり, 画面表示したりするわけですが, 複雑なデータ構造になると, とても煩雑なコードとなってしまいます.

エンティティクラスを作成することにより, 簡潔にコーディングでき, 開発効率向上が望めます.

<?php

/**
 * エンティティクラスの例
 */
class SC_Entity_OrderDetail {

    private $order_detail_id;
    private $order_id;
    private $product_id;
    private $product_class_id;
    private $product_name;
    private $product_code;
    private $classcategory_name1;
    private $classcategory_name2;
    private $price;
    private $quantity;
    private $point_rate;

    /**
     * コンストラクタ.
     * 
     * 引数の連想配列で, 本オブジェクトのプロパティを設定可能.
     * 
     * @param array $arrOrderDetail
     */
    public function __construct($arrOrderDetail = array()) {
        if (is_array($arrOrderDetail)) {
            $this->setPropertiesFromArray($arrOrderDetail);
        }
    }

    /**
     * 税込単価を返します.
     * 
     * @return integer 税込単価
     */
    public function getTotalInTax() {
        return SC_Helper_DB_Ex::sfTax($this->getPrice()) * $this->getQuantity();
    }

    /**
     * 該当商品のカテゴリIDを配列で返します.
     * 
     * @return array カテゴリ一覧の配列
     */
    public function getCategoryIds() {
        // エンティティクラスの内部での DBアクセスは極力避けるべきですが,
        // 内部でDBアクセスした方が効率が良い場合は, その旨のコメントを入れて使用します.
        // この場合は, product_id に紐づいた category_id を取得します.
        // 以下のロジックを外出しするより簡潔なコードになります.
        $objQuery = SC_Query_Ex::getSingletonInstance();
        $col = 'category_id';
        $from = 'dtb_product_categories';
        $where = 'product_id = ?';
        $arrResults = $objQuery->select($col, $from, $where,
                                        array($this->getProductId()));
        $arrCategoryIds = array();
        foreach ($arrResults as $val) {
            $arrCategoryIds[] = $val;
        }
        return $arrCategoryIds;
    }

    /*
     * 読み取り専用にしたいプロパティは, getter のみ作成します.
     * 更新可能なプロパティは setter を作成します.
     */
    public function getOrderId() {
        return $this->order_id;
    }
    public function getProductId() {
        return $this->product_id;
    }
    public function getProductClassId() {
        return $this->product_class_id;
    }
    public function getProductCode() {
        return $this->product_code;
    }
    public function getClassCategoryName1() {
        return $this->classcategory_name1;
    }
    public function getClassCategoryName2() {
        return $this->classcategory_name2;
    }
    public function getPrice() {
        return $this->price;
    }
    public function setQuantity($quantity) {
        $this->quantity = $quantity;
    }
    public function getQuantity() {
        return $this->quantity;
    }
    protected function getPointRate() {
        return $this->point_rate;
    }

    /**
     * 引数の連想配列を元にプロパティを設定します.
     * DBから取り出した連想配列を, プロパティへ設定する際に使用します.
     * 
     * @param array プロパティの情報を格納した連想配列
     * @param ReflectionClass $parentClass 親のクラス. 本メソッドの内部的に使用します.
     */
    public function setPropertiesFromArray($arrProps, ReflectionClass $parentClass = null) {
        $objReflect = null;
        if (is_object($parentClass)) {
            $objReflect = $parentClass;
        } else {
            $objReflect = new ReflectionClass($this);
        }

        $arrProperties = $objReflect->getProperties();
        foreach ($arrProperties as $objProperty) {
            $objProperty->setAccessible(true);
            $name = $objProperty->getName();
            $objProperty->setValue($this, $arrProps[$name]);
        }

        // 親クラスがある場合は再帰的にプロパティを取得
        $parentClass = $objReflect->getParentClass();
        if (is_object($parentClass)) {
            self::setPropertiesFromArray($arrProps, $parentClass);
        }
    }

    /**
     * プロパティの値を連想配列で返します.
     * DBを更新する場合などで, 連想配列の値を取得したい場合に使用します.
     * 
     * @return array 連想配列のプロパティの値
     */
    public function toArray(ReflectionClass $parentClass = null) {
        $objReflect = null;
        if (is_object($parentClass)) {
            $objReflect = $parentClass;
        } else {
            $objReflect = new ReflectionClass($this);
        }
        $arrProperties = $objReflect->getProperties();
        $arrResults = array();
        foreach ($arrProperties as $objProperty) {
            $objProperty->setAccessible(true);
            $name = $objProperty->getName();
            $arrResults[$name] = $objProperty->getValue($this);
        }

        // 親クラスがある場合は再帰的にプロパティを取得
        $parentClass = $objReflect->getParentClass();
        if (is_object($parentClass)) {
            $arrParents = self::toArray($parentClass);
            if (!is_array($arrParents)) {
                $arrParents = array();
            }
            if (!is_array($arrResults)) {
                $arrResults = array();
            }
            $arrResults = array_merge($arrParents, $arrResults);
        }
        return $arrResults;
    }
}

PHP で, 以下のようにデータベースから取得できます

  <?php
  $objPurchase = new SC_Helper_Purchase_Ex();
  $arrOrderDetail = $objPurchase->getOrderDetail($order_id);
  $this->arrObjOrderDetail = array();
  foreach ($arrOrderDetail as $orderDetail) {
      $this->arrObjOrderDetail[] = new SC_Entity_OrderDetail($orderDetail);
  }

Smarty で記述した場合に, 連想配列より簡潔にコーディングできます

  <!--{foreach from=$arrObjOrderDetail item=objOrderDetail}-->
      <!-- 各受注明細の税込小計を取得します -->
      <p><!--{$objOrderDetail->getTotalInTax()}-->円</p>
  <!--{/foreach}-->

データベースを更新する場合も簡潔にコーディングできます

  <?php
  // 数量を2に変更します
  $objOrderDetail->setQuantity(2);
  $objQuery = SC_Query_Ex::getSingletonInstance();
  $objQuery->update('dtb_order_detail', $objOrderDetail->toArray(),
                    'order_detail_id = ?',
                    $objOrderDetail->getOrderDetailId());

ユニットテストを活用する

EC-CUBE標準規約/単体テストガイドライン