저자: Gavin Andresen, 한동훈 역
원문:
http://www.onlamp.com/pub/a/php/2006/03/16/autofill-forms.html

나는 타이핑하는 것은 싫어하지만 코드를 작성하는 것은 좋아한다. 한동안 PHP에서 폼을 다루기 위해 필요한 기계적인 코드들을 타이핑했던 적이 있었는데, 그 이후로 "보다 좋은 방법"을 찾아나서기 시작했다. 이 글에서는 내가 PHP 정규식을 사용해서 폼을 처리하는 데 필요한 많은 작업들을 어떻게 제거했으며, 내가 싫어하는 타이핑을 줄이고, 코드를 작성하거나 넷핵(
NetHack)을 즐기는 데 시간을 할애하게 되었는지 설명할 것이다.

문제

제프 콕스웰(Jeff Cogswell)은
PHP와 CSS를 사용한 사용자 친화적인 폼 검증에서 일반적인 문제를 기술했다. 즉, 폼을 화면에 표시하고, 입력을 검증하고, 감사 페이지를 보여주거나, 검증이 실패하면 사용자가 입력한 값과 폼에 에러가 난 부분들을 표시해서 다시 보여주는 등의 일반적인 문제를 기술했다. 나는 깔끔하고 세련돼 보이는 웹 사이트를 위해 많은 돈을 지불하는 고객들을 위해 폼을 작성하기 때문에, 폼은 항상 멋있어야 하고, 사이트의 나머지 부분들과도 잘 조화되어야 한다.

지루한 입력에 대한 해결책

문제를 해결하는 가장 쉬운 방법은 위지윅 HTML 에디터를 사용해서 멋있는 폼을 만들고, 적절한 위치에 폼 값과 에러 메시지를 표시하기 위한 PHP 코드를 추가하는 것이다. 예를 들어, 폼에 "email,"이라는 필드 이름을 갖고 있다면, 나는 이 필드를 검증하기 위해 다음과 같은 PHP 코드를 사용한다.
$validationData = array();
$validationData['email'] =
array('isRequired', type='emailAddress');

$formErrors = validateForm($_POST, $validationData)
나쁘지 않아 보인다. validateForm() 함수를 한번 작성하기만 하면 계속에서 이것을 사용할 수 있고, 다른 값을 처리할 필요가 있을 때 마다 그에 해당하는 처리를 확장하면 된다.

에러와 올바른 값을 폼에 표시하는 것은 다소 지저분하다. 조금 간단한 HTML은 다음과 같다.
<td align="right">Email:</td>
<td><input name="email" value="" /></td>
이제, HTML과 PHP로 섞인 미로가 시작된다. <td align="right"
<?php if (isset($formErrors['email'])) {
echo 'class="error"';
} ?> >
Email:</td>
<td><input name="email"
<?php if (isset($_POST['email'])) {
echo 'value="'.$_POST['email'].'"';
} ?> /></td>
폼에 있는 모든 필드에 대해서 이를 반복하는 것은 번거롭다. 특히, 50개나 되는 주를 선택하는 "살고 있는 주를 선택하세요" 같은 드롭다운 목록에 사용된 <option> 태그의 value에 이 같은 코딩을 하는 것은 너무 끔찍한 일이어서 차라리 배관공이 되는 게 나을 것이다.

문제, 다시 설명하는 버전

PHP 코드가 없는 HTML 폼부터 시작해보자. 폼 검증 오류시 표시할 값들을 PHP 배열로 갖고 있다. 배열과 HTML을 넘겨받아서 적절한 위치에 값을 써넣은 수정된 HTML을 반환하는 PHP 함수가 필요하다. 예를 들어, <input name="email">과 $_POST['email'] = "gavin@mailinator.com"을 넘겨주면 <input name="email" value="gavin@mailinator.com">을 반환하는 것이다. PHP 설명서와 웹을 뒤적인 다음에 이런 역할을 하는 코드가 없다는 사실을 알았다. 그래서 스스로 그런 함수를 작성하고, 함수 이름을 fillInFormValues()라고 붙였다.

fillInFormValues의 동작

