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