Caution: You are browsing the legacy symfony 1.x part of this website.

symfony advent calendar day nine: local improvements

1.0

지난 줄거리

8일째 에는 askeet 에 AJAX 인터렉션들을 별 어려움 없이 추가해보았습니다. 이제 어플리케이션이 상당히 쓸만해지긴 했지만, 몇군데 더 개선을 하는 것도 좋을 것입니다. 질문의 본문 (body) 에는 리치 텍스트를 입력할 수 있도록 해야 할 것이고, URI 에는 프라이머리 키가 보여서는 안 될 것입니다. 이것들을 추가하는 것이 symfony 에서는 어려운 것이 아닙니다. 오늘 우리는 여러분이 지금껏 배운것들을 연습하고 MVC 구조의 모든 계층구조들을 조작하는 방식을 점검할 수 있도록 할 것입니다.

질문과 대답들에 리치 텍스트 입력하기

Markdown

질문과 대답들에는 아직 일반 텍스트만을 입력할 수 있습니다. 기본적인 텍스트 포맷팅 - 굵은글씨, 이탤릭, 하이퍼링크, 이미지, 기타등등 - 을 지원하기 위해서 우리는 외부 라이브러리를 사용할 것입니다.

만약 텍스트 형식의 심포니 문서들을 보신적이 있다면, 우리가 Markdown 의 광팬이라는 것을 눈치채셨을 것입니다. Markdown 은 텍스트를 HTML 형식으로 변환해 주는 툴이며, 텍스트 포맷팅을 위한 문법이기도 합니다. 위키 또는 포럼의 문법으로써 Markdown 의 가장 큰 장점은, 예를 들자면, 일반텍스트 자체가 매우 읽기 쉽다는 것입니다.

Test Markdown text
------------------

This is a **very simple** example of [Markdown][1].
The best thing about markdown is its _auto-escape_ feature for code chunks:

    <a href="http://www.symfony-project.com">link to symfony</a>

>The `<` and `>` are properly escaped as `&lt;` and `&gt;`,
>and are not interpreted by any browser

[1]: http://daringfireball.net/projects/markdown/   "Markdown"

위의 마크다운은 아래와 같이 표시될 것입니다.

Test Markdown text

This is a very simple example of Markdown. The best thing about markdown is its auto-escape feature for code chunks:

 <a href="http://www.symfony-project.com">link to symfony</a>

The < and > are properly escaped as &lt; and &gt;, and are not interpreted by any browser

Markdown 라이브러리

Markdown 은 원래 Perl 로 작성되었지만, PHP 버전의 라이브러리도 PHP Markdown 에서 얻을 수 있습니다. 해당 웹사이트로 가셔서 markdown.php 파일을 다운로드 받아 askeet 프로젝트의 lib 폴더에 넣어두십시오. 이게 다입니다. 이제 아래와 같이 선언함으로써, 이 라이브러리는 askeet 어플리케이션의 다른 모든 클래스들에서 사용이 가능합니다.

require_once('markdown.php');   

우리는 텍스트를 출력할 때마다 Markdown 을 통해 텍스트를 HTML 로 변환하는 대신에 질문이 생성될 때에 텍스트를 HTML 로 변환하고 이를 Question 테이블에 저장하는 방식을 사용할 것입니다. 여러분들은 이제 이런 모델 확장같은 것이 전혀 낯설지 않을 것입니다.

모델 확장하기

먼저 schema.xml 파일을 열어 Question 테이블에 컬럼을 추가합니다.

<column name="html_body" type="longvarchar" />

그리고 모델을 다시 생성하고 데이터베이스를 업데이트 합니다.

$ symfony propel-build-model
$ symfony propel-build-sql
$ symfony propel-insert-sql

setBody 메쏘드를 재정의하기

Question 클래스의 ->setBody() 메쏘드가 호출될때, html_body 컬럼 역시, Markdown 을 통해 본문의 일반 텍스트를 변환하여, 업데이트가 되어야 합니다. askeet/lib/model/Question.php 모델 파일을 먼저 열고, 다음을 입력합니다.

public function setBody($v)
{
  parent::setBody($v);
 
  require_once('markdown.php');
 
  // strip all HTML tags
  $v = htmlentities($v, ENT_QUOTES, 'UTF-8');
 
  $this->setHtmlBody(markdown($v));
}

HTML 으로의 변환을 수행하기 전에, htmlentities() 함수를 호출함으로써 <script> 태그를 벗겨내고, cross-site-scription (XSS) 공격을 차단할 수 있습니다.

테스트 데이터 업데이트

변환이 잘 되는지 살펴보기 위해서 테스트 데이터 (askeet/data/fixtures/test_data.yml) 의 몇몇 질문을 Markdown 형식으로 바궈보겠습니다.