fillInFormValues는 $html, $values, $errors 같은 인자를 넘겨받는다.
  1. $html은 폼에 사용할 HTML 마크업을 포함한다. fillInFormValues는 어떤 HTML을 전달해도 관계없다. 즉, HTML3, HTML4, XHTML을 처리할 수 있으며, HTML 전체를 전달하거나 폼의 입력 필드가 있는 HTML의 일부분을 전달해도 상관없다. fillInFormValues는 표준 HTML 테스트를 통과한 것이면 된다. HTML을 전달할 때 </textarea>와 같은 닫기 태그를 생략하면 안된다. 일부 브라우저는 이를 허용하지 않기 때문에 화면에 표시되지 않을 수 있다.
  2. $values는 폼에서 표시할 값들을 갖고 있는 PHP 배열이며, 형식은 $values['fieldName'] = fieldValue이다. 폼 검증 오류 때문에 폼을 다시 표시하려면 $_POST, $_GET, $_REQUEST 중에 하나를 전달하면 된다.
  3. $errors는 검증 오류 메시지를 갖고 있는 PHP 배열이며, 형식은 $errors['fieldName'] = "error message"이다. fillInFormValues는 HTML에서 <ul class="error"></ul> 요소를 찾아서, 에러 메시지를 삽입한다. 폼 텍스트를 마크업 하려면 <label> 태그를 사용해야 한다. <label for="address">Street Address:</label>
    <input name="address" id="address" />
    fillInFormValues는 $errors['address']가 설정되어 있으면 있으면 해당 <label>에 class="error"을 추가한다. CSS는 label.error: color:red;와 같이 간단한 규칙을 정의하고, 에러가 발생한 부분을 빨간색으로 표시한다. <label> 태그를 사용하면 스크린 리더기나 다른 보조 도구를 사용하는 사람들에게 보다 접근성을 높일 수 있으며, 사용자가 Street Address 텍스트를 클릭하면 address 필드에 커서를 위치할 것을 브라우저에게 지시해준다. 레이블은 입력 필드의 id 속성에서 일치시키는 것이며, name 요소로 일치시키는 것이 아니라는 것에 주의해야 한다.(내 경우엔 name과 ID에 모두 같은 이름을 사용한다)
fillInFormValues는 $values와 $errors를 표시할 수 있는 $html을 가능한한 수정하지 않은 형태로 반환한다. 빈 배열을 전달하면 $html은 변경되지 않으며, 처음에는 이런식으로 화면을 표시하는 것이 쉽다. 완전히 동작하는 예제는 이곳에서 볼 수 있다. <?php
ob_start();
?>
<html>
<head>
<title>fillInFormValues: short example</title>
<style>
.error { color: red; }
</style>
</head>
<body>
<h1>Sign up for our newsletters</h1>
<ul class="error"><li>PLACEHOLDER FOR FORM ERRORS</li></ul>
<form action="<?php echo $_SERVER['PHP_SELF']; ?>" method="GET">
<table border="0">
<tr>
<td><label for="email">Your Email Address:</label></td>
<td><input type="text" name="email" id="email"></td>
</tr>
<tr>
<td><label for="newsletter">Sign up for these newsletters:</label></td>
<td>
<input type="checkbox" name="news" id="news">
<label for="news">News</label><br />
<input type="checkbox" name="security" id="security">
<label for="security">Security Notices</label><br />
<input type="checkbox" name="specials" id="specials">
<label for="specials">Specials</label>
</td>
</tr>
<tr><td> </td>
<td><input type="submit" name="submit" value="Sign Up"></td>
</tr>
</table>
</form>
</body>
</html>

<?php
$html = ob_get_contents();
ob_end_clean();

require_once("fillInFormValues.php");
require_once("validateForm.php");

$request = (get_magic_quotes_gpc() ?
array_map('stripslashes', $_REQUEST), $_REQUEST);

$validationData['email'] = array('isRequired', 'type' => 'email');
if (isset($request['submit'])) {
$formErrors = validateForm($request, $validationData);
if (count($formErrors) == 0) {
// Normally there would be code here to process the form
// and redirect to a thank you page...
// ... but for this example, we just always re-display the form.
$info = "No errors; got these values:".
nl2br(htmlspecialchars(print_r($request, 1)));
$html = preg_replace('/<body>/', "<body><p>$info</p>", $html);
}
}
else {
$formErrors = array();
}

echo fillInFormValues($html, $request, $formErrors);

?>
페이지의 HTML을 fillInFormValues에 전달할 수 있는 PHP 문자열로 얻기 위해 PHP의 출력 버퍼링 루틴인 ob_start, ob_get_contents, ob_end_clena을 사용했다. fillInFormValues는 $validationData 배열에 값을 채우는 것 뿐만 아니라 어떤 종류의 폼에 대해서도 사용할 수 있다. 예를 들어, 보험회사의 웹 사이트에서 볼 수 있는 끔찍한 폼에 대해서도 이 함수를 사용해서 값을 채울 수 있다.

fillInFormValues의 구현

