ECサイトをリニューアルオープン

symfonyの開発実例って大規模なものが多いとか言われたりしますが、小さなECサイトの開発をsymfonyをベースで作ったのでちょこっと紹介。

愛犬のための犬グッズ専門サイト アットペット

アットペット
元々個人的に関わっているサイトなのですが、7年前に古い自前PHPフレームワークで動いていたものをそろそろどうにかするかということで慣れたsymfonyで機能を追加しつつ作り直しました。

オンラインショッピングはもちろんのこと、ポイントでお買い物したり、買った商品に愛犬の写真と共にレビューを投稿することもできます。ポイント還元率が高いので大容量のドッグフードをお探しの方など是非利用してみてください。

なぜsymfonyを選択したか?

PHPECサイトをさくっと作るとなると、EC-CUBEで構築するという選択肢もありましたが次の理由でEC-CUBEを使わないことにしました。

カスタマイズする部分が結構多いのでEC-CUBEだと余計に工数が掛かりそう

買い物のフローがEC-CUBEと異なっていたり、会員機能にもペット情報が紐づいていたりと、改修するのにどのファイルを触ればよいか調べたり、その修正が他の機能に影響を与えないかテストを書いたりすることを考えると今回はEC-CUBEを使わないほうがベターと判断

旧データの移行をEC-CUBEに合わせるのが面倒

受注データやポイントの履歴などもあったので、EC-CUBEを熟知していない状態でそれらを全て移すのもこれまた大変と。

今後がっつり機能追加していきたいので拡張性重視

会員機能やコミュニティー機能を追加していくのに、ベースが奇麗なほうが良いと判断。無理に組み込んで拡張したEC-CUBEを更に拡張は大変だろうなぁと。


正直EC-CUBEをカスタマイズしたこともありますが、かなり昔のことですし精通しているわけではないので現在はこれらの理由は当てはまらないかもしれません。
で、PHPといえばフレームワーク天国ですがsymfonyを選んだ理由は次の理由

自分がsymfonyに慣れ親しんでいるから

symfony1が出る前から触ってるぐらいですから、愛着もあります。なんでもそうですが慣れれば便利です。そして結構重要な要素です。

アドミンジェネレーターを使いたかったから

一番の理由はこれです。
symfonyには管理画面のようなCRUD画面を設定(yaml)ファイルの記述だけで作れてしまうアドミンジェネレーターという超便利な機能があります。
もちろん、yamlだけで制御できないところはカスタマイズできますし、テーブルにカラムが追加されてもソースを修正する必要もありませんし、あったとしても最小限で済みます。
とにかく工数を抑えたかったので管理画面についてはアドミンジェネレータを駆使して作りました。あれをゼロから作ったとしていたら工数は倍以上は掛かっていたと思います。(もちろん見た目など妥協すべき点もありますが)。

キャッシュ機能が豊富

symfonyではページ全体をキャッシュしたり、ページの一部(パーシャルやコンポーネント)だけをキャッシュしたりできます。symfonyそのものがヘビーなのでこういう部分には結構力を入れている印象です。サイトのアクセス数が少ないとはいえキャッシュ機能が充実しているのは重要です。



小規模なECサイトですが、開発するときに注意したことや感想をまとめておきます。

CMSとしてWordpressを利用しsymfonyと連携

オーナーがページを増やしたり、記事を書いたりするのはすべてWordpressにお任せしました。そして、Wordpressで表示する画面のヘッダーのphpsymfonyインスタンスを作成し「ログイン」状態でログインボタンの表示を着替えたりできるように連動させました。つまり、Wordpressのテンプレートからsymfonyのlink_toヘルパー関数などを呼んだり、Userインスタンスを利用できりるようにしたということですね。これが簡単にできるのは便利でした。
via:http://www.exgear.jp/blog/2010/10/wordpress-with-symfony/

プラグインとことん活用