Question:
  q1:
    title: What shall I do tonight with my girlfriend?
    user_id: fabien
    body:  |
      We shall meet in front of the __Dunkin'Donuts__ before dinner, 
      and I haven't the slightest idea of what I can do with her. 
      She's not interested in _programming_, _space opera movies_ nor _insects_.
      She's kinda cute, so I __really__ need to find something 
      that will keep her to my side for another evening.

  q2:
    title: What can I offer to my step mother?
    user_id: anonymous
    body:  |
      My stepmother has everything a stepmother is usually offered
      (watch, vacuum cleaner, earrings, [del.icio.us](http://del.icio.us) account). 
      Her birthday comes next week, I am broke, and I know that 
      if I don't offer her something *sweet*, my girlfriend 
      won't look at me in the eyes for another month.

데이터베이스에 테스트 데이터들을 입력합니다.

$ php batch/load_data.php

템플릿 변경

question 모듈의 showSuccess.php 템플릿을 조금 수정해야 합니다.

...
<div class="question_body">
  <?php echo $question->getHtmlBody() ?>
</div>
...

질문 목록을 표시하는 템플릿 조각파일 (_list.php) 도 길이가 제한된 질문 내용을 표시합니다.

<div class="question_body">
  <?php echo truncate_text(strip_tags($question->getHtmlBody()), 200) ?>
</div>

마지막 테스트를 위한 준비가 끝났습니다. 수정이 가해진 다음 세 페이지들을 열어보시고, 테스트 데이터들이 HTML 포맷으로 표시되는지 잘 살펴보시기 바랍니다.

http://askeet/question/list
http://askeet/recent
http://askeet/question/show/stripped_title/what-shall-i-do-tonight-with-my-girlfriend    

markdown text

Answer 테이블의 body 컬럼에도 똑같은 작업을 해주어야 합니다. html_body 컬럼을 모델에 추가하고, ->setBody() 메쏘드를 재정의하고, question/show 액션에서 ->getBody() 대신 ->getHtmlBody() 메쏘드를 사용하도록 해야 합니다. 이에 관한 코드들은 위에 사용된 것들과 정확히 동일하기 때문에 다시 기술하진 않겠습니다. 다만, SVN 저장소에 있는 오늘의 코드에서는 확인 가능하실 것입니다.

id 들 숨기기

symfony 의 또다른 좋은 점은 요청값들에서 프라이머리 키를 최대한 숨길 수 있다는 것입니다. 이는 프라이머리키는 기본적으로 자동으로 증가되는 값이기 때문이기도하고, 프라이머리 키를 공개하는 것은 해커들에게 데이터베이스 레코드들에 관한 많은 정보를 흘리는 것이기 때문이기도 합니다. 덧붙여서 출력되는 URI 가 의미없는 문자열이 되버리기 때문에, 검색엔진에게도 좋지 않습니다.

사용자 프로파일 페이지를 예로 들면, 현재는 사용자의 id 를 요청값으로 넘겨받습니다. 하지만 우리가 nickname 을 유일하도록 만든다면, 이 역시 요청값으로 사용되어질 수 있을 것입니다. 한 번 위와 같이 바꿔보도록 하겠습니다.

액션 변경하기

user/show 액션 수정하기

public function executeShow()
{
  $this->subscriber = UserPeer::retrieveByNickname($this->getRequestParameter('nickname'));
  $this->forward404Unless($this->subscriber);
 
  $this->interests = $this->subscriber->getInterestsJoinQuestion();
  $this->answers   = $this->subscriber->getAnswersJoinQuestion();
  $this->questions = $this->subscriber->getQuestions();
}

모델 변경

askeet/lib/model/ 디렉토리의 UserPeer 클래스에 다음 메쏘드를 추가합니다.

public static function retrieveByNickname($nickname)
{
  $c = new Criteria();
  $c->add(self::NICKNAME, $nickname);
 
  return self::doSelectOne($c);
}

템플릿 수정

사용자 프로필로 향하는 링크들도 사용자의 id 대신 nickname 을 표시하도록 수정되어야 합니다.

question/showSuccess.phpquestion/_list.php 템플릿의 아래 줄을,

<?php echo link_to($question->getUser(), 'user/show?id='.$question->getUserId()) ?>

다음과 같이 수정합니다.

<?php echo link_to($question->getUser(), 'user/show?nickname='.$question->getUser()->getNickname()) ?>

똑같은 수정ㅇ르 answer/_answer.php 에서도 해주시기 바랍니다.

라우팅 규칙 추가하기

이 액션을 위한 라우팅 규칙을 수정해서 nickname 이 들어간 url 형태를 적절한 액션으로 연결시킬 수 있도록 합니다.

user_profile:
  url:   /user/:nickname
  param: { module: user, action: show }    

symfony clear-cache 를 실행하신 후, 수정사항을 테스트 해 보십시오.

라우팅

오늘 작성된 것들과는 별개로, 기존에 쓰여졌던 대부분의 액션들은 기본 라우팅 규칙을 사용하기 때문에 모듈 이름과 액션 이름들이 브라우저의 주소창에 표시될 것입니다. 이것들을 어떻게 수정할지는 이미 배우셨기 때문에, 바로 URL 패턴을 정의해보도록 하겠습니다. askeet/apps/frontend/config/routing.yml 파일을 수정합니다.

# question
question:
  url:   /question/:stripped_title
  param: { module: question, action: show }

popular_questions:
  url:   /index/:page
  param: { module: question, action: list, page: 1 }

recent_questions:
  url:   /recent/:page
  param: { module: question, action: recent, page: 1 }

add_question:
  url:   /add_question
  param: { module: question, action: add }

# answer
recent_answers:
  url:   /recent/answers/:page
  param: { module: answer, action: recent, page: 1 }

# user
login:
  url:   /login
  param: { module: user, action: login }

logout:
  url:   /logout
  param: { module: user, action: logout }

user_profile:
  url:   /user/:nickname
  param: { module: user, action: show }

# default rules
homepage:
  url:   /
  param: { module: question, action: list }

default_symfony:
  url:   /symfony/:action/*
  param: { module: default }

default_index:
  url:   /:module
  param: { action: index }

default:
  url:   /:module/:action/*

만약 안정버전을 사용하고 계시다면, 설정을 변경하신 후 반드시 캐시를 지우시기 바랍니다.

symfony 라우팅 규칙의 좋은 점 하나는 link_to() 헬퍼에서 module/action 을 사용하는 대신에 규칙 이름을 사용할 수 있다는 것입니다. 그렇게 하는 것이 빠를뿐 아니라 (라우팅 엔진이 라우팅 규칙이 무엇인지 확인할 필요가 없습니다), 추후에 규칙 이름은 나둔채로 해당하는 액션만 바꿀 수 있게 됩니다. 온라인 문서중 라우팅 부분 에 좀 더 자세한 내용을 찾아보실 수 있습니다.

<?php link_to('@user_profile?id='.$user->getId()) ?>
// is better than
<?php link_to('user/show?id='.$user->getId()) ?>

Askeet 은 symfony 가 권하는 방식을 따르기 때문에, 오늘 튜토리얼의 코드를 다운로드 받아보신다면, 링크 헬퍼들은 오직 규칙 라우팅 규칙이름만을 가지고 있을 것입니다. 모든 템플릿과 사용자 헬퍼들에서 action/module@rule 로 바꾸는 작업은 그리 재밌지만은 않습니다. 따라서 오늘의 교훈은, 액션을 만드실때 라우팅 규칙도 함께 만들고 라우팅 규칙 이름을 처음부터 사용하라는 것입니다.

내일 이 시간에

오늘의 내용은 어렵진 않았지만, 길었을 것입니다. 덧붙이자면, 다른 코드들에도 오늘 한 것과 비슷한 코드들이 적용되었습니다. 오늘은 실제 기능이 추가되진 않았지만, 코드는 많이 수정되었습니다.

만약 여러분이 오늘 symfony 에 대해서 많이 배운 것 같지 않다고 생각된다면, 이는 여러분이 여러분 자신의 프로젝트를 시작할 준비가 되었다는 뜻입니다. 액션을 생성하고, 모델을 변경하여 생성된 액션을 지원하고, 출력을 담당할 템플릿을 작성하고, 설정을 변경하여 새로운 액션이 어플리케이션 로직에 포함되도록 하는 것들이 심포니 개발에 있어서 가장 기본이 되는 작업들입니다.

오늘 소개된 좋은 예들은 (symfony 에서 모든 것을 구현하는 대신에 외부 라이브러리를 이용하는 것이나 프라이머리 키를 감추고, module/action 대신 라우팅 규칙 이름을 쓰는 것) 여러분의 어플리케이션을 깔끔하고 안전하게, 그리고 빠르고 유지보수 가능하게 해줄 것입니다.

하지만 askeet 어플리케이션이 끝나려면 아직 멀었습니다. 가장 부족한 기능은 새로운 질문이나 답변을 추가하는 기능입니다. 이 부분을 내일 살펴보도록 하겠습니다.

21일째에 대한 의견이 있으십니까? askeet 메일링 리스트 로 보내주십시오. 채널 고정하시고요.