fillInFormValues는 HTML에서 수정할 부분을 찾기 위해 preg_replace_callback을 사용한다. 예를 들어, class="error"를 추가하기 위해 <label> 태그를 찾기 위한 정규표현식은 다음과 같다.
/<label([^>]*)>/i
왼쪽에서 오른쪽으로 읽어가면 된다. 첫번째 /는 정규식의 시작을 의미한다. <label은 정확히 일치해야하는 부분을 의미한다. 괄호 ()안에 들어있는 [^>]*과 같은 이상한 표현은 "> 문자를 제외한 모든 문자"를 의미한다. 괄호 다음에 >은 문자 '>'을 의미한다. /i는 정규식의 끝을 의미하며, i는 <LABEL ...>이나 <label ...> 등을 찾을 수 있게끔 지정하는 것으로 대소문자 구분을 하지 않는 것을 의미한다.

이것은 HTML을 분석하는 간단한 방법인 동시에 지저분한 방법이기도 하다. 예를 들어, 이 표현식은 HTML 주석으로 둘러싼 경우도 고려하지 않는다. 즉, 문제가 되는 것은 아니지만 주석으로 표시된 label 태그도 변경한다. <input value="<label for='foo'*>"*>과 같이 HTML4 코드안에 속성 값을 직접 전달하면 문제가 생길 수 있다. 따라서, 이런식으로 코드를 사용하면 된다. 만약, HTML 파서가 필요하다면
XML_HTMLSax을 사용해야 한다.

이제, label 태그를 알아보고 처리할 수 있는 콜백 함수를 위한 코드를 작성해야 한다. 이 함수의 역할은 경우에 따라 label 태그를 변경하지 않거나 class="error"와 같은 문자열을 추가하는 것이다.
function fillInLabel($matches)
{
global $formErrors;
global $idToNameMap;

$tag = $matches[0];

$for = getAttributeVal($tag, "for");
if (empty($for) or !isset($idToNameMap[$for])) { return $tag; }
$name = $idToNameMap[$for];
if (array_key_exists($name, $formErrors)) {
return replaceAttributeVal($tag, 'class', 'error');
}
return $tag; // No error.
}
preg_replace_callback에 전달된 콜백 함수는 항상 인자가 하나 있다. 이 인자는 정규식으로 검색된 내용에 대한 배열이다. $matches[0]는 전체 내용을 의미하며, $matches[1]은 괄호의 첫번째 집합에 의해 일치된 내용이 된다. 여기서 $matches[0]는 전체 <label> 태그이다. label의 속성을 가져오고, $formErrors 배열에서 해당하는게 있는지 확인한다. 만약, 같은 것을 발견하면 label의 class 속성을 class="error"와 같이 변경한다. label에서 해당 에러가 없으면 함수는 태그를 변경하지 않는다. fillInLabel()에 필요한 정보를 전달하기 위해 전역변수를 사용했다.

<input>, <select>, <textarea>, <ul class="error">에 대한 패턴과 콜백도 이와 유사하지만 조금 더 복잡하다. <select>의 콜백이 가장 복잡하다. 이 콜백은 <select>와 </select> 태그 사이의 <option> 태그를 찾아서 변경하기 위해 preg_replace_callback을 재귀적으로 사용한다.HTML4에서는 다음과 같이 value를 사용하는 것이 허용되기 때문에 <input> 태그에 대한 정규표현식은 복잡하다.
<input name="foo" value="hello <smile>">
인용부호 안의 > 문자는 ">을 제외한 모든 문자"([^>]*)를 의미하며, input 태그 안에서는 동작하면 안 된다. XML과 XHTML의 속성 값에는 <과 >을 사용하는 것이 허용되지 않는다. 이 값들은 반드시 &lt;와 &gt;로 사용되야 한다. HTML4가 사라지게되면 삶은 보다 단순해질 것이다. getAttributeVal과 replaceAttributeVal 함수는 PHP에서 매우 강력한 정규표현식 함수인 preg_match_all을 사용한다. 이들 함수는 HTML 태그안의 속성들을 찾기 위해 정규식을 사용한다. /(\w+)((\s*=\s*".*?")|(\s*=\s*'.*?')|(\s*=\s*\w+)|())/s
대충 봐서는 이게 무엇을 의미하는지 이해하는 것이 쉽지 않다. 다음과 문자열이 전달된다고 가정해보자. name="foo" value='123' style=purple checked
정규식의 첫번째 부분인 (\w+)는 name, value, style, checked 등을 검색한다. 이 표현식은 하나 이상의 "단어"들을 일치시킨다. 나머지 정규표현식은 HTML에서 속성 값을 지정할 수 있는 네 가지 방법을 지정하기 위한 것이다. (\s*=\s*".*?")는 ="foo"와 같은 형태를 찾기 위한 것이며, (\s*=\s*'.*?')는 ='123'과 같은 형태를, (\s*=\s*\w+)는 =purple과 같은 형태를 찾기 위한 것이다. ()는 어떤 값도 사용하지 않는 checked와 같은 요소를 찾기 위한 것이다.