あるものは利用させていただく方針。個人的にPEARで管理はあまりしたくない派なので公式サイトやOpenpearSubversionGithubからgit cloneでプロジェクトに持ってきてます。
そして自分で作ったプラグインも含め以下のプラグインを利用

  • sfDoctrineGuardPlugin
    • 言わずと知れたDoctrineで会員管理するためのプラグイン。これをベースに会員登録、ログイン、パスワード再発行などの実装を行いました。また、追加の会員属性(ふりがななど)を保持する必要があるので、そういう情報を含め全て別テーブルにデータを持つようにし、sfDoctrineGuardPluginでは基本情報のみ持つようにしました。スッキリ。
    • via:symfony 1.x legacy website
  • sfFormExtraPlugin
  • jpFormExtraPlugin
  • sfSimplePagePlugin
  • sfThumbnailPlugin
    • 画像のサイズを変更したりするライブラリですね。投稿写真のリサイズ処理に利用しています。
    • via:symfony 1.x legacy website

他にもライブラリをプラグイン化したりしました。ある意味プラグインだらけです。

Google AnalyticsAPIを利用したアクセスランキング表示

Google AnalyticsAPIを利用すればアクセス数ランキングのためのデータを集計したりすることも簡単です。本当はタスクとしと書きたかったのですが、とりあえずコンポーネントとして切り出して1日に1回だけAPIにアクセスし以後はその結果をキャッシュしたものを表示するようにしました。モジュールのconfig/cache.ymlに以下のように書けば特定のコンポーネントだけキャッシュできるsymfonyの標準の機能そのままです。
via:symfony 1.x legacy website

_accessRanking:
  enabled: true

ジェネレータは自前のひな形を用意

symfonyのデフォルトのgenerate:moduleタスクではコンポーネントクラスを作ってくれなかったりするので、自前のスケルトンを用意しそれをひな形にするようにしました。また、そのひな形にはアクションクラスでよく使うメソッドや使い方をactions.class.sample.phpとして書き出すようにしておいたのでオンラインでググる必要も最小限に。
via: http://www.exgear.jp/blog/2010/04/symfony%E3%81%A7%E3%81%AE%E9%96%8B%E7%99%BA%E3%82%92%E6%A5%BD%E3%81%AB%E3%81%99%E3%82%8B3%E3%81%A4%E3%81%AE%E6%96%B9%E6%B3%95/

