'Programming'에 해당되는 글 10건

  1. 2007/01/17 Markdown 을 소개합니다.
  2. 2007/01/11 FLEX 구매대행 요금 계산기 (3)
  3. 2007/01/09 Flex Gnuboard Frontend
  4. 2007/01/08 Extending Symfony Adjacent List
  5. 2007/01/03 Symfony Adjacent List
  6. 2006/12/29 AJAX Post-It 만들기 (1)
  7. 2006/12/27 Building Protein List using BeautifulSoup and BioPython
  8. 2006/12/24 Symfony Form Helper - object_select_tag
  9. 2006/12/20 Adapter Pattern in PHP
  10. 2006/12/20 Symfony v1 beta 2 has been released

Markdown 을 소개합니다.

Programming 2007/01/17 09:32
텍스트 파일은 간편하게 기록하기에 정말 알맞은 포맷입니다. 또한 다른 포맷으로 변환하기에도 용이하구요. 하지만 여러 폰트를 사용할 수 없고, 레이아웃과 관련된 설정을 할 수 없기에 다른 한편으로는 굉장히 불편한 포맷입니다.
최근 CHARMM[각주:1] 이라는 프로그램의 문서들을 HTML 로 변환하고 있습니다. 처음에는 MoinMoin[각주:2] 의 parser 를 본따서 규칙들을 만들었는데, 나중에는 잘 동작을 안 하게 되더군요. 이유는 문서들을 만든 사람들이 제각각이고, 텍스트 파일에서 보기 좋게 만든다는 목적 아래 각자 자신만의 규칙을 도입했기 때문입니다. 가장 큰 문제가 된 것이 들여쓰기와 리스트, 그리고 코드와 문서의 구분입니다. 결국 현재의 문서에서 어떤 규칙을 찾아내고자 했던 노력은 실패로 돌아갔습니다.

그래서, 이제는 문서를 직접 수정하여 변환에 용이한 포맷으로 고치는 것으로 방향을 바꿨습니다. 그리고 이 포맷을 사용자 그룹에게 제안할 것입니다. 그리하여 텍스트 관련 포맷을 찾던중, Markdown[각주:3] 을 찾게 되었습니다. Markdown 은 텍스트 기반 포맷팅 규칙이면서, 동시에 Markdown 포맷의 텍스트를 HTML 로 변환하여 주는 툴이기도 합니다. 맥오에스의 불세출의 텍스트 에디터 TextMate 에도 Markdown 및 Markdown 에 LaTeX 이나 PDF 로의 변환기능까지 덧붙인 MultiMarkdown 이 Bundle 되어 바로 프리뷰 및 변환을 할 수도 있습니다.

아래 이미지들은 임시로 변환해본 파일입니다. HTML 문서나 복잡한 텍스트 포맷의 경우에는 다른 뷰어를 거치지 않고서는 읽기가 어려운 경우가 많습니다만, Markdown 포맷의 가장 큰 장점은, 쉽게 작성할 수 있으면서도, 텍스트 자체로 훌륭한 가독성을 제공한다는 것입니다.

사용자 삽입 이미지

Markdown Text

사용자 삽입 이미지

Markdown Generated HTML


우리나라에서는 웹에 문서등을 올릴때에 리치 에디터 쪽으로 많이 옮겨간 것 같은데, 저의 경우에는 간편한 텍스트 형식이 좋습니다. 그래야 다른 곳으로 자료를 이동하기가 편리하니까요. 지금 테터툴즈에 글을 작성하면서도, 만약에 내가 다른 곳으로 이 글들을 옮겨야 하는 경우를 생각해 보면 앞이 깜깜합니다.

Useful Links
------------
  1. Markdown - John Gruber 가 만든 Original Perl Markdown
  2. Variations
    1. PHP Markdown
    2. Python Markdown
  3. Markdown Cheat Sheet
  4. MultiMarkdown - Fletcher Penny 가 Markdown 에 LaTeX, PDF, XML 등으로의 변환 기능과 기타 기능들을 덧 붙여 만든 Perl 기반 툴입니다. 간결함을 유지하면서 더욱 강력합니다.
  5. MultiMarkdown Syntax
  1. http://www.charmm.org/ [본문으로]
  2. http://moinmoin.wikiwikiweb.de/ [본문으로]
  3. http://daringfireball.net/projects/markdown/ [본문으로]

'Programming' 카테고리의 다른 글

Markdown 을 소개합니다.  (0) 2007/01/17
FLEX 구매대행 요금 계산기  (3) 2007/01/11
Flex Gnuboard Frontend  (0) 2007/01/09
Extending Symfony Adjacent List  (0) 2007/01/08
Symfony Adjacent List  (0) 2007/01/03
AJAX Post-It 만들기  (1) 2006/12/29
tags : Markdown
Trackback 0 : Comment 0

FLEX 구매대행 요금 계산기

Programming 2007/01/11 09:39
구매대행 사이트 작업해줄 일이 있었는데, 공부하는셈 치고 요금계산기를 한 번 만들어봤습니다. 동작하는 예제는 http://s4mtech.nt.co.kr/bemarket/EstimateDeliveryFee/EstimateDeliveryFee.html 입니다. 맨 아래에 소스파일도 있습니다.
사용자 삽입 이미지사용자 삽입 이미지


1. Validator
사용자 삽입 이미지
Validator 를 이용하면 쉽게 입력된 값들을 확인할 수 있었습니다. 게다가 에러 표시까지 알아서..
# Definition of NumberValidator from Adobe Livedoc
<mx:NumberValidator
    allowNegative="true|false"
    decimalPointCountError="The decimal separator can only occur once."
    decimalSeparator="."
    domain="real|int"
    exceedsMaxError="The number entered is too large."
    integerError="The number must be an integer."
    invalidCharError="The input contains invalid characters."
    invalidFormatCharsError="One of the formatting parameters is invalid."
    lowerThanMinError="The number entered is too small."
    maxValue="NaN"
    minValue="NaN"
    negativeError="The number may not be negative."
    precision="-1"
    precisionError="The number entered has too many digits beyond the decimal point."
    separationError="The thousands separator must be followed by three digits."
    thousandsSeparator=","
  />

# Custom NumberValidator
<mx:NumberValidator id="numVal" property="text"
        allowNegative="false" domain="real"
        decimalPointCountError="입력형식을 살펴주십시오."
        decimalSeparator="."
        exceedsMaxError="입력값이 너무 큽니다."
        invalidCharError="입력형식을 살펴주십시오."
        lowerThanMinError="입력값이 너무 작습니다."
        maxValue="NaN"
        minValue="0"
        negativeError="0보다 작은 수는 입력하실 수 없습니다."
        precision="2"
        precisionError="소수점 아래로는 두자리까지만 쓰실 수 있습니다."
        requiredFieldError="필수입력 항목입니다."
        separationError="입력형식을 살펴주십시오." />
