開発効率向上のための 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());