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

Version 3 (modified by nanasess, 7 years ago) (diff)

単体テストガイドラインへのリンクを追加

開発効率向上のための 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($orderId) {
          $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() {
          $arrResults = array();
          $objReflect = new ReflectionClass($this);
          $arrProperties = $objReflect->getProperties();

          foreach ($arrProperties as $objProperty) {
              $objProperty->setAccessible(true);
              $name = $objProperty->getName();
              $arrResults[$name] = $objProperty->getValue($this);
          }
          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標準規約/単体テストガイドライン