Validate 해야할 필드들이 여러개였기 때문에, 아래와 같은 식으로 활용했습니다. 원래는 Validator 에 'source' 항목에 확인할 필드의 id 를 적어주고, `validator_id.validate()` 식으로 호출하면 됩니다.
import mx.events.ValidationResultEvent;

private var vResult:ValidationResultEvent;

public function validateNumFields() : Boolean
{
    var listeners:Array = [exchangeRate, itemPrice, itemGroundFee, itemWeight];
    var isValid:Boolean = true;
    for each (var listener:Object in listeners)
    {
        numVal.listener = listener;
        vResult = numVal.validate(listener.text);
        if (vResult.type == ValidationResultEvent.INVALID)
        {
            isValid = false;
        }
    }
   
    return isValid;
}


2. Trap TextInput
Validator 를 알기전에, 텍스트박스에 숫자 대신 다른 문자를 입력하는 것을 막기 위해서 키 입력을 가로채고자 했습니다. 처음에는 KEY_DOWN 이벤트를 사용하려고 했는데, 이벤트를 Cancel 하는 것이 불가능하더군요. 그래서 TEXT_INPUT 이벤트를 이용하여야 했습니다. Event 객체의 preventDefault 메쏘드는 원래 실행되어야 하는 이벤트를 막는 역할을 하며, Event 의 읽기 가능 옵션인 cancelable 이 true 일 때만 동작합니다.
public function trapTextInput( event:TextEvent ) : void
{
    if ((event.text < '0' || event.text > '9') && event.text != '.')
    {
        event.preventDefault();
    }
}
대신 KEY_DOWN 으로는 텍스트 박스에서 엔터를 눌렀을 경우를 처리할 수 있더군요.
public function keyListener( event:KeyboardEvent ) : void
{
    if (event.keyCode == 13)
    {
        estimateDeliveryFee();
    }
}


3. Formatter
# Definition of CurrencyFormatter from Adobe Livedoc
<mx:CurrencyFormatter
    alignSymbol="left|right"
    currencySymbol="$"
    decimalSeparatorFrom="."
    decimalSeparatorTo="."
    precision="-1"
    rounding="none|up|down|nearest"
    thousandsSeparatorFrom=","
    thousandsSeparatorTo=","
    useNegativeSign="true|false"
    useThousandsSeparator="true|false"
 />

# Custom CurrencyFormatter
<mx:CurrencyFormatter id="WonPrice" precision="0"
        rounding="up"
        decimalSeparatorTo="."
        thousandsSeparatorTo=","
        useThousandsSeparator="true"
        useNegativeSign="true"
        currencySymbol=" 원"
        alignSymbol="right"/>
요런 CurrencyFormatter 를 가지고 Text 박스에 숫자를 입력할때,
textTotalWon.text = WonPrice.format(totalprice.toString());
와 같이 입력하면 됩니다.


4. XML 파일 읽기
XML 파일 읽고 사용하기는 어렵지 않더군요. 차이점이라면, 로컬에 있는 자료를 읽는 것도 비동기방식으로 처리된다는 것 정도?
var myXML:XML = new XML();               
var XML_URL:String = "config.xml";
var myXMLURL:URLRequest = new URLRequest(XML_URL);
var myLoader:URLLoader;
myLoader = new URLLoader(myXMLURL);
myLoader.addEventListener("complete", xmlLoaded);
               
public function xmlLoaded(evtObj:Event):void
{
    myXML = XML(myLoader.data);
    exchangeRate.text = myXML.exchangeRate;
}

FLEX 의 장점은 뭐니뭐니 해도, 저같이 디자인에 0점인 사람도 Flash UI 를 깔끔하게 만들 수 있다는게 아닐까요. Flex Builder Project 를 첨부합니다.


'Programming' 카테고리의 다른 글

Markdown 을 소개합니다.  (0) 2007/01/17
FLEX 구매대행 요금 계산기  (3) 2007/01/11
Flex Gnuboard Frontend  (0) 2007/01/09
Extending Symfony Adjacent List  (0) 2007/01/08
Symfony Adjacent List  (0) 2007/01/03
AJAX Post-It 만들기  (1) 2006/12/29
tags : flex
Trackback 0 : Comment 3

Flex Gnuboard Frontend

Programming 2007/01/09 14:00
사용자 삽입 이미지

그누보드 Frontend


사용자 삽입 이미지

그누보드 Frontend


허접하지만 FLEX 공부하면서 그누보드[각주:1] Frontend 를 만들어봤습니다. 소스는 Mike Potter 의 Flex2 and AMFPHP[각주:2] 를 참조하였습니다. 공부에 좀 도움이 되네요. PHP Remoting 으로는 AMFPHP 를 사용했습니다. 제가 지금은 가지고 있는 서버가 없어서 예제는 보여드릴수가 없어서 아쉽네요. 다른 분들도 혹시 도움이 될지 몰라 여기에 소스를 함께 올리겠습니다.

FLEX 를 해보니 확실히 Frontend 형식의 무언가를 만드는데 정말 좋다는 생각을 많이 했습니다. 이와 더불어 PHP 나 기타 웹 어플리케이션들이 웹 서비스 형식으로 많이 발전할 것 같다는 생각을 했구요. 하지만 기존의 웹 어플리케이션은 나름대로 살아남을 것입니다. 웹 서비스와 기존 방식의 웹 서비스를 동시에 하기에는 프레임웍의 도입이 절실한 것 같습니다.

이런 PHP Remoting 을 이용하기 위해서는 Array 나 객체 형식의 자료형을 주고 받아야 하는데, 그누보드의 경우에는 중첩된 include 나 require 를 사용하기 때문에, 기존의 코드는 전혀 사용할 수가 없었습니다. 만약 객체지향으로 설계된 (최소한 각각의 기능이 함수로 감싸져있다면) 웹 어플리케이션이 있다면, 이를 FLEX 웹 서비스로 바꾸는 것은 UI 만 고려하면 될 것입니다.

자료는 맨 아래부분에 올려두겠습니다. sample.zip 은 Flex Builder 프로젝트이고, amfphp.tar.gz 은 PHP 서비스입니다. 그누보드 폴더 아래에 압축을 푸시면 될 것입니다. 아래는 제가 했던 작업들, 나중에 참고하기 위해, 적어둡니다.

1. Forum list
 - add `getForums` service in amfphp service
 - add mxTree control on flex project
 - add mxPanel control on flex project and place mxTree control on the panel control
 - set borderStyle as `none` and width as `100%` for mxTree control
 - modify onResult method on flex project

2. Category list and bind event
 - Use `bo_category_list` to populate child nodes
 - Bind events on each of them, and add event which fetch items on a given forum with category if the selected item is category.

3. Next step
 - User validation
 - Paginated item list
 - Display item contents

amfphp.tar.gz

AMFPHP Service

sample.zip

FLEX Builder Project





  1. http://www.sir.co.kr [본문으로]
  2. http://www.adobe.com/devnet/flex/articles/flex2_amfphp.html [본문으로]

'Programming' 카테고리의 다른 글

Markdown 을 소개합니다.  (0) 2007/01/17
FLEX 구매대행 요금 계산기  (3) 2007/01/11
Flex Gnuboard Frontend  (0) 2007/01/09
Extending Symfony Adjacent List  (0) 2007/01/08
Symfony Adjacent List  (0) 2007/01/03
AJAX Post-It 만들기  (1) 2006/12/29
tags : flex, 그누보드
Trackback 0 : Comment 0

Extending Symfony Adjacent List

Programming 2007/01/08 14:42
지난번에 만들었던 Adjacent List 를 확장해볼 생각입니다. 추가할 기능은, 체크박스를 넣고 상태 토글 기능을 넣어보겠습니다.

1. schema.yml 수정
각 item 에 상태를 추가하기 위해서 status 테이블을 만들고, item 테이블을 status 테이블과 조인할것입니다.
# config/schema.yml
propel:
  item:
    _attributes: { phpName: Item }
    id:
    parent_id:    integer
    thread:       integer
    depth:       { type: tinyint, default: 0 }
    title:        varchar(255)
    description:  longvarchar
    lft:         { type: integer, default: 1 }
    rgt:         { type: integer, default: 2 }
    status_id:    tinyint
    _foreign_keys:
      - self_joined_keys
        foreign_table: item
        on_delete: cascade
        references:
          - { local: parent_id, foreign: id }
      - status_joined_keys
        foreign_table: status
        on_delete: setnull
        references:
          - { local: status_id, foreign: id }

  status:
    _attributes: { phpName: Status }
    id:
    tag:          varchar(255)
    title:        varchar(255)
원래, `status_id` 란 필드명을 사용하고 `status` 란 테이블이 존재할 경우에는 propel 이 알아서 foreign key 를 생성해 주는데, 이번 경우에서는 `_foreign_keys` 옵션이 있어서 그런지 자동으로는 안 되는군요. `status_id` 를 위한 foreign_keys 항목을 추가합니다.

테스트 데이터는 다음과 같이 변경합니다.
# data/fixtures/fixtures.yml
Status:
  opened
    tag: opened
    title: Opened
  closed
    tag: closed
    title: Closed
   
Item:
  first:
    title: first item
    description: first list item
    status_id: opened
    depth: 0
  second:
    title: second item
    description: second list item
    status_id: opened
    depth: 0
  third:
    title: third item
    description: child of first item
    status_id: opened
    parent_id: first
  fourth:
    title: fourth item
    description: child of first item
    status_id: closed
    parent_id: first
  fifth:
    title: fifth item
    description: child of third item
    status_id: closed
    parent_id: third
  sixth:
    title: sixth item
    description: child of fifth item
    status_id: opened
    parent_id: fifth
  seventh:
    title: seventh item
    description: child of fourth item
    status_id: opened
    parent_id: fourth
이제 `symfony propel-build-all-load item` 명령으로 테이블을 생성하고, 테스트데이터를 입력합니다. 이후 테이블을 살펴보면 데이터가 입력된 것을 확인하실 수 있습니다.

사용자 삽입 이미지

item 테이블

사용자 삽입 이미지

status 테이블



2. Item 클래스의 `->setStatusId()` 메쏘드 오버라이드
BaseItem 클래스의 `->setStatusId()` 메쏘드는 `status_id` 필드에 저장될 아이디를 지정합니다. 하지만 이 자료는 숫자형이기 때문에, 나중에 상태 변경을 할때 문제가 생길 수 있습니다. 따라서 여기서는 `status` 테이블의 `tag` 필드값을 바탕으로 `item` 의 `status` 를 지정할 수 있도록 해보겠습니다.
# lib/model/StatusPeer.php
  static function getOneByTag($value)
  {
    $c = new Criteria;
    $c->add(self::TAG, $value);
    return self::doSelectOne($c);
  }
# lib/model/Item.php
  public function setStatusId($value) {
    if (is_numeric($value)) {
      $status_id = $value;
    }
    else {
      $status = StatusPeer::getOneByTag($value);
      $status_id = $status->getId();
    }
    parent::setStatusId($status_id);
  }
이제 $item->setStatusId('opened') 와 같이 함으로써 상태를 지정할 수 있게 되었습니다.


3. status 테이블과 조인
`item/list` 액션에서 `status` 관련 정보는 `$item->getStatus()` 를 함으로써 얻을 수 있습니다. 하지만 이는 추가적인 데이터베이스 쿼리를 발생시킵니다. 따라서 미리 조인을 하여서 이런 추가적인 쿼리를 하지 않도록 할 수 있습니다. `lib/model/itemPeer.php` 의 `getAllByThread()` 를 아래와 같이 수정합니다.
  static function getAllByThread()
  {
    $c = new Criteria;
    $c->addDescendingOrderByColumn(self::THREAD);
    $c->addAscendingOrderByColumn(self::LFT);
    return self::doSelectJoinStatus($c);
  }

4. View 수정
View 를 수정하기 위해, 먼저 `lib/model/Status.php` 에 `__toString()` 메쏘드를 추가합니다.
  public function __toString()
  {
    return $this->getTitle();
  }
이제 `$item->getStatus()` 호출을 통해서 `status` 테이블의 `title` 필드를 출력할 수 있습니다. (이렇게 하지 않는다면 `$item->getStatus()->getTitle()` 을 해야겠지요..) 마지막으로 템플릿을 수정합니다.

<style>
.closed
{
  text-decoration: line-through;
}
</style>

<p style="margin-top:20px">
<h1>Adjacent List Items</h1>

<table>
<thead>
<tr>
  <th>Id</th>
  <th>Parent</th>
  <th>Thread</th>
  <th>Depth</th>
  <th>Title</th>
  <th>Description</th>
  <th>Lft</th>
  <th>Rgt</th>
  <th>Status</th>
</tr>
</thead>
<tbody>
<?php foreach ($items as $item): ?>
<tr class="<?php echo $item->getStatus()->getTag() ?>">
    <td><?php echo link_to($item->getId(), 'item/show?id='.$item->getId()) ?></td>
      <td><?php echo $item->getParentId() ?></td>
      <td><?php echo $item->getThread() ?></td>
      <td><?php echo $item->getDepth() ?></td>
      <td><?php echo $item->getTitle() ?></td>
      <td><?php echo $item->getDescription() ?></td>
      <td><?php echo $item->getLft() ?></td>
      <td><?php echo $item->getRgt() ?></td>
      <td><?php echo $item->getStatus() ?></td>
  </tr>
<?php endforeach; ?>
</tbody>
</table>
사용자 삽입 이미지

List 템플릿



5. AJAX Interaction 추가
`listSuccess.php` 템플릿에 다음을 추가하여 AJAX 링크를 만든다.
<?php foreach ($items as $item): ?>
<tr class="<?php echo $item->getStatus()->getTag() ?>" id="item_<?php echo $item->getId() ?>">
    <td><?php echo link_to($item->getId(), 'item/show?id='.$item->getId()) ?></td>
      <td><?php echo $item->getParentId() ?></td>
      <td><?php echo $item->getThread() ?></td>
      <td><?php echo $item->getDepth() ?></td>
      <td><?php echo $item->getTitle() ?></td>
      <td><?php echo $item->getDescription() ?></td>
      <td><?php echo $item->getLft() ?></td>
      <td><?php echo $item->getRgt() ?></td>
      <td><?php echo link_to_remote($item->getStatus()->getTitle(), array(
        'url'       => 'item/toggle?id='.$item->getId(),
        'method'    => 'post',
        'complete'  => "$('item_".$item->getId()."').className=request.responseText;",
      )) ?></td>
  </tr>