최대한 많이 일치시키려고 하기 때문에 정규표현식은 탐욕스럽지만 preg_match_all은 여러분이 원하는 작업을 정확하게 수행해줄 것이다. name="foo" value='123' style=purple checked를 전달하면 4개가 일치되었다고 알려줄 것이다.

getAttributeVal의 전체 코드는 다음과 같다.
/**
* Returns value of $attribute, given guts of an HTML tag.
* Returns false if attribute isn't set.
* Returns empty string for no-value attributes.
*
* @param string $tag Guts of HTML tag, with or without the <tag and >.
* @param string $attribute E.g. "name" or "value" or "width"
* @return string|false Returns value of attribute (or false)
*/
function getAttributeVal($tag, $attribute) {
$matches = array();
// This regular expression matches attribute="value" or
// attribute='value' or attribute=value or attribute
// It's also constructed so $matches[1][...] will be the
// attribute names, and $matches[2][...] will be the
// attribute values.
preg_match_all('/(\w+)((\s*=\s*".*?")|(\s*=\s*\'.*?\')|(\s*=\s*\w+)|())/s',
$tag, $matches, PREG_PATTERN_ORDER);

for ($i = 0; $i < count($matches[1]); $i++) {
if (strtolower($matches[1][$i]) == strtolower($attribute)) {
// Gotta trim off whitespace, = and any quotes:
$result = ltrim($matches[2][$i], " \n\r\t=");
if ($result[0] == '"') { $result = trim($result, '"'); }
else { $result = trim($result, "'"); }
return $result;
}
}
return false;
}
preg_match_all에 PREG_PATTERN_ORDER를 전달하면 $matches[1][$i]에서 속성 이름을, $matches[2][$i]에서 속성 값을 반환한다. replaceAttributeVal의 코드도 모든 속성들의 위치를 찾기 위해 PREG_OFFSET_CAPTURE(PHP 4.3.0 이상에서 지원)를 전달하고, 기존 값을 변경하거나 HTML 태그에 값을 추가하기 위해 substr_replace를 사용한다는 점만 제외하면 동일하다.

정리

fillInFormValues()의 첫번째 버전은 인자들을 전역변수에 집어넣고, 이들을 실제로 처리하는 콜백 함수에 인자를 전달하는 것입니다. 모든 콜백 함수는 PHP 함수 네임스페이스(namespace)에 등록됩니다.

"helper" 클래스에서 인자와 콜백 함수를 캡슐화하였고, array( &$this, "function" )과 같이 콜백 인자를 갖는 모든 PHP 함수들이 사용하는 콜백 구문을 사용한다. fillInFormValues()는 헬퍼 객체를 생성하고, 해당 객체에서 모든 작업을 처리할 메서드를 호출한다.
function fillInFormValues($formHTML, $request = null, $formErrors = null)
{
if ($request === null) {
// magic_quotes on: gotta strip slashes:
if (get_magic_quotes_gpc()) {
function stripslashes_deep(&$val) {
$val = is_array($val) ? array_map('stripslashes_deep', $val)
: stripslashes($val);
return $val;
}
$request = stripslashes_deep($_REQUEST);
}
else {
$request = $_REQUEST;
}
}
if ($formErrors === null) { $formErrors = array(); }

$h = new fillInFormHelper($request, $formErrors);
return $h->fill($formHTML);
}

/**
* Helper class, exists to encapsulate info needed between regex callbacks.
*/
class fillInFormHelper
{
var $request; // Normally $_REQUEST, passed into constructor
var $formErrors;
var $idToNameMap; // Map form element ids to names

function fillInFormHelper($r, $e)
{
$this->request = $r;
$this->formErrors = $e;
}

function fill($formHTML)
{
$s = fillInFormHelper::getTagPattern('input');
$formHTML = preg_replace_callback("/$s/is",
array(&$this, "fillInInputTag"), $formHTML);

// Using simpler regex for textarea/select/label, because in practice
// they never have >'s inside them:
$formHTML = preg_replace_callback('!(<textarea([^>]*>))(.*?)(</textarea\s*>)!is',
array(&$this, "fillInTextArea"), $formHTML);

$formHTML = preg_replace_callback('!(<select([^>]*>))(.*?)(</select\s*>)!is',
array(&$this, "fillInSelect"), $formHTML);

// Form errors: tag <label> with class="error", and fill in
// <ul class="error"> with form error messages.
$formHTML = preg_replace_callback('!<label([^>]*)>!is',
array(&$this, "fillInLabel"), $formHTML);
$formHTML = preg_replace_callback('!<ul class="error">.*?</ul>!is',
array(&$this, "getErrorList"), $formHTML);

return $formHTML;
}

/**
* Returns pattern to match given a HTML/XHTML/XML tag.
* NOTE: Setup so only the whole expression is captured
* (subpatterns use (?: ...) so they don't catpure).
* Inspired by http://www.cs.sfu.ca/~cameron/REX.html
*
* @param string $tag E.g. 'input'
* @return string $pattern
*/
function getTagPattern($tag)
{
$p = '('; // This is a hairy regex, so build it up bit-by-bit:
$p .= '(?is-U)'; // Set options: case-insensitive, multiline, greedy
$p .= "<$tag"; // Match <tag
$sQ = "(?:'.*?')"; // Attr val: single-quoted...
$dQ = '(?:".*?")'; // double-quoted...
$nQ = '(?:\w*)'; // or not quoted at all, but no wacky characters.
$attrVal = "(?:$sQ|$dQ|$nQ)"; // 'value' or "value" or value
$attr = "(?:\s*\w*\s*(?:=$attrVal)?)"; // attribute or attribute=
$p .= "(?:$attr*)"; // any number of attr=val ...
$p .= '(?:>|(?:\/>))'; // End tag: > or />
$p .= ')';
return $p;
}

/**
* Returns value of $attribute, given guts of an HTML tag.
* Returns false if attribute isn't set.
* Returns empty string for no-value attributes.
*
* @param string $tag Guts of HTML tag, with or without the <tag and >.
* @param string $attribute E.g. "name" or "value" or "width"
* @return string|false Returns value of attribute (or false)
*/
function getAttributeVal($tag, $attribute) {
$matches = array();
// This regular expression matches attribute="value" or
// attribute='value' or attribute=value or attribute
// It's also constructed so $matches[1][...] will be the
// attribute names, and $matches[2][...] will be the
// attribute values.
preg_match_all('/(\w+)((\s*=\s*".*?")|(\s*=\s*\'.*?\')|(\s*=\s*\w+)|())/s',
$tag, $matches, PREG_PATTERN_ORDER);

for ($i = 0; $i < count($matches[1]); $i++) {
if (strtolower($matches[1][$i]) == strtolower($attribute)) {
// Gotta trim off whitespace, = and any quotes:
$result = ltrim($matches[2][$i], " \n\r\t=");
if ($result[0] == '"') { $result = trim($result, '"'); }
else { $result = trim($result, "'"); }
return $result;
}
}
return false;
}
/**
* Returns new guts for HTML tag, with an attribute replaced
* with a new value. Pass null for new value to remove the
* attribute completely.
*
* @param string $tag Guts of HTML tag.
* @param string $attribute E.g. "name" or "value" or "width"
* @param string $newValue
* @return string
*/
function replaceAttributeVal($tag, $attribute, $newValue) {
if ($newValue === null) {
$pEQv = '';
}
else {
// htmlspecialchars here to avoid potential cross-site-scripting attacks:
$newValue = htmlspecialchars($newValue);
$pEQv = $attribute.'="'.$newValue.'"';
}

// Same regex as getAttribute, but we wanna capture string offsets
// so we can splice in the new attribute="value":
preg_match_all('/(\w+)((\s*=\s*".*?")|(\s*=\s*\'.*?\')|(\s*=\s*\w+)|())/s',
$tag, $matches, PREG_PATTERN_ORDER|PREG_OFFSET_CAPTURE);

for ($i = 0; $i < count($matches[1]); $i++) {
if (strtolower($matches[1][$i][0]) == strtolower($attribute)) {
$spliceStart = $matches[0][$i][1];
$spliceLength = strlen($matches[0][$i][0]);
$result = substr_replace($tag, $pEQv, $spliceStart, $spliceLength);
return $result;
}
}

if (empty($pEQv)) { return $tag; }

// No match: add attribute="newval" to $tag (before closing tag, if any):
$closed = preg_match('!(.*?)((>|(/>))\s*)$!s', $tag, $matches);
if ($closed) {
return $matches[1] . " $pEQv" . $matches[2];
}
return "$tag $pEQv";
}

/**
* Returns modified <input> tag, based on values in $request.
*
* @param array $matches
* @return string Returns new guts.
*/
function fillInInputTag($matches) {
$tag = $matches[0];

$type = fillInFormHelper::getAttributeVal($tag, "type");
if (empty($type)) { $type = "text"; }
$name = fillInFormHelper::getAttributeVal($tag, "name");
if (empty($name)) { return $tag; }
$id = fillInFormHelper::getAttributeVal($tag, "id");
if (!empty($id)) { $this->idToNameMap[$id] = $name; }

switch ($type) {
/*
* Un-comment this out at your own risk (users shouldn't be
* able to modify hidden fields):
* case 'hidden':
*/
case 'text':
case 'password':
if (!array_key_exists($name, $this->request)) {
return $tag;
}
return fillInFormHelper::replaceAttributeVal($tag, 'value', $this->request[$name]);
break;
case 'radio':
case 'checkbox':
$value = fillInFormHelper::getAttributeVal($tag, "value");
if (empty($value)) { $value = "on"; }

if (strpos($name, '[]')) {
$name = str_replace('[]', '', $name);
}

if (!array_key_exists($name, $this->request)) {
return fillInFormHelper::replaceAttributeVal($tag, 'checked', null);
}
$vals = (is_array($this->request[$name])?$this->request[$name]:array($this->request[$name]));

if (in_array($value, $vals)) {
return fillInFormHelper::replaceAttributeVal($tag, 'checked', 'checked');
}
return fillInFormHelper::replaceAttributeVal($tag, 'checked', null);
}
return $tag;
}
/**
* Returns modified <textarea...> tag, based on values in $request.
*
* @param array $matches
* @return string Returns new value.
*/
function fillInTextArea($matches) {
$tag = $matches[1]; // The <textarea....> tag
$val = $matches[3]; // Stuff between <textarea> and </textarea>
$endTag = $matches[4]; // The </textarea> tag

$name = fillInFormHelper::getAttributeVal($tag, "name");
if (empty($name)) { return $matches[0]; }
$id = fillInFormHelper::getAttributeVal($tag, "id");
if (!empty($id)) { $this->idToNameMap[$id] = $name; }

if (!array_key_exists($name, $this->request)) { return $matches[0]; }
return $tag.htmlspecialchars($this->request[$name]).$endTag;
}
/**
* Returns modified <option value="foo"> tag, based on values in $vals.
*
* @param array $matches
* @return string Returns tag with selected="selected" or not.
*/
function fillInOption($matches)
{
$tag = $matches[1]; // The option tag
$valueAfter = $matches[2]; // Potential value (stuff after option tag)
$val = fillInFormHelper::getAttributeVal($tag, "value");
if (empty($val)) { $val = trim($valueAfter); }
if (in_array($val, $this->selectVals)) {
return fillInFormHelper::replaceAttributeVal($tag, 'selected', 'selected').$valueAfter;
}
else {
return fillInFormHelper::replaceAttributeVal($tag, 'selected', null).$valueAfter;
}
}

var $selectVals;

/**
* Returns modified <select...> tag, based on values in $request.
*
* @param array $matches
* @return string
*/
function fillInSelect($matches) {
$tag = $matches[1];
$options = $matches[3];
$endTag = $matches[4];

$name = fillInFormHelper::getAttributeVal($tag, "name");
if (empty($name)) { return $matches[0]; }
$id = fillInFormHelper::getAttributeVal($tag, "id");
if (!empty($id)) { $this->idToNameMap[$id] = $name; }

if (strpos($name, '[]')) {
$name = str_replace('[]', '', $name);
}
if (!array_key_exists($name, $this->request)) { return $matches[0]; }

$this->selectVals =
(is_array($this->request[$name])?$this->request[$name]:array($this->request[$name]));

// Handle all the various flavors of:
// <option value="foo" /> OR <option>foo</option> OR <option>foo
$s = fillInFormHelper::getTagPattern('option');
$pat = "!$s(.*?)(?=($|(</option)|(</select)|(<option)))!is";
$options = preg_replace_callback($pat, array(&$this, "fillInOption"), $options);
return $tag.$options.$endTag;
}

/**
* Returns modified <label...> tag, based on $formErrors.
*
* @param array $matches
* @return string
*/
function fillInLabel($matches) {
$tag = $matches[0];
$for = fillInFormHelper::getAttributeVal($tag, "for");
if (empty($for) or !isset($this->idToNameMap[$for])) { return $tag; }
$name = $this->idToNameMap[$for];

if (array_key_exists($name, $this->formErrors)) {
return fillInFormHelper::replaceAttributeVal($tag, 'class', 'error');
}
return $tag; // No error.
}

/**
* Returns modified <ul class="error"> list with $formErrors error messages.
*
* @return string
*/
function getErrorList() {
$result = "";
foreach (array_unique($this->formErrors) AS $f => $msg) {
if (!empty($msg)) {
$result .= "<li>".htmlspecialchars($msg)."</li>\n";
}
}
if (empty($result)) { return ""; } // No errors: return empty string.
$result = '<ul class="error">'.$result.'</ul>';
return $result;
}
} // End of helper class.

다른 방법

패키지들 중에는 PHP 코드에서 HTML 폼을 생성해주는 것들이 있다. 예를 들어,
HTML_QuickForm과 같은 멋진 도구를 알고 있다. 대부분의 패키지들이 폼 검증과 다시 표시하는 것을 자동화해주지만, 나는 HTML을 생성하기 위해 PHP를 사용하는 것을 좋아하지 않는다. 나는 가능한 한 화면(HTML)과 응용프로그램 로직(PHP 코드)을 분리하는 것을 좋아한다. 웹 페이지 형태로 편집하는 것을 매우 좋아하기 때문에 드림위버와 같은 위지윅 에디터를 사용한다.

단순한 것이 최선이다

fillInFormValues()는 매우 간단한 인터페이스를 갖고 있다. 바로 함수 하나로 되어 있다. 구현 코드는 끔찍할 정도로 복잡하지도 않으며, 코멘트를 포함해서 400라인이 안되는 코드이다. 나는 단순한 것을 좋아하며, 이를 보다 큰 프로젝트에 통합시키는 것도 쉽다. 나는 데이터베이스에서 가져온 값을 표시하는 폼을 만들기 위해 fillInFormValues()를 사용한다. 또한, 이 구현을
스마티(Smarty)의 블록 함수로 등록해두었따. 어쨌거나 이 기능을 사용하기 시작한 이후로는 배관공이 되려는 유혹을 느낄 필요가 없었다.

소스 다운로드

이 글의 소스 코드는 여기에서 다운로드 받을 수 있으며, fillInFormValues에 사용할 수 있는 소스 코드와 유닛테스트를 다운받을 수 있는 압축파일로 제공한다.

, .

mail_lib.php

<?

function GetTimeStamp($date) {
/* 인자 형식
YYYY-MM-DD
YYYY-MM-DD HH:mm:ss
*/
if (strlen($DATE) == 10) {
$time = mktime(0,0,0,(int)substr($date,5,2),(int)substr($date,8,2),(int)substr($date,0,4));
} else {
$time = mktime((int)substr($date,11,2),(int)substr($date,14,2),(int)substr($date,17,2),
(int)substr($date,5,2),(int)substr($date,8,2),(int)substr($date,0,4));
}
return $time;
}

function HtmlHeader($strTitle)
{ ?>
<html>
<head>
<meta http-equiv=Content-Type content=text/html; charset=euc-kr>
<title><?echo $strTitle;?></title>
<link rel="stylesheet" href="web_mail.css" type="text/css">
</head>
<?
}

function DisplayCopyRight()
{
global $C_TABLE_SIZE; ?>

<table width="<?echo $C_TABLE_SIZE;?>" cellspacing=0 cellpadding=0 border=0>
<tr>
<td>
<hr width=100% size=2 color=#007394>
</td>
</tr>
<tr>
<td align=center><b><font face=굴림 size=2 color=#003354>
Copyright ⓒ 2000<img src="/img/ihelpers.gif" border=0
align=absmiddle> All rights reserved.</font></b></td>
</tr>
</table>
<br>
<?
}

function HtmlTail()
{
echo "</body></html>";
}

function PrintMsg($strMessage)
{
?>
<script language="javascript">
<!--
alert("<?echo $strMessage;?>");
//-->
</script>
<?
}

function PrintMsgBack($strMessage)
{
?>
<script language="javascript">
<!--
alert("<?echo $strMessage;?>");
history.back();
//-->
</script>
<?
exit;
}

function GoUrl($strUrl)
{
?>
<script language="javascript">
<!--
varUrl = '<?echo $strUrl;?>';
if (varUrl !="") {
document.location.replace(varUrl);
}
//-->
</script>
<?
}

function RedirectTarget($url,$target)
{ ?>
<html>
<body onLoad="document.form1.submit()";>
<form action="<?echo $url;?
>" target="<?echo $target;?>" name=form1 method=post>
<input type=hidden name=name value="">
</form>
</body>
</html>
<?
}

function CheckBroswer($num, $num2)
{
global $HTTP_USER_AGENT;

if (strpos($HTTP_USER_AGENT, "MSIE")) {
return $num;
} else {
return $num2;
}
}

function CompStr($buffer, $value) {
if (strlen($buffer) <= strlen($value)) return false;

if (substr($buffer, 0, strlen($value)) == $value) {
return true;
} else {
return false;
}
}

function Decode($val) {
if(substr($val,0,2) == "=?") { //인코딩 여부 확인
$code = strpos($val, "?", 3);
$code = strpos($val, "?", $code+1);
$val = substr($val, $code+1, strlen($val) - $code -3);
return imap_base64($val);
} else {
return $val;
}
}

function printOutLook($val) {
$line = split("
", $val);
$val = "";
$cnt = 0;
for($i=0;$i<count($line);$i++) {
if($line[$i]=="") $cnt++;
if($cnt == 4) $val .= $line[$i] . "
";
}
echo imap_base64($val);
}
?>
, .

1. 메일 확인
2. 메일 조회
3. 메일 지우기
4. 메일 보내기

다섯번째 강좌 시간입니다. 오늘은 메일을 삭제하는 것입니다. 지난번 메일 확인 강좌에서 나온 리스트 보기에서 삭제할 메일을 선택한후 이를 삭제하는 부분이 되겠습니다..

소스를 보죠..

mail_cmd.php

<?
include ("mail_lib.php");

$cmd = $CMD;
$box = $BOX;
$part_no = $PART_NO;
if($box == "") $box = "INBOX";
$login = "userid";
$pass = "pwd";
$C_DOMAIN = "hagopa.co.kr";

$mailstream = imap_open("{" . $C_DOMAIN . ":143}" . $box, $login, $pass);
if ($mailstream == 0) {
echo "Error!
";
exit;
}

switch($cmd) {
case "del":
for($i=0;$i<count($NO);$i++) {
$result = imap_delete($mailstream, $NO[$i]);
// 해당 번호의 메일에 삭제 표시를 합니다. 즉 위 함수는 실제
// 삭제시키는 함수가 아니라는 거죠..

if(!$result) {
echo "삭제실패";
imap_close($mailstream);
exit;
}
imap_expunge($mailstream);
// 위에서 삭제 표시를 한 메일을 삭제하는 명령을 수행합니다.
// 이 함수가 호출 되지 않고 루틴이 끝나면 삭제되지 않습니다.
}
break;
}

imap_close($mailstream);
RedirectTarget("mail_list.php?BOX=".$box, "");
?>
, .

1. 메일 확인
2. 메일 조회
3. 메일 지우기
4. 메일 보내기

단지 지난 번엔 하나의 메일에 있는 boundary로 나뉘어진 여러 part들을 분석해서 화면에 출력하는 것을 했고 이번엔 그중 첨부파일에 대한 part 하나를 다운로드시키는 부분이 되겠습니다.

소스를 보죠..

mail_down.php

<?
// 지난번 mail_detail.php에 있는 printbody 함수와 거의 같습니다..
// 다른 부분만 설명하죠..


function printbody($mailstream, $MSG_NO, $numpart, $encode, $mime, $file_name) {
$val = imap_fetchbody($mailstream, $MSG_NO, (string)($numpart+1), FT_UID);

switch($encode) {
case 0: // 7bit
break;
case 1: // 8bit
$val = imap_base64(imap_binary(imap_qprint(imap_8bit($val))));
break;
case 2: // binary
$val = imap_base64(imap_binary($val));
break;
case 3: // base64
$val = imap_base64($val);
break;
case 4: // quoted-print
$val = imap_base64(imap_binary(imap_qprint($val)));
break;
case 5: // other
echo "알수없는 Encoding 방식.";
exit;
}

// 이부분이 다르죠..
// 전엔 이부분에서 첨부파일일경우 단순 링크만 시켜 놓았고..
// 여기서는 해당 부분을 그대로 출력합니다. 그렇게 되면 사용자 측에선
// 다운로드가 실행되는 것이죠..


switch($mime) {
case "PLAIN":
Header ( "Content-Type: text/plain");
echo str_replace("\n", "<br>", $val);
break;
case "HTML":
Header ( "Content-Type: text/html");
echo $val;
break;
case "OCTET-STREAM":
Header ( "Content-Type: octet-stream");
Header ( "Content-Disposition: attachment; filename=$file_name");
echo $val;
break;
default:
Header ( "Content-Type: octet-stream");
Header ( "Content-Disposition: attachment; filename=$file_name");
echo $val;
}
}

include ("mail_lib.php");

$box = $BOX;
$part_no = $PART_NO;
if($part_no == "") $part_no = 0;
if($box == "") $box = "INBOX";
$login = "userid";
$pass = "pwd";
$C_DOMAIN = "hagopa.co.kr";

$mailstream = imap_open("{" . $C_DOMAIN . ":143}" . $box, $login, $pass);

if ($mailstream == 0) {
echo "Error!
";
exit;
}

$struct = imap_fetchstructure($mailstream, $MSG_NO);
$part = $struct->parts[$part_no];
$param = $part->parameters[0];
$file_name = Decode($param->value); // 첨부파일일 경우 파일명
$mime = $part->subtype; // MIME 타입
$encode = $part->encoding; // encoding

printbody($mailstream, $MSG_NO, $part_no, $encode, $mime, $file_name);
imap_close($mailstream);
?>

, .