actions.class.sample.php
<?php
class sampleActions extends sfActions
{
  public function preExecute()
  {
    // code here
  }
 /**
  * Executes index action
  *
  * @param sfRequest $request A request object
  */
  public function executeIndex(sfWebRequest $request)
  {
    // default
    return sfView::SUCCESS;
    // not return template but render text
    return $this->renderText('message here');
  }
  private function __sampleCode()
  {
    // for debug
    $this->logMessage($message, 'debug'); // emerg, alert, crit, err, warning, notice, info, and debug

    // get Rouging object
    $object = $this->getRoute()->getObject();
    
    // with form
    $this->form = new HogeForm($object);
    $this->form->bind($request->getParameter('hoge'));
    if ($this->form->isValid()) {
      // code here
      $name = $this->form->getValue('name');
    }
    $widget = $this->form['name']->getWidget();
    
    // use Request Parameter
    $name = $request->getParameter('name', 'default_value');
    $all_parameters = $request->getParameterHolder()->getAll();
    $condition = $request->isMethod('post'); // if request method is 'post', return true.
    
    // set template
    $this->setTemplate('new'); // render 'newSuccess.php' instead of 'indexSuccess.php'
    
    // use session
    $this->getUser()->setAttribute('name', 'value');
    $condition = $this->getUser()->hasAttriute('error');
    $name = $this->getUser()->getAttribute('name', 'default_value');
    $condition = $this->getUser()->isAuthenticated();
    
    // use flash
    $this->getUser()->setFlash('error', 'error is occured!');
    $condition = $this->getUser()->hasFlash('error');  
    $name = $this->getUser()->getFlash('error');   
    
    // forward
    $this->forward('module', 'action');
    $this->forwardIf($condition, 'module', 'action'); // if $condition is true, forwarding to 'module/action'
    $this->forward404If($condition, 'message'); // if $condition is true, forwarding to the 404 page
    
    // redirect
    $this->redirect('@routing_name');
    $this->redirect('module/action');
    $this->redirectIf($condition, '@routing_name');
    $this->redirect404If($condition, '@routing_name');
    
    // call Doctrine
    $result = Doctrine_Core::getTable('ModelName')->find('id');
    $q = Doctrine_Query::create()->from('ModelName');
    
    // access to Global Parameters
    $request->getMethod() 	      // $_SERVER['REQUEST_METHOD']
    $request->getUri() 	          // $_SERVER['REQUEST_URI']
    $request->getReferer() 	      // $_SERVER['HTTP_REFERER']
    $request->getHost() 	        // $_SERVER['HTTP_HOST']
    $request->getLanguages() 	    // $_SERVER['HTTP_ACCEPT_LANGUAGE']
    $request->getCharsets() 	    // $_SERVER['HTTP_ACCEPT_CHARSET']
    $request->isXmlHttpRequest() 	// $_SERVER['X_REQUESTED_WITH'] == 'XMLHttpRequest'
    $request->getHttpHeader() 	  // $_SERVER
    $request->getCookie() 	      // $_COOKIE
    $request->isSecure() 	        // $_SERVER['HTTPS']
    $request->getFiles() 	        // $_FILES
    $request->getGetParameter() 	// $_GET
    $request->getPostParameter() 	// $_POST
    $request->getUrlParameter() 	// $_SERVER['PATH_INFO']
    $request->getRemoteAddress() 	// $_SERVER['REMOTE_ADDR']
    
    // add a new parameter to RequestParameter
    $request->addRequestParameters(array('new_key' => 'new_value'));
  }
}

気軽なデプロイ

小規模なプロジェクトなのでデプロイはsymfony標準コマンドの project:deploy をそのまま利用。このコマンドはrsyncでファイルを転送してくれます。ある意味PHP関係ないですがこんなコマンドが用意されているところがsymfonyっぽい。

sfFormはきつい

symfonyを触っているというよりsfFormに苦しんだ開発でした。sfFormとはsymfonyに用意されているフォーム処理用のサブフレームワークのようなものです。ある程度場数をこなせばその流用で早く開発ができるのですが、そこにたどり着くまでがかなり大変です。今回もこの部分で苦しみました。
doctrine:build-formsでフォームクラスは生成されますが、このフォームクラスは直接使わずにextendsしたものを必ず用意するようにしました。同じ会員テーブルでも、会員登録時と管理画面の編集時でフォームが異なることなんてよくありますから。

UIはJQuery

JavaScriptJQueryを使いました。これも使い慣れているのが一番の理由。カレンダーとかそういうウィジット関連はsymfonyプラグインが用意されているのでそれを利用。でも、表示の日本語化とかそのあたりは自前で拡張。

ソース管理はGitとDropbox

実質一人で作業してるので個人で使っているgitリポジトリを利用してます。そしてgitを使えない非開発者とのドキュメント系の共有はDropboxで行いました。十分です。

動く状態のものができるまで1.5ヶ月

本業とは別で作業してました。家に帰ってちょこちょことちまちまと。。で管理画面も含めて実作業期間は1.5ヶ月ぐらい掛かったと思います。そして機能追加や修正やデザイン部分も含めて3ヶ月程度でしょうか。実際デザインも自分でやったのでPHPでごりごり書いている時間よりも慣れていないHTMLやCSSを調整してる時間のほうが確実に長かったと思います。



機会があれば勉強会とかでもう少し詳しく話すかなー。聞きたい人がいれば。。(´・ω・`)


さて、今月はSymfony2 + MongoDBでアプリ1つと、LithiumでSukonvを作ってしまわなければ!!!