<?php endforeach; ?>
그리고  `Item` 모듈에 `toggle` 액션을 추가하고, 클래스에 `toggle` 메쏘드를 추가하여 AJAX 결과를 처리할 수 있도록 한다.
# apps/item/modules/item/actions/actions.class.php
  public function executeToggle ()
  {
    $item = ItemPeer::retrieveByPk($this->getRequestParameter('id'));
    $this->forward404Unless($item);
    
    $status_tag = $item->toggleStatus();
    $this->status_tag = $status_tag;
  }
# lib/model/Item.php
  public function toggleStatus()
  {
    switch ($this->getStatus()->getTag())
    {
      case 'opened':
        $status_tag = 'closed';
        break;
      default:
        $status_tag = 'opened';
    }
    $this->setStatusId($status_tag);
    $this->save();
    return $status_tag;
  }
# apps/item/modules/item/templates/toggleSuccess.php
<?php echo $status_tag ?>

6. Next Steps
 - Nested status toggle: If a parent node is toggled to closed, its child nodes would be closed. And if every child nodes are closed, their parent node should be closed also.
 - Reduce database query: In toggle action, there are three queries that we can reduce.

'Programming' 카테고리의 다른 글

FLEX 구매대행 요금 계산기  (3) 2007/01/11
Flex Gnuboard Frontend  (0) 2007/01/09
Extending Symfony Adjacent List  (0) 2007/01/08
Symfony Adjacent List  (0) 2007/01/03
AJAX Post-It 만들기  (1) 2006/12/29
Building Protein List using BeautifulSoup and BioPython  (0) 2006/12/27
tags : Ajax, Symfony
Trackback 0 : Comment 0

Symfony Adjacent List

Programming 2007/01/03 16:02
What I am going to build today is adjacent list. Data with hierarchic structure is very common. Today's output will looks like the image below. I, fisrtly, wanted to make a sortable and inplace editable list, but it was way too difficult. I am still learning how symfony is working. Today's work took whole my morning plus three hours. So I will do it later if I have some free time. (Anyway, symfony's in_place_edit_tag wrapper does not fully support what I want)

사용자 삽입 이미지

Adjacent List


1. Project preparation
# symfony init-project sandbox
# symfony init-app item
# mysqladmin -u root create item
# modify config/database.yml, config/propel.ini

2. Writing Schema and CRUD generation
# config/schema.yml
propel:
  item:
    _attributes: { phpName: Item }
    id:
    parent_id:    integer
    thread:       integer
    depth:       { type: tinyint, default: 0 }
    title:        varchar(255)
    description:  longvarchar
    _foreign_keys:
      - self_joined_keys
        foreign_table: item
        on_delete: cascade
        references:
          - { local: parent_id, foreign: id }

# symfony propel-build-model
# symfony propel-build-sql
# symfony propel-insert-sql
# symfony propel-generate-crud item item Item (app name, module name, and model name, respectively)

사용자 삽입 이미지

Database structure


3. Testdata
# data/fixtures/testdata.yml
Item:
  first:
    title: first item
    description: first list item
    depth: 0
  second:
    title: second item
    description: second list item
    depth: 0
  third:
    title: third item
    description: child of first item
    parent_id: first
  fourth:
    title: fourth item
    description: child of first item
    parent_id: first
  fifth:
    title: fifth item
    description: child of third item
    parent_id: third
  sixth:
    title: sixth item
    description: child of fifth item
    parent_id: fifth
  seventh:
    title: seventh item
    description: child of fourth item
    parent_id: fourth

# symfony propel-load-data item
사용자 삽입 이미지

Loaded Testdata


4. Handle child nodes
To register a child node, we will make a link in `showSuccess.php`
# apps/item/modules/item/templates/showSuccess.php
<?php echo link_to('create child', 'item/create?parent='.$item->getId()) ?>
`item/create` action checks request parameter whether a `parent` parameter exists, and if it exists, the action will fetch the parent object. (We don't need parent information when a item is edited.)
# apps/item/modules/item/action/action.class.php
public function executeCreate ()
{
  $this->item = new Item();
   
  $this->parent = ItemPeer::retrieveByPk($this->getRequestParameter('parent'));

  $this->setTemplate('edit');
}

public function executeEdit ()
{
  $this->item = ItemPeer::retrieveByPk($this->getRequestParameter('id'));

  $this->parent = null;
   
  $this->forward404Unless($this->item);
}
Open and modify `editSuccess.php` template. We don't need to display thread number or depth number in this form, so remove them. And only if when a user is adding a child node, parent_id is needed.
# apps/item/modules/item/templates/showSuccess.php
<tbody>
<?php if ($parent): ?>
<tr>
  <th>Parent:</th>
  <td>
<?php echo object_input_tag($item, 'getParentId', array (
  'type' => 'hidden',
), $parent->getId()) ?>
    <?php echo $parent->getTitle() ?>
  </td>
</tr>
<?php endif; ?>
<tr>
  <th>Title:</th>
  <td><?php echo object_input_tag($item, 'getTitle', array (
  'size' => 80,
)) ?></td>
</tr>
<tr>
  <th>Description:</th>
  <td><?php echo object_textarea_tag($item, 'getDescription', array (
  'size' => '30x3',
)) ?></td>
</tr>
</tbody>

5. Override Item model
Now everything is prepared for child nodes registering. Now we need some codes actually save child nodes and set parent_id, thread, and depth for them. Let's override `Item` class.
# lib/model/Item.php
public function save($con = null) {
  // New records need to be initialized with same parents and thread number
  if (!$this->getId())
  {
    $con = Propel::getConnection(ItemPeer::DATABASE_NAME);
    try
    {
      $con->begin();
      
      parent::save();
      
      // Insert a record in a existing tree
      if ($this->parent_id)
      {
        $parent = ItemPeer::retrieveByPk($this->parent_id);
        $this->setParentId($parent->getId());
        $this->setThread($parent->getThread());
        $this->setDepth($parent->getDepth()+1);
      }
      else
      {
        $this->setParentId($this->getId());
        $this->setThread($this->getId());
      }
      parent::save();
      
      $con->commit();
    }
    catch (Exception $e)
    {
      $con->rollback();
      throw $e;
    }
  }
  else
  {
    parent::save();
  }
}

public function delete($con = null) {
  if ($this->parent_id != $this->getId())
  {
    $con = Propel::getConnection(ItemPeer::DATABASE_NAME);
    try
    {
      $con->begin();
      
      // Remove cascading records
      $delete = new Criteria();
      $delete->add(ItemPeer::PARENT_ID, $this->getId());
      BasePeer::doDelete($delete, $con);
     
      parent::delete();

      $con->commit();
    }
    catch (Exception $e)
    {
      $con->rollback();
      throw $e;
    }
  }
  else
  {
    parent::delete();
  }
}
Based on Propel Guide[각주:1], it supports cascading delete. But I am not sure what will happen, so I just wrote some code for cascading delete.

Now Let's test it using testdata.

# symfony propel-load-data item
사용자 삽입 이미지

Now our list has a basic representation of hierarchic structure. However, how can we display them as a nested list?

6. Implement Adjacent List
Now it's time to implement adjacent list. I will use an algorithm from the article "Storing Hierarchical Data in a Database[각주:2]. Two images from the same article will be helpful to understand what is it and how to implement it.

사용자 삽입 이미지사용자 삽입 이미지

First, we need a left and right field in our data model. Since 'left' and 'right' are one of mysql keywords, I will use 'lft' and 'rgt' instead.
# config/schema.yml
propel:
  item:
    _attributes: { phpName: Item }
    id:
    parent_id:    integer
    thread:       integer
    depth:       { type: tinyint, default: 0 }
    title:        varchar(255)
    description:  longvarchar
    lft:         { type: integer, default: 1 }
    rgt:         { type: integer, default: 2 }
    _foreign_keys:
      - self_joined_keys
        foreign_table: item
        on_delete: cascade
        references:
          - { local: parent_id, foreign: id }

# symfony propel-build-all (It will do 'propel-build-model', 'propel-build-sql', and 'propel-insert-sql')

Then, let's modify `Item` model, so that the model can insert and remove dangled nodes.
# lib/model/Item.php#->save() Method
        if ($this->parent_id)
        {
          $parent = ItemPeer::retrieveByPk($this->parent_id);
          $this->setParentId($parent->getId());
          $this->setThread($parent->getThread());
          $this->setDepth($parent->getDepth()+1);
          // Last in, First out
          $this->setLft($parent->getLft()+1);
          $this->setRgt($this->getLft()+1);
         
          // Last in, Last out
          // $this->setLft($parent->getRgt());
          // $this->setRgt($this->getLft()+1);
         
          $sql = "UPDATE ".ItemPeer::TABLE_NAME." SET ".ItemPeer::RGT." = ".ItemPeer::RGT." + 2 WHERE ".ItemPeer::THREAD." = ".$this->getThread()." AND ".ItemPeer::RGT." >= ". $this->getLft();
          $stmt = $con->prepareStatement($sql);
          $stmt->executeUpdate();
          $stmt->close();
         
          $sql = "UPDATE ".ItemPeer::TABLE_NAME." SET ".ItemPeer::LFT." = ".ItemPeer::LFT." + 2 WHERE ".ItemPeer::THREAD." = ".$this->getThread()." AND ".ItemPeer::LFT." >= ". $this->getLft();
          $stmt = $con->prepareStatement($sql);
          $stmt->executeUpdate();
          $stmt->close();
        }
# lib/model/Item.php#->save() Method    
      try
      {
        $con->begin();
       
        // Remove cascading records
        $delete = new Criteria();
        $delete->add(ItemPeer::LFT, $this->getLft(), Criteria::GREATER_THAN);
        $delete->addAnd(ItemPeer::LFT, $this->getRgt(), Criteria::LESS_THAN);
        BasePeer::doDelete($delete, $con);

        $cascading_records = $this->getRgt() - $this->getLft() + 1;
       
        $sql = "UPDATE ".ItemPeer::TABLE_NAME." SET ".ItemPeer::RGT." = ".ItemPeer::RGT." - ".$cascading_records." WHERE ".ItemPeer::THREAD." = ".$this->getThread()." AND ".ItemPeer::RGT." >= ". $this->getLft();
        $stmt = $con->prepareStatement($sql);
        $stmt->executeUpdate();
        $stmt->close();
       
        $sql = "UPDATE ".ItemPeer::TABLE_NAME." SET ".ItemPeer::LFT." = ".ItemPeer::LFT." - ".$cascading_records." WHERE ".ItemPeer::THREAD." = ".$this->getThread()." AND ".ItemPeer::LFT." >= ". $this->getLft();
        $stmt = $con->prepareStatement($sql);
        $stmt->executeUpdate();
        $stmt->close();
       
        parent::delete();

        $con->commit();
      }
Yes. We don't have any elegant method to wrap an update query like "UPDATE item SET lft = lft + 2 WHERE ...". I thought I could you Criteria::CUSTOM[각주:3], but it turned out I can't use it for an update query. Maybe Doctrine supports it. I will check them out later.

Then, we need a method to fetching those items in certain order.
# lib/model/ItemPeer.php
  static function getAllByThread()
  {
    $c = new Criteria;
    $c->addDescendingOrderByColumn(self::THREAD);
    $c->addAscendingOrderByColumn(self::LFT);
    return self::doSelect($c);
  }
# apps/item/modules/item/action/action.class.php
  public function executeList ()
  {
    $this->items = ItemPeer::getAllByThread();
  }
Now let's test our modification.

# symfony propel-load-data item
사용자 삽입 이미지

# item/list action
사용자 삽입 이미지

7. Validating form


8. Other References
  1. How to make sortable lists[각주:4]
  2. Symfony model[각주:5]
  1. http://propel.phpdb.org/docs/user_guide/chapters/Relationships.html [본문으로]
  2. http://www.sitepoint.com/article/hierarchical-data-database [본문으로]
  3. http://www.symfony-project.com/snippets/snippet/53 [본문으로]
  4. http://www.symfony-project.com/book/trunk/sortable [본문으로]
  5. http://www.oxyscripts.com/manuals/symfony/model.html [본문으로]

'Programming' 카테고리의 다른 글

Flex Gnuboard Frontend  (0) 2007/01/09
Extending Symfony Adjacent List  (0) 2007/01/08
Symfony Adjacent List  (0) 2007/01/03
AJAX Post-It 만들기  (1) 2006/12/29
Building Protein List using BeautifulSoup and BioPython  (0) 2006/12/27
Symfony Form Helper - object_select_tag  (0) 2006/12/24
Trackback 0 : Comment 0

AJAX Post-It 만들기

Programming 2006/12/29 13:37
PHP 웹 프레임워크인 Symfony[각주:1] 를 이용하여 AJAX 기반의 어플리케이션을 만들어보겠습니다. 그 중에서도 특히 Script.aculo.us[각주:2] 의 draggable_element 와 droppable_element 를 활용하여 Post-It 스타일의 메모장을 만들어볼 계획입니다.

1. 먼저 symfony 명령을 활용하여 프로젝트를 준비합니다. 프로젝트명은 note, 어플리케이션 이름도 note 로 하겠습니다.
# mkdir note
# cd note
# symfony init-project note
# symfony init-app note

2. 아파치 설정을 변경하시거나 해당 디렉토리를 웹이 접근가능한 폴더로 변경합니다. 그리고 테스트를 하시면 'Symfony Project Created' 페이지를 만나실 수 있습니다. 참고로 제 웹 설정을 올려보겠습니다.
# /etc/httpd/users/sunhwan.conf
<Directory "/usr/local/lib/php/data/symfony/web/sf">
    AllowOverride All
    Allow from all
</Directory>

<VirtualHost *:80>
    ServerName note
    DocumentRoot "/Users/sunhwan/working/note/web"
    DirectoryIndex index.php
    Alias /sf /usr/local/lib/php/data/symfony/web/sf

    <Directory "/Users/sunhwan/working/note/web">
        AllowOverride All
        Allow from all
    </Directory>
</VirtualHost>
사용자 삽입 이미지

좀 더 상세한 프로젝트 생성방법은 심포니 홈페이지[각주:3] 을 참고하시면 됩니다.

3. 이제 우리가 사용할 어플리케이션이 사용할 데이터베이스를 설정합니다. 지금 우리가 만들 어플리케이션은 규모도 크지 않고 다른 어플리케이션들과의 연동도 별로 없기때문에 XML 이나 다른 파일 형태로 저장하는 것도 나쁘지 않을 것 같았지만, 사실 Symfony 에서 이를 어떻게 구현할지 익숙하지 않아서 일단 데이터베이스를 사용하기로 했습니다.

note/config 디렉토리에 있는 database.yml, propel.ini, schema.yml 파일을 수정합니다.
# database.yml
all:
  propel:
    class:          sfPropelDatabase
    param:
      dsn:          mysql://root:@localhost/Note

# propel.ini
propel.database.url        = mysql://root@localhost/note

# schema.yml
propel:
  note:
    _attributes: { phpName: Note }
    id:
    title:    varchar(255)
    contents: longvarchar
    x:        smallint
    y:        smallint
    z:        tinyint
    width:    smallint
    height:   smallint
사실 이러면 안 되지만, 저는 방화벽이 설치된 로컬에서 MySQL 을 돌리는 관계로, root 비밀번호가 없습니다. database.yml 이나 propel.ini 파일의 아이디 뒷부분에 자신에 해당하는 비밀번호를 입력하면 됩니다.

4. 이제 데이터베이스를 생성합니다. Symfony 는 schema.yml 에 입력된 정보를 바탕으로 데이터베이스 구조를 생성할 수 있습니다.
symfony propel-build-model
symfony propel-build-sql
mysqladmin -u root create database note
symfony propel-insert-sql
데이터베이스에 접속해보시면 note 테이블이 생성된 것을 확인하실 수 있습니다. 또한 데이터베이스 자료와 PHP 자료형을 연결시켜주는 역할을 하는 모델들이 note/lib/model 디렉토리에 생긴것을 확인하실 수 있습니다.

5. 이제 생성된 모델을 이용해서 기본적인 데이터 입력, 호출, 수정, 그리고 삭제 (CRUD - Create, read, update, and delete) 를 하는 모듈을 만들어 보겠습니다. 모델명은.. 역시 note 입니다.
symfony propel-generate-crud note note Note
참고로 앞에서부터 어플리케이션, 모듈, 그리고 데이터모델 순서입니다. 즉, 만약 blog 라는 어플리케이션을 만들고, 그 아래 Article 이라는 데이터모델을 이용하여 post 라는 모듈을 만든다면 'symfony propel-generate-crud blog post Article' 이 되는 것입니다.
 
사용자 삽입 이미지

CRUD - List


사용자 삽입 이미지

CRUD - Show


사용자 삽입 이미지

CRUD - Create/Edit


6. 이제 템플릿을 수정할 차례입니다. 다른 부분이야 그대로 두고, note/list 액션의 템플릿을 바꿔서 노트형식으로 보이도록 할 계획입니다. 또한 이부분 draggable/dropperable element 들을 설정하여 만약 개별 노트의 위치가 변경되는 경우에 데이터베이스를 변경하도록 할 것입니다.

먼저 note/apps/note/modules/note/templates/listSuccess.php 파일을 수정합니다.
<div id='notepad' class='notepad' style='position: absolute; top: 5px; left: 5px;'>

<h1>note</h1>

<?php foreach ($notes as $note): ?>

  <div id='note_<?php echo $note->getId() ?>' class='note' style="
    position: absolute;
    left: <?php echo $note->getX() ?>px;
    top: <?php echo $note->getY() ?>px; ">
    <h2><?php echo link_to($note->getTitle(), 'note/show?id='.$note->getId()) ?></h2>
   
    <?php echo $note->getContents() ?>
  </div>
 
<?php endforeach; ?>

<?php echo link_to ('create', 'note/create') ?>

</div>
note/web/main.css 에 아래 내용을 추가합니다.
.note
{
  background-color: #FFFFCC;
  width: 200px;
  height: 200px;
}
그리고 테스트를 해보신다면 아래와 같은 화면을 보실 수 있습니다. 각각의 노트는 데이터베이스에 있는 X, Y 값을 사용하여 위치가 결정될 것입니다.
사용자 삽입 이미지

Post It!?


7. 이제 마지막으로 AJAX 액션을 추가할 차례입니다. Symfony 에는 Prototype 과 Script.aclo.us 가 기본적으로 내장되어 있기 때문에 따로 설치할 필요는 없습니다. 개체를 드래그 앤 드랍이 가능하도록 만들어주는 명령은 draggable_element 입니다. 또한 어떠한 개체가 자신의 위에 드랍되었을때 특정한 액션을 일으킬 수 있도록 하기 위해서 drop_receiving_element 를 사용합니다.

apps/note/modules/note/templates/listSuccess.php 를 여시고, 다음 내용을 맨 위쪽에 입력합니다.
<?php echo use_helper('Javascript') ?>
아래 내용은 'endforeach' 위에 입력합니다.
<?php echo draggable_element('note_'.$note->getId(), array()) ?>
그리고 아래내용은 파일의 제일 아래쪽에 입력합니다.
<?php echo drop_receiving_element('notepad', array(
  'url' => 'note/translate',
  'with' => "'id='+encodeURIComponent(element.id)+'&x='+element.style.left+'&y='+element.style.top")
) ?>
파일 전체를 다시 살펴보시려면 클릭하십시오.

more..


7. 마지막으로 apps/note/modules/actions/action.class.php 에 'executeTranslate' 을 추가하면 끝입니다.
  public function executeTranslate ()
  {
    $tmp = split('_', $this->getRequestParameter('id', ''));
    $note_id = $tmp[1];
   
    $note = NotePeer::retrieveByPk($note_id);
    $this->forward404Unless($note);

    $note->setId($note_id);
    $note->setX($this->getRequestParameter('x', 0));
    $note->setY($this->getRequestParameter('y', 0));
    $note->setZ($this->getRequestParameter('z', 0));

    $note->save();

    return true;
  }


  1. http://www.symfony-project.com [본문으로]
  2. http://script.aculo.us [본문으로]
  3. http://www.symfony-project.com/book/trunk/project_creation [본문으로]

'Programming' 카테고리의 다른 글

Extending Symfony Adjacent List  (0) 2007/01/08
Symfony Adjacent List  (0) 2007/01/03
AJAX Post-It 만들기  (1) 2006/12/29
Building Protein List using BeautifulSoup and BioPython  (0) 2006/12/27
Symfony Form Helper - object_select_tag  (0) 2006/12/24
Adapter Pattern in PHP  (0) 2006/12/20
tags : Ajax, Symfony
Trackback 1 : Comment 1

Building Protein List using BeautifulSoup and BioPython

Programming 2006/12/27 20:32
1. BeautifulSoup[각주:1]
 - Parsing an HTML is incredibly easy.
from BeautifulSoup import BeautifulSoup
import urllib2
doc = urllib2.urlopen('http://www.google.com').read()
soup = BeautifulSoup(doc)
 - Navigating through parsed tree is simple.
soup.contents[0].name
# u'html'
soup.head.next.name
# u'title'
soup.head.next.nextSibling.name
# u'body'
 - Finding a tag / tags is powerful.
soup.findAll('div', id='sectionName')
soup.findAll('div', {'class': 'singleprotein'})
soup.find('p', align='center')
soup.find('p', align=re.compile('^b.*')
 - You don't need to worry about modifying the parsed tree.
soup = BeautifulSoup('<a1></a1><a><b>Amazing content<c><d></a><a2></a2>')
soup.a1.nextSibling
# <a><b>Amazing content<c><d></d></c></b></a>
soup.a2.previousSibling
# <a><b>Amazing content<c><d></d></c></b></a>

subtree = soup.a
subtree.extract()

print soup
# <a1></a1><a2></a2>
soup.a1.nextSibling
# <a2></a2>
soup.a2.previousSibling
# <a1></a1>

2. BioPython[각주:2]
 - BioPython is a group of python libraries for biology.
 - Searching biology db and parsing it is a piece of cake.
from Bio import db
from Bio.PDB import PDBParser
db
# db, exporting 'embl', 'embl-dbfetch-cgi', 'embl-ebi-cgi', 'embl-fast', 'embl-xembl-cgi', 'embl-xml', 'fasta', 'fasta-sequence-eutils', 'genbank-nucleotide', 'genbank-protein', 'interpro', 'interpro-ebi-cgi', 'medline', 'medline-eutils', 'nucleotide-genbank-eutils', 'pdb', 'pdb-ebi-cgi', 'pdb-rcsb-cgi', 'prodoc', 'prodoc-expasy-cgi', 'prosite', 'prosite-expasy-cgi', 'protein-genbank-eutils', 'swissprot', 'swissprot-expasy-cgi', 'swissprot-usmirror-cgi'

f = db['pdb']['1kdx']
p = PDBParser()
s = p.get_structure('1kdx', f)
s[0]
# <model id=0>
s[0].child_list
# [<Chain id=A>, <Chain id=B>]
len(s[0].child_list[0].child_list)
# 81 (81 residues)
3. Building Protein List
 - The objective is building a table of membrane-proteins so we can get some insight of what to choose.
 - Protein list from http://blanco.biomol.uci.edu/Membrane_Proteins_xtal.html is used.
 - It didn't provide size and the image information, I downloaded pdb to calculate how many residues are there and downloaded images from opm[각주:3] website.

4. Protein List
 - At first, I went through the original protein list and fetch pdb files and image files, then build the whole list.
 - protein_list.py, protein_list.html, protein_list.pdf
 -



5. Inspected List
 - Then, I extract rows, which don't meet my needs.
 - inspect.py, inspected_list.html, inspected_list.pdf
 -



  1. http://www.crummy.com/software/BeautifulSoup/ [본문으로]
  2. http://biopython.org/wiki/Main_Page [본문으로]
  3. http://opm.phar.umich.edu [본문으로]

'Programming' 카테고리의 다른 글

Symfony Adjacent List  (0) 2007/01/03
AJAX Post-It 만들기  (1) 2006/12/29
Building Protein List using BeautifulSoup and BioPython  (0) 2006/12/27
Symfony Form Helper - object_select_tag  (0) 2006/12/24
Adapter Pattern in PHP  (0) 2006/12/20
Symfony v1 beta 2 has been released  (0) 2006/12/20
Trackback 9 : Comment 0

Symfony Form Helper - object_select_tag

Programming 2006/12/24 09:37

Building an select box filled with various options are quite common for web programmers. For example, let's assume we are making a form to write an post for a blog. We have a category table and we want to display categories that we can use. Basically what we can do is, select categories and display its string field. In symfony, we can use 'object_select_tag', instead.  Based on its declaration, it is called by:
object_select_tag(object $object, string $method, array $option, mixed $default)
I am building project ticket managing system. So I have tables named ticket, project, and milestone. A project can have number of milestone, and a ticket is confined to a project and a milestone.
+---------------+---------+------------+
| ticket           | project  | milestone |
+---------------+---------+------------+
| id                 | id         | id             |
| title              | title     | tag           |
| project_id     |            | project_id |
| milestone_id |            |                |
+---------------+---------+------------+
Symfony generated templates for ticket will have 'object_select_tag' by default. But it will display id numbers of milestone for selection box. Since the helper utilitze object's 'toString()' method, we need to define '__toString()' method for 'Milestone' class to fill selection box with milestone tag.
public function __toString() {
    return $this->getTag();
}
Now the selection box displays tag of milestones. However, it displays every milestones in the table. We want to display milestones only for this project. So how we can do that? Let's assume we have a variable named 'project', which stores project id. Then we can change 'object_select_tag' command:
object_select_tag($ticket, 'getMilestoneId', array (
  'related_class' => 'Milestone', '
))
as below:
object_select_tag($ticket, 'getMilestoneId', array (
  'related_class' => 'Milestone', 'peer_method' => 'getProjectMilestones'
))
and add 'getProjectMilestones' method in 'MilestonePeer' class:
public static function getProjectMilestones ()
{
  $criteria = new Criteria();
  $project = sfContext::getInstance()->getRequest()->getParameter('project');
  $criteria->add(MilestonePeer::PROJECT_ID, $project);

   return self::doSelect($criteria);
}
Check out the differences:

사용자 삽입 이미지

object_select_tag default

사용자 삽입 이미지

object_select_tag changed








So the helper uses options to populate select box, and fetch default value from $object->$method if it doesn't have default value. Other helpers and more detail can be found in the helper chapter[각주:1]  at symfony online documentation
  1. http://www.symfony-project.com/book/trunk/templating_form_helpers [본문으로]

'Programming' 카테고리의 다른 글

Symfony Adjacent List  (0) 2007/01/03
AJAX Post-It 만들기  (1) 2006/12/29
Building Protein List using BeautifulSoup and BioPython  (0) 2006/12/27
Symfony Form Helper - object_select_tag  (0) 2006/12/24
Adapter Pattern in PHP  (0) 2006/12/20
Symfony v1 beta 2 has been released  (0) 2006/12/20
tags : helper, Symfony
Trackback 0 : Comment 0

Adapter Pattern in PHP

Programming 2006/12/20 23:03
사용자 삽입 이미지

Adapter Kit








Head First Design Pattern[각주:1] 에서는 어댑터 패턴을 아래와 같이 정의하고 있습니다.

어댑터 패턴 (Adapter Pattern) - 한 클래스의 인터페이스를 클라이언트에서 사용하고자 하는 다른 인터페이스로 변환합니다. 어댑터를 이용하면 인터페이스의 호환성 문제 때문에 같이 쓸 수 없는 클래스들을 연결해서 쓸 수 있습니다.

디자인 패턴에 관한 책을 읽어보긴 했어도, 실제 예가 없어서인지 감이 잘 오지 않았었습니다. 이번에 devshed 에 나온 아티클[각주:2] 을 읽고서 다시 패턴책을 보니 훨씬 이해가 잘 되는 것 같습니다.

어댑터 패턴은 다음과 같은 경우에 적용될 수 있습니다.
  1. 기존의 클래스를 확장해야 하는데, 상속을 할 수 없는 경우.
  2. 하나의 인터페이스를 다른 인터페이스로 변환
1.
devshed 의 경우 첫번째 경우를 PHP 코드로 설명하고 있습니다. 먼저 추상 클래스와 이를 상속받아 구현한 클래스를 선언합니다.

abstractclass AbstractDirectoryProcessor {
   private $dirPath;
   abstract public function fetchDirContent();
   abstract public function getDirInfo();
}

class DirectoryProcessor extends AbstractDirectoryProcessor {
   public function __construct($dirPath) {
       if(!is_dir($dirPath)) {
           throw new Exception('Invalid directory path!');
       }
       $this->dirPath=$dirPath;
   }
         
   public function fetchDirContent() {
       if(!$dp=opendir($this->dirPath)) {
           throw new Exception('Error opening selected directory!');
       }        
       $dircont='';        
       while($file=readdir($dp)) {
           $dircont.=$file.'<br />';     
       }         
       fclose($dp);        
               
       return $dircont;
   }
        
   public function getDirInfo() {
       $pathinfo=pathinfo($this->dirPath);        
       return 'Name of selected directory is '.$pathinfo['dirname'].' and its base name is the following: '.$pathinfo['basename'];
   }
}

'->fetchDirContent()' 메쏘드는 해당 디렉토리에 있는 파일들의 이름을 하나씩 출력하고, '->getDirInfo()' 메쏘드는 해당 디렉토리에 대한 정보를 간단하게 출력합니다. 자, 이제 이 클래스를, 상속을 이용하지 않고 어댑터 패턴을 이용하여 기능을 확장하도록 하겠습니다.

abstract class AbstractDirectoryProcessorAdapter {
     private $dirProcessor;
     abstract public function getDetailedDirInfo();
}

class DirectoryProcessorAdapter extends AbstractDirectoryProcessorAdapter {
   public function __construct(DirectoryProcessor $dirProcessor) {
       $this->dirProcessor=$dirProcessor;
   }
 
   public function getDetailedDirInfo() {
       return '<h1>Detailed information about selected directory is as follows:</h1><h2>Contents of selected directory is as  follows:</h2>'.$this->dirProcessor->fetchDirContent().'<h2>Data about selected directory is as follows:</h2>'.$this->dirProcessor->getDirInfo();
   }
}

트릭은 생성자에 있습니다. 어댑터의 생성자에서 어댑티 (인터페이스가 변활될 클래스) 를 자신의 속성으로 지정하여, 이후 확장된 메쏘드들에서 어댑티의 메쏘드를 사용합니다.

2.
Head First Design Pattern 의 어댑터 패턴 부분에 보면, 어댑터 패턴을 통해 기능을 확장하기 보다는 기존 클래스의 메쏘드들을 그대로 재정의하는 예제가 있습니다.

class MallardDuck {
   public function quack() {
       echo 'Quack';
   }

   public function fly() {
       echo "I'm flying";
   }
}

class WildTurkey {
   public function gobble() {
       echo 'Gobble gobble';
   }

   public function fly() {
       echo "I'm flying a short distance";
   }
}

class TurkeyAdapter {
   public function __construct(WildTurkey $turkey) {
       $this->turkey = $turkey;
   }

   public function quack() {
       $this->turkey->gobble();
   }

   public function fly() {
       for ($i=0; $i<5; $i++) {
            $this->turkey->fly();
       }
   }
}

만약 devshed 예제 코드를 이용해서 DirectoryProcessor 를 인자로 받는 어떤 함수를 새로 정의했다고 해보겠습니다. 하지만 어떤 이유에서인지 이 함수에서는 '->getDirInfo()' 메쏘드를 사용하는 대신에 '->getSimpleDirInfo()' 라는 메쏘드를 호출합니다. 자, DirectoryProcessor 도 이미 다른 파일들에서 사용중이므로 메쏘드 이름을 바꿀 수는 없고, 함수 내용도 바꿀 수가 없습니다. 이럴때 어댑터 패턴을 사용할 수 있습니다. 어댑터 패턴에서는 간단히 '->getSimpleDirInfo()' 메쏘드를 정의하고, 이 메쏘드에서는 어댑티의 '->getDirInfo()' 를 호출해 주면 됩니다.


Head First Design Pattern 에서는, 이 외에도 유사한 패턴으로 다음 패턴들이 있다고 합니다.
  1. 데코레이터 패턴 - 인터페이스는 바꾸지 않고 책임(기능)만 추가
  2. 퍼사드 (Facade) 패턴 - 인터페이스를 간단히 바꿈
  1. 스토리가 있는 패턴 학습법 - Head First Design Pattern, 에릭 프리먼 외, 서환수 역, 한빛미디어, 2005 [본문으로]
  2. The Basics of Implementing Adapter Objects with PHP, Alejandro Gervasio [본문으로]

'Programming' 카테고리의 다른 글

Symfony Adjacent List  (0) 2007/01/03
AJAX Post-It 만들기  (1) 2006/12/29
Building Protein List using BeautifulSoup and BioPython  (0) 2006/12/27
Symfony Form Helper - object_select_tag  (0) 2006/12/24
Adapter Pattern in PHP  (0) 2006/12/20
Symfony v1 beta 2 has been released  (0) 2006/12/20
Trackback 0 : Comment 0

Symfony v1 beta 2 has been released

Programming 2006/12/20 09:37
Symfony-1.0 beta 2 has been released.

Install? pear install symfony/symfony-beta
and don't forget 'symfony cc'

'Programming' 카테고리의 다른 글

Symfony Adjacent List  (0) 2007/01/03
AJAX Post-It 만들기  (1) 2006/12/29
Building Protein List using BeautifulSoup and BioPython  (0) 2006/12/27
Symfony Form Helper - object_select_tag  (0) 2006/12/24
Adapter Pattern in PHP  (0) 2006/12/20
Symfony v1 beta 2 has been released  (0) 2006/12/20
tags : Symfony
Trackback 0 : Comment 0