기본
변수, 타입 & 상수
PHP 변수는 $로 시작하고 동적으로 타입이 지정됩니다. PHP 7.4+는 타입이 지정된 속성을 지원합니다. 런타임 상수에는 define()을, 컴파일 타임 상수에는 const를 사용하세요(더 빠름). PHP 8.1은 enum을 도입했습니다 — 클래스 상수보다 우수한 타입화된 열거 시스템입니다. 가능한 곳에 항상 타입을 선언(PHP 7+)하여 조기 에러 감지와 더 나은 IDE 지원을 받으세요.
<?php
$name = "Alice"; // string
$age = 30; // integer
$price = 19.99; // float (double)
$active = true; // boolean
$items = [1, 2, 3]; // array
$null = null; // null
// constants
define("MAX_USERS", 100); // runtime constant
const PI = 3.14159; // compile-time constant
echo MAX_USERS; // no $ prefix
// PHP 8.1+ enums
enum Status: string {
case Active = 'active';
case Inactive = 'inactive';
}
$s = Status::Active;Echo, Print & 디버깅
echo는 언어 구조(함수 아님)입니다 — 출력에 가장 빠릅니다. print는 1을 반환하므로 표현식에 사용할 수 있습니다. printf/sprintf는 C 스타일 형식 지정자(%s 문자열, %d 정수, %f 실수, %x 16진수)를 사용합니다. var_dump()는 주요 디버깅 도구입니다 — 타입과 값을 표시합니다. error_log()는 PHP 에러 로그나 syslog에 기록합니다. 프로덕션에서는 디버그 출력을 사용자에게 노출하지 마세요.
<?php
// echo: no return, multiple args (fastest)
echo "Hello", " ", "World";
// print: returns 1, single arg
print "Hello World";
// printf: formatted output
printf("Name: %s, Age: %d, Price: %.2f", "Alice", 30, 19.99);
// debugging output
print_r($array); // human-readable
var_dump($variable); // type + value (detailed)
var_export($array, true); // valid PHP code (for caching)
// sprintf: return formatted string (don't print)
$log = sprintf("[%s] %s", date('H:i:s'), "Started");
error_log($log); // log to error log연산자 & 비교
항상 ===(엄격한 비교)를 사용하여 타입 강제 변환 버그를 피하세요. ==는 비교 전에 타입을 변환하여 예상치 못한 결과를 초래합니다(0 == 'abc'는 PHP 7에서 true였습니다). 우주선 연산자(<=>)는 -1/0/1을 반환합니다 — usort에 유용합니다. 널 병합 연산자(??)는 기본값을 제공하는 관용적인 방법입니다. 널 안전 연산자(?->)(PHP 8+)는 null에서 메서드 체인을 단락시켜 장황한 isset() 검사를 대체합니다.
<?php
// arithmetic
$sum = 10 + 3; // 13
$mod = 10 % 3; // 1
$pow = 2 ** 3; // 8 (PHP 5.6+)
// comparison: == vs ===
echo (0 == "abc"); // true (loose, PHP 7-); false (PHP 8+)
echo (0 === "abc"); // false (strict — type + value)
echo ("1" == 1); // true (loose)
echo ("1" === 1); // false (strict)
// spaceship operator (PHP 7+)
echo 1 <=> 2; // -1 (less than)
echo 2 <=> 2; // 0 (equal)
echo 3 <=> 2; // 1 (greater than)
// null coalescing
$name = $input ?? "default"; // if $input is null
$deep = $data['user']['name'] ?? "Anonymous";
// null safe operator (PHP 8+)
$country = $user?->getAddress()?->country; // null if any step is null슈퍼글로벌 & 웹
슈퍼글로벌은 모든 범위에서 사용 가능한 내장 연관 배열입니다. $_GET과 $_POST는 사용자 입력을 포함합니다 — 사용 전에 항상 살균/검증하세요. filter_input()이 직접 접근보다 안전합니다. 클라이언트가 위조할 수 있는 $_SERVER 값(예: HTTP_USER_AGENT)은 절대 신뢰하지 마세요. header('Location:') 후 항상 exit를 호출하세요 — 그렇지 않으면 PHP가 계속 실행됩니다. 세션은 출력 전에 session_start()로 시작하세요.
<?php
// superglobals available everywhere
$_GET['name']; // query string params
$_POST['email']; // form POST data
$_REQUEST['x']; // GET + POST + COOKIE
$_SERVER['HTTP_HOST']; // server/env info
$_SERVER['REQUEST_METHOD']; // GET, POST, etc.
$_COOKIE['session']; // cookies
$_FILES['upload']; // file uploads
$_SESSION['user_id']; // session data (after session_start())
// get client IP
$ip = $_SERVER['REMOTE_ADDR'];
// check request method
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$name = filter_input(INPUT_POST, 'name', FILTER_SANITIZE_STRING);
}
// redirect
header("Location: /dashboard");
exit; // always exit after redirectInclude & Require
include/require는 지정된 파일을 실행합니다. require는 파일이 없으면 치명적 에러를 발생시킵니다(중요 의존성에 사용); include는 경고만 합니다(선택적 템플릿에 사용). _once 변형은 이중 포함을 방지하기 위해 포함된 파일을 추적합니다 — 함수/클래스 정의에 필수적입니다. Composer의 오토로더(require_once 'vendor/autoload.php')는 수동 include 관리를 제거합니다. 파일은 값을 반환할 수 있어 설정에 유용합니다.
<?php
// include: warning on failure, continues
include 'header.php';
include_once 'config.php'; // only once
// require: fatal error on failure, stops
require 'database.php';
require_once 'vendor/autoload.php'; // Composer autoload
// _once variants prevent re-declaration errors
// use require for critical files (DB config, autoloaders)
// use include for optional files (templates)
// return values from included files
$config = include 'config.php';
// config.php: return ['db' => 'mysql://...', 'debug' => true];문자열
문자열 함수
PHP는 100개 이상의 문자열 함수가 있습니다. strpos()는 찾지 못하면 false를 반환합니다 — 확인에는 === false를 사용하세요(0은 유효한 위치입니다). str_replace()는 검색/대체에 배열을 사용할 수 있습니다. substr()은 음수 오프셋(끝에서부터)을 지원합니다. 멀티바이트 문자열(UTF-8)의 경우 mb_* 동등 함수(mb_strlen, mb_substr)를 사용하세요 — strlen은 문자가 아닌 바이트 를 셉니다. 항상 default_charset='UTF-8'로 설정하고 비 ASCII 텍스트에는 mb_* 함수를 사용하세요.
<?php
$s = "Hello, World";
// length and case
echo strlen($s); // 12
echo str_word_count($s); // 2
echo strtoupper($s); // HELLO, WORLD
echo strtolower($s); // hello, world
echo ucfirst("hello"); // Hello
echo ucwords("hello world"); // Hello World
// search and replace
echo strpos($s, "World"); // 7 (false if not found)
echo str_replace("o", "0", $s); // Hell0, W0rld
echo substr($s, 0, 5); // Hello
echo substr($s, -5); // World
echo strrev($s); // dlroW ,olleH
// trim
echo trim(" hi "); // "hi"
echo ltrim(" hi"); // "hi"
echo rtrim("hi "); // "hi"문자열 보간 & Heredoc
큰따옴표 문자열은 변수를 보간합니다; 작은따옴표 문자열은 보간하지 않습니다(리터럴 텍스트에는 작은따옴표 사용 — 약간 더 빠름). 복잡한 표현식(객체 속성, 배열 접근, 메서드 호출)에는 {$var}를 사용하세요. Heredoc(<<<ID)은 SQL이나 HTML 같은 다중 줄 문자열에 이상적입니다 — 변수를 보간합니다. Nowdoc(<<<'ID')은 해석하지 않는 버전으로, 백슬래시가 있는 정규식 패턴에 유용합니다.
<?php
$name = "Alice";
$age = 30;
// double quotes: variable interpolation
echo "Hello, $name!"; // Hello, Alice!
echo "Hello, {$name}!"; // Hello, Alice! (braces for clarity)
echo "Age: {$age}"; // Age: 30
// single quotes: no interpolation (faster)
echo 'Hello, $name'; // Hello, $name (literal)
// complex expressions need braces
echo "Result: {$obj->method()}";
echo "Item: {$array['key']}";
// heredoc: multi-line, interpolates
$sql = <<<SQL
SELECT * FROM users
WHERE name = '$name'
AND age > $age
SQL;
// nowdoc: multi-line, NO interpolation (like single quotes)
$regex = <<<'REGEX'
\d{3}-\d{4}
REGEX;sprintf & 형식화
sprintf()는 안전하게 형식화된 문자열을 구축하는 데 필수적입니다 — 문자열 보간과 달리 타입 변환과 패딩을 처리합니다. 정수에는 %s가 아닌 %d를 사용하여 숫자 형식을 보장하세요. number_format()은 천 단위 구분자로 숫자를 형식화합니다 — 통화 표시에 중요합니다. 준비된 문장의 SQL 조각에는 항상 sprintf를 사용하세요(사용자 입력에는 여전히 준비된 문장이 필요합니다).
<?php
// sprintf: format string (returns, doesn't print)
$formatted = sprintf(
"%-10s | %5d | %8.2f",
"Alice", 42, 19.99
);
echo $formatted; // "Alice | 42 | 19.99"
// format specifiers:
// %s string, %d int, %f float, %x hex, %b binary, %c char
// %5d right-padded to width 5
// %-5d left-padded
// %05d zero-padded
// %.2f 2 decimal places
// %8.2f width 8, 2 decimals
// numbered placeholders
echo sprintf("Hi %1$s, bye %1$s", "Alice"); // Hi Alice, bye Alice
// number_format
echo number_format(1234567.891, 2); // 1,234,567.89
echo number_format(1234567.89, 2, ',', '.'); // European format정규식 (PCRE)
PHP는 /pattern/ 구분자와 함께 PCRE(Perl 호환 정규식)를 사용합니다. preg_match는 일치하면 1, 아니면 0을 반환합니다(0은 falsy이므로 ==가 아닌 ===를 사용). 항상 정규식으로 사용자 입력을 검증하지만 그것만 의존하지 마세요 — 이메일, URL에는 filter_var()를 사용하세요. preg_replace는 강력하지만 큰 문자열에서 느릴 수 있습니다. 블랙리스트 대신 문자를 화이트리스트하려면 [^...]를 사용하세요. i 플래그는 대소문자 구분 없이 일치시킵니다.
<?php
// preg_match: test pattern (returns 0 or 1)
if (preg_match('/^[a-z]+$/i', "Hello")) {
echo "alphabetic";
}
// capture groups
preg_match('/(d{4})-(d{2})-(d{2})/', "2024-01-15", $matches);
// $matches[0] = "2024-01-15" (full match)
// $matches[1] = "2024" (group 1)
// $matches[2] = "01"
// $matches[3] = "15"
// preg_match_all: all matches
preg_match_all('/d+/', "a1 b22 c333", $matches);
// $matches[0] = ["1", "22", "333"]
// preg_replace: substitute
$clean = preg_replace('/[^a-z0-9]/i', '', "Hello, World!"); // HelloWorld
// preg_split: split by pattern
$parts = preg_split('/[s,]+/', "one, two,three four");
// ["one", "two", "three", "four"]멀티바이트 & 인코딩
PHP의 기본 문자열 함수는 문자 지향이 아닌 바이트 지향입니다 — 멀티바이트 문자(UTF-8, 중국어, 이모지)에서 깨집니다. 비 ASCII 텍스트에는 항상 mb_* 함수(mb_strlen, mb_substr, mb_strpos, mb_strtoupper)를 사용하세요. 애플리케이션 시작 시 mb_internal_encoding('UTF-8')을 설정하세요. JSON 출력에서 중국어/이모지를 읽을 수 있게 유지하려면 JSON_UNESCAPED_UNICODE를 사용하세요. 이는 국제 애플리케이션에서 버그의 일반적인 원인입니다.
<?php
// UTF-8 strings: use mb_* functions
$text = "Café"; // 4 chars, 5 bytes (é = 2 bytes)
echo strlen($text); // 5 (bytes!)
echo mb_strlen($text); // 4 (characters!)
echo substr($text, 0, 3); // "Caf" (might break UTF-8!)
echo mb_substr($text, 0, 3); // "Caf" (safe)
// case conversion
echo strtoupper("straße"); // "STRAßE" (wrong!)
echo mb_strtoupper("straße"); // "STRAßE" (correct, locale-aware)
// encoding detection
$encoding = mb_detect_encoding($text);
$utf8 = mb_convert_encoding($text, 'UTF-8', 'auto');
// set internal encoding
mb_internal_encoding('UTF-8');
mb_regex_encoding('UTF-8');
// JSON with UTF-8
$json = json_encode($data, JSON_UNESCAPED_UNICODE);배열
인덱스 & 연관 배열
PHP 배열은 실제로 정렬된 해시 맵입니다 — 리스트와 딕셔너리 모두로 작동합니다. 인덱스 배열은 숫자 키를 자동 할당; 연관 배열은 문자열 키를 사용합니다. isset()은 null 값에 대해 false를 반환; array_key_exists()는 null에도 true를 반환합니다. unset()은 요소를 제거하지만 재인덱스하지 않습니다. 진정한 리스트(간격 없음)를 위해 삭제 후 array_values()로 재인덱스하세요. PHP 8.1+에는 readonly 배열 타입이 있습니다.
<?php
// indexed array (numeric keys)
$nums = [10, 20, 30];
$nums[] = 40; // append
echo $nums[0]; // 10
echo count($nums); // 4
// associative array (string keys)
$user = [
"name" => "Alice",
"age" => 30,
"email" => "[email protected]",
];
echo $user["name"]; // Alice
$user["phone"] = "555-1234"; // add key
// mixed keys
$mixed = [0 => "a", "name" => "b", 5 => "c"];
// check key existence
if (isset($user["email"])) { /* ... */ }
if (array_key_exists("name", $user)) { /* ... */ }
// remove element
unset($user["phone"]);다차원 & 반복
다차원 배열은 배열의 배열입니다. foreach는 반복하는 관용적인 방법입니다 — for 루프보다 빠르고 읽기 쉽습니다. 요소를 제자리에서 수정하려면 &$value를 사용하세요(버그를 피하기 위해 루프 후 항상 참조 해제). array_column()은 2D 배열에서 단일 열을 추출합니다 — 데이터베이스 결과 집합을 변환하는 데 매우 유용합니다. PHP 배열은 삽입 순서를 유지합니다.
<?php
$users = [
["name" => "Alice", "age" => 30],
["name" => "Bob", "age" => 25],
["name" => "Carol", "age" => 35],
];
// iterate with key + value
foreach ($users as $index => $user) {
echo "$index: {$user['name']} ({$user['age']})\n";
}
// modify by reference
foreach ($users as &$user) {
$user['age'] += 1; // increment each age
}
unset($user); // break reference!
// nested iteration
$matrix = [[1, 2], [3, 4], [5, 6]];
foreach ($matrix as $row) {
foreach ($row as $cell) {
echo $cell . " ";
}
echo "\n";
}
// extract column
$names = array_column($users, 'name'); // ["Alice", "Bob", "Carol"]배열 함수: map, filter, reduce
array_map, array_filter, array_reduce는 배열의 함수형 프로그래밍 3인방입니다. 화살표 함수(fn() =>)는 이들을 간결하게 만듭니다. array_filter는 키를 보존합니다 — 필요하면 array_values()로 재인덱스하세요. array_merge는 숫자 키를 재인덱스하지만 문자열 키는 보존합니다(나중 값이 덮어씀). array_column, array_chunk, array_slice는 데이터 조작에 필수적입니다. 이 함수들은 PHP에서 데이터 처리의 중추입니다.
<?php
$nums = [1, 2, 3, 4, 5];
// map: transform each element
$doubled = array_map(fn($n) => $n * 2, $nums); // [2, 4, 6, 8, 10]
// filter: keep elements matching condition
$evens = array_filter($nums, fn($n) => $n % 2 === 0); // [2, 4]
// reduce: accumulate to single value
$sum = array_reduce($nums, fn($carry, $n) => $carry + $n, 0); // 15
// walk: like map but modifies in place (by reference)
array_walk($nums, fn(&$n) => $n *= 2);
// combining arrays
$merged = array_merge([1, 2], [3, 4]); // [1, 2, 3, 4]
$combined = array_combine(['a', 'b'], [1, 2]); // ['a' => 1, 'b' => 2]
$sliced = array_slice($nums, 1, 2); // [2, 3]
$chunked = array_chunk($nums, 2); // [[1,2], [3,4], [5]]배열 정렬
PHP 정렬 함수는 배열을 제자리에서 수정합니다(참조로 전달). sort/rsort는 재인덱스; asort/arsort는 키를 보존합니다. usort와 비교 함수(<=> 사용)는 사용자 정의 논리로 정렬합니다. natsort()는 자연 정렬을 수행합니다(img10보다 img2가 먼저) — 파일명에 필수적입니다. 다차원 배열의 경우 원하는 필드를 비교하는 클로저와 함께 usort를 사용하세요. 우주선 연산자(<=>)는 비교 함수를 단순화합니다.
<?php
$nums = [3, 1, 4, 1, 5, 9, 2, 6];
// sort by value (reindexes)
sort($nums); // [1, 1, 2, 3, 4, 5, 6, 9]
rsort($nums); // descending
// sort preserving keys
asort($nums); // ascending, preserve keys
arsort($nums); // descending, preserve keys
// sort by key
ksort($nums); // by key ascending
krsort($nums); // by key descending
// custom sort with callback
$users = [["name" => "Bob", "age" => 25], ["name" => "Alice", "age" => 30]];
usort($users, fn($a, $b) => $a['age'] <=> $b['age']);
// sorted by age ascending
// natural sort (for strings with numbers)
$files = ["img10.jpg", "img2.jpg", "img1.jpg"];
natsort($files); // ["img1.jpg", "img2.jpg", "img10.jpg"]
sort($files); // ["img1.jpg", "img10.jpg", "img2.jpg"] (wrong!)배열 검사 & 조작
in_array에 strict=true(세 번째 매개변수)를 사용하면 타입 강제 변환 버그를 방지합니다. array_search는 키를 반환합니다(=== false로 확인). array_push/pop은 LIFO(스택)를 구현; array_shift/unshift는 FIFO(큐)를 구현 — 하지만 shift는 O(n)입니다. 큰 큐의 경우 SplQueue나 SplDoublyLinkedList를 사용하세요. array_unique는 키를 보존합니다. array_diff/intersect는 값을 비교; 키 기반 비교에는 array_diff_key/intersect_key를 사용하세요.
<?php
$arr = [1, 2, 3, 4, 5];
// inspection
echo count($arr); // 5
echo in_array(3, $arr); // true
echo in_array("3", $arr, true); // false (strict)
echo array_search(3, $arr); // 2 (key, false if not found)
print_r(array_keys($arr)); // [0, 1, 2, 3, 4]
print_r(array_values($arr)); // [1, 2, 3, 4, 5]
// stack/queue operations
array_push($arr, 6); // push to end
$last = array_pop($arr); // pop from end
$first = array_shift($arr); // remove from front
array_unshift($arr, 0); // add to front
// set operations
$unique = array_unique([1, 2, 2, 3]); // [1, 2, 3]
$diff = array_diff([1, 2, 3], [2, 3, 4]); // [1] (in first, not second)
$intersect = array_intersect([1, 2, 3], [2, 3, 4]); // [2, 3]
// flip and reverse
$flipped = array_flip(['a' => 1, 'b' => 2]); // [1 => 'a', 2 => 'b']
$reversed = array_reverse([1, 2, 3]); // [3, 2, 1]제어 흐름
If / Else / Elseif
PHP는 elseif(한 단어)를 사용합니다 — 공백이 있는 'else if'가 아닙니다(물론 그것도 작동합니다). 대체 구문(if: ... endif;)은 중괄호 매칭 혼란을 피하기 위해 HTML 템플릿에 유용합니다. 삼항 연산자는 우결합성입니다 — 중첩을 피하세요. 널 병합 할당 연산자(??=)는 현재 null인 경우에만 값을 설정합니다 — 설정 기본값의 지연 초기화에 완벽합니다.
<?php
$score = 85;
if ($score >= 90) {
$grade = "A";
} elseif ($score >= 80) {
$grade = "B";
} elseif ($score >= 70) {
$grade = "C";
} else {
$grade = "F";
}
// alternative syntax (for templates)
if ($score >= 90):
echo "Excellent";
elseif ($score >= 80):
echo "Good";
else:
echo "Try harder";
endif;
// ternary
$status = $age >= 18 ? "adult" : "minor";
// null coalescing assignment (PHP 7.4+)
$config['timeout'] ??= 30; // set if not setSwitch & Match
switch는 느슨한 비교(==)를 사용하고 fall-through를 방지하기 위해 break가 필요합니다 — 일반적인 버그 원인입니다. match(PHP 8+)는 엄격한 비교(===)를 사용하고, 값을 직접 반환하며, 일치하는 arm이 없으면 예외를 던집니다(조용한 실패 없음). 값이 필요할 때 match는 switch의 현대적 대체입니다. 복잡한 다중 명령문 케이스에는 switch를; 단순 값 선택에는 match를 사용하세요. 항상 default 케이스를 포함하세요.
<?php
// switch: loose comparison (==)
$day = "Mon";
switch ($day) {
case "Mon":
case "Tue":
case "Wed":
echo "Weekday";
break;
case "Sat":
case "Sun":
echo "Weekend";
break;
default:
echo "Unknown";
}
// match (PHP 8+): strict comparison (===), returns value
$status = 404;
$message = match($status) {
200, 201 => "Success",
301, 302 => "Redirect",
404 => "Not Found",
500 => "Server Error",
default => "Unknown",
};
// match throws UnhandledMatchError if no match and no default루프: for, while, foreach, do-while
foreach는 배열의 관용적 루프입니다 — count()와 for보다 빠르고 안전합니다. continue로 반복을 건너뛰고 break로 종료하세요. PHP에는 레이블이 있는 break/continue가 없습니다(Java/Rust와 달리). 연관 배열의 경우 foreach ($arr as $key => $value)가 표준 패턴입니다. do-while은 최소 한 번 실행 — 입력 검증에 유용합니다. foreach 중에 배열을 수정하지 마세요(결과를 별도 배열에 저장).
<?php
// for loop
for ($i = 0; $i < 5; $i++) {
echo $i; // 01234
}
// foreach (most common in PHP)
$fruits = ["apple", "banana", "cherry"];
foreach ($fruits as $fruit) {
echo $fruit;
}
// foreach with key
foreach ($fruits as $index => $fruit) {
echo "$index: $fruit";
}
// while
$count = 0;
while ($count < 3) {
echo $count++;
}
// do-while (runs at least once)
do {
$line = readline("> ");
} while ($line !== "quit");
// break and continue
for ($i = 0; $i < 10; $i++) {
if ($i === 3) continue; // skip 3
if ($i === 7) break; // stop at 7
echo $i;
}템플릿에서의 제어 흐름
PHP의 대체 제어 구문(if:/elseif:/else:/endif;, foreach:/endforeach;)은 HTML 템플릿용으로 설계되었습니다. <?= $var ?>는 <?php echo $var; ?>의 약어입니다 — 템플릿에서 가독성을 위해 항상 사용하세요. XSS를 방지하기 위해 항상 htmlspecialchars()로 출력을 이스케이프하세요. PHP 로직과 HTML 프레젠테이션의 분리는 Twig와 Blade 같은 템플릿 시스템의 기초이며, 더 깔끔한 구문과 자동 이스케이핑을 제공합니다.
<?php // template file ?>
<?php if ($user->isAdmin()): ?>
<div class="admin-panel">Admin Tools</div>
<?php elseif ($user->isEditor()): ?>
<div class="editor-tools">Edit Tools</div>
<?php else: ?>
<div class="user-view">Read Only</div>
<?php endif; ?>
<?php foreach ($products as $p): ?>
<div class="product">
<?= htmlspecialchars($p['name']) ?>
- $<?= number_format($p['price'], 2) ?>
</div>
<?php endforeach; ?>
<?php // shorthand echo ?>
<h1><?= $title ?></h1>
<?php // ternary in templates ?>
<span class="<?= $active ? 'on' : 'off' ?>"><?= $active ? 'Active' : 'Inactive' ?></span>예외 & 에러 처리
PHP 7+는 대부분의 에러에 예외를 사용합니다. 항상 특정 예외 타입(단순 Exception이 아닌)을 catch하여 다른 실패를 적절히 처리하세요. finally는 항상 실행됩니다 — 정리(파일 닫기, 연결)에 사용하세요. 사용자 정의 예외는 Exception을 확장하고 도메인 컨텍스트를 추가합니다. PHP 8+는 |로 여러 예외 타입을 catch할 수 있습니다. 일관된 에러 처리를 위해 PDO를 예외 모드로 설정하세요. 로깅 없이 예외를 catch하지 마세요 — 조용한 실패는 버그를 숨깁니다.
<?php
// try / catch / finally
try {
$pdo = new PDO("mysql:host=localhost;dbname=test", "user", "pass");
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch (PDOException $e) {
error_log("DB connection failed: " . $e->getMessage());
die("Service unavailable");
} finally {
// always runs, even after return/throw
echo "Cleanup";
}
// custom exception
class ValidationException extends Exception {
public function __construct(string $field, string $message = "") {
parent::__construct("$field: $message");
}
}
// throw
if (empty($email)) {
throw new ValidationException("email", "is required");
}
// catch multiple types (PHP 8+)
try {
riskyOperation();
} catch (PDOException | RuntimeException $e) {
// catches either type
log_error($e);
}함수
함수 정의
PHP 7+는 타입이 지정된 매개변수와 반환 타입(int, string, array, ?Type은 nullable)을 지원합니다. PHP 8+는 명명된 인수(기본값 건너뛰기, 매개변수 순서 변경), 유니온 타입(int|string), mixed 타입을 추가합니다. 가변 매개변수(...$nums)는 추가 인수를 배열로 수집합니다. 전개 연산자(...$arr)는 배열을 인수로 언팩합니다. 참조로 전달(&)은 원본을 수정합니다 — 코드를 추론하기 어렵게 만들므로 드물게 사용하세요.
<?php
// basic function with return type
function add(int $a, int $b): int {
return $a + $b;
}
// default parameters
function greet(string $name, string $greeting = "Hello"): string {
return "$greeting, $name!";
}
// named arguments (PHP 8+)
echo greet(name: "Alice", greeting: "Hi");
// variadic functions
function sum(int ...$nums): int {
return array_sum($nums);
}
echo sum(1, 2, 3, 4); // 10
// spread operator
$nums = [1, 2, 3];
echo sum(...$nums); // 6
// pass by reference
function increment(int &$n): void {
$n++;
}
$x = 5;
increment($x);
echo $x; // 6화살표 함수 & 클로저
화살표 함수(fn() =>)는 간결하고, 단일 표현식 클로저로 외부 변수를 값으로 자동 캡처합니다. 전통적인 클로저(function() use ($var))는 다중 줄 본문이나 참조로 캡처(&$var)할 때 필요합니다. 클로저는 array_map, array_filter, usort, 이벤트 핸들러에 필수적입니다. 화살표 함수는 명령문을 가질 수 없습니다(if, for 없음) — 복잡한 로직에는 전통적 인 클로저를 사용하세요. 클로저는 일급 객체입니다(Closure 클래스).
<?php
// arrow function (PHP 7.4+): single expression, auto-capture
$square = fn($x) => $x * $x;
echo $square(5); // 25
// auto-captures outer variables by value
$multiplier = 3;
$multiply = fn($x) => $x * $multiplier;
echo $multiply(5); // 15
// traditional closure (multi-line, explicit capture)
$factor = 10;
$scale = function ($x) use ($factor) {
return $x * $factor + 1;
};
// capture by reference
$count = 0;
$increment = function () use (&$count) {
$count++;
};
$increment();
echo $count; // 1
// closures as callbacks
$nums = [1, 2, 3, 4];
$evens = array_filter($nums, fn($n) => $n % 2 === 0);
$doubled = array_map(fn($n) => $n * 2, $nums);변수 범위 & 전역
PHP는 함수 수준 범위를 가집니다 — 함수 외부에 정의된 변수는 'global'이나 $GLOBALS 없이는 함수 내부에서 접근할 수 없습니다. 'global'을 피하세요 — 숨겨진 의존성을 만들고 테스트를 어렵게 합니다. 대신 의존성 주입을 사용하세요. static 변수는 함수 호출 간에 유지되지만 함수에 범위가 지정됩니다 — 캐싱/메모이제이션에 유용하지만 장기 실행 프로세스에서 문제를 일으킬 수 있습니다. 클로저는 'use'로 변수를 명시적으로 캡처해야 합니다.
<?php
$global = "I'm global";
function testScope(): void {
// echo $global; // ERROR: not in scope
global $global; // import global
echo $global; // OK
// $GLOBALS superglobal (alternative)
echo $GLOBALS['global'];
}
// static variables: persist across calls
function counter(): int {
static $count = 0;
return ++$count;
}
echo counter(); // 1
echo counter(); // 2
echo counter(); // 3
// closures don't see outer scope by default
$outer = "hello";
$closure = function () {
// echo $outer; // ERROR
};
$closure2 = function () use ($outer) {
echo $outer; // OK, captured
};타입 선언 & 엄격한 타입
declare(strict_types=1)은 첫 번째 명령문이어야 합니다 — 전체 파일에 대해 엄격한 타입 검사(강제 변환 없음)를 적용합니다. 없으면 PHP가 타입을 강제 변환합니다(문자열 매개변수에 전달된 int 5는 '5'가 됩니다). 새 코드에는 항상 엄격한 타입을 사용하세요. PHP 8+는 유니온 타 입, mixed, never(함수가 반환하지 않음), static(클래스 반환)을 추가합니다. 일급 호출 가능 구문(func(...))은 모든 callable에서 클로저를 생성합니다 — 함수 참조보다 깔끔합니다.
<?php
// declare strict types (FIRST line of file)
declare(strict_types=1);
function divide(float $a, float $b): float {
if ($b === 0.0) {
throw new DivisionByZeroError();
}
return $a / $b;
}
// nullable types (?Type or Type|null)
function findUser(?int $id): ?string {
if ($id === null) return null;
return "User $id";
}
// union types (PHP 8+)
function process(int|string $input): int|string {
return is_int($input) ? $input * 2 : strtoupper($input);
}
// return types: void, never, mixed, static
function log(string $msg): void { /* no return */ }
function redirect(): never { header("Location: /"); exit; }
// first-class callable syntax (PHP 8.1+)
$func = strlen(...); // creates Closure from function
echo $func("hello"); // 5제너레이터 & Yield
제너레이터(yield가 있는 함수)는 값을 지연해서 생성합니다 — 모든 값을 미리 계산하지 않아 메모리를 절약합니다. 이는 큰 파일이나 데이터셋을 처리하는 데 필수적입니다. yield는 함수를 일시 정지하고 값을 반환; 다음 값이 요청되면 함수가 재개됩니다. 제너레이터는 Iterator를 구현하여 foreach와 작동합니다. 파일 처리, 데이터베이스 행 반복, 무한 시퀀스, 파이프라인에 제너레이터를 사용하세요. key=>value 쌍을 yield할 수도 있고 send()로 값을 받을 수도 있습니다.
<?php
// generator: memory-efficient iteration
function readLines(string $file): Generator {
$handle = fopen($file, 'r');
while (($line = fgets($handle)) !== false) {
yield trim($line);
}
fclose($handle);
}
foreach (readLines("large.txt") as $line) {
echo $line; // one line at a time, low memory
}
// infinite generator
function fibonacci(): Generator {
[$a, $b] = [0, 1];
while (true) {
yield $a;
[$a, $b] = [$b, $a + $b];
}
}
// take first 10
$fib = fibonacci();
for ($i = 0; $i < 10; $i++) {
echo $fib->current() . " ";
$fib->next();
}
// yield with key
function pairs(): Generator {
yield 'a' => 1;
yield 'b' => 2;
yield 'c' => 3;
}OOP & 클래스
클래스, 속성 & 생성자
PHP 8 생성자 프로모션은 보일러플레이트를 제거합니다 — 속성을 생성자 매개변수로 선언합니다. 속성 가시성: public(어디서나), protected(클래스 + 서브클래스), private(클래스만). readonly(PHP 8.1)는 초기화 후 수정을 방지합니다. self는 현재 클래스를 참조; static은 호출 클래스를 참조(지연 정적 바인딩). 상속 계층에서 적절한 다형성을 위해 self:: 대신 static::을 사용하세요.
<?php
class Person {
// typed properties (PHP 7.4+)
public string $name;
protected int $age;
private string $email;
public readonly string $id; // PHP 8.1+
// constructor promotion (PHP 8+)
public function __construct(
string $name,
int $age,
string $email = "",
string $id = ""
) {
$this->name = $name;
$this->age = $age;
$this->email = $email;
$this->id = $id;
}
// methods
public function greet(): string {
return "Hi, I'm {$this->name}";
}
// static method
public static function create(string $name): self {
return new self($name, 0);
}
}
$p = new Person("Alice", 30);
echo $p->greet();
echo Person::create("Bob")->name;상속 & 추상 클래스
abstract 클래스는 인스턴스화할 수 없습니다 — 서브클래스를 위한 템플릿을 정의합니다. 추상 메서드는 구체적인 서브클래스가 구현해야 합니다. PHP는 단일 상속만 지원합니다(하나의 extends). 구현이 변경되지 않아야 할 때 final로 상속/오버라이딩을 방지하세요. protected 멤버는 서브클래스에서 접근 가능 — 내부 API에 사용하세요. 부모에 생성자가 있으면 항상 parent::__construct()를 호출하세요. instanceof로 타입 확인: if ($dog instanceof Animal).
<?php
abstract class Animal {
protected string $name;
public function __construct(string $name) {
$this->name = $name;
}
// abstract method: must be implemented by subclasses
abstract public function speak(): string;
// concrete method: inherited as-is
public function describe(): string {
return "{$this->name} says {$this->speak()}";
}
}
class Dog extends Animal {
public function speak(): string {
return "Woof";
}
}
class Cat extends Animal {
public function speak(): string {
return "Meow";
}
}
$dog = new Dog("Rex");
echo $dog->describe(); // Rex says Woof
// final class/method: cannot be extended/overridden
final class Singleton { /* ... */ }인터페이스 & 트레이트
인터페이스는 계약을 정의 — 클래스는 여러 인터페이스를 구현할 수 있습니다(단일 상속과 달리). 모든 인터페이스 메서드는 public이어야 합니다. 트레이트는 상속 없는 코드 재사용을 제공 — 언어 수준의 '복사-붙여넣기'입니다. 트레이트는 속성, 메서드, 추상 메서드를 가질 수 있습니다. 횡단 관심사(타임스탬프, 로깅, 소프트 삭제)에 트레이트를 사용하세요. 충돌 해결: 트레이트에 같은 메서드 이름이 있을 때 TraitA::method insteadof TraitB를 사용하세요.
<?php
// interface: contract (no implementation)
interface Comparable {
public function compareTo(object $other): int;
}
interface JsonSerializable {
public function jsonSerialize(): mixed;
}
// a class can implement multiple interfaces
class Product implements Comparable, JsonSerializable {
public function __construct(public float $price) {}
public function compareTo(object $other): int {
return $this->price <=> $other->price;
}
public function jsonSerialize(): mixed {
return ['price' => $this->price];
}
}
// trait: reusable code (horizontal reuse)
trait Timestampable {
public DateTime $createdAt;
public function setCreatedAt(): void {
$this->createdAt = new DateTime();
}
public function age(): DateInterval {
return $this->createdAt->diff(new DateTime());
}
}
class Article {
use Timestampable; // use trait
}매직 메서드
매직 메서드는 객체 작업을 가로채는 특수 메서드입니다. __get/__set은 속성 오버로딩(동적 속성)을 구현합니다. __toString은 문자열 캐스팅을 가능하게 합니다. __invoke는 객체를 호출 가능하게 만듭니다. __clone은 복제 시 실행됩니다(clone $obj). 추적하기 어려운 '매직'을 추가하므로 드물게 사용하세요. __get/__set은 데이터 전송 객체나 지연 로딩에 유용합니다. 매직 동작을 항상 명확히 문서화하세요. PHP 8.2는 동적 속성을 사용 중단 — __get/__set이나 #[AllowDynamicProperties]를 사용하세요.
<?php
class Magic {
private array $data = [];
// called when accessing undefined property
public function __get(string $name): mixed {
return $this->data[$name] ?? null;
}
// called when setting undefined property
public function __set(string $name, mixed $value): void {
$this->data[$name] = $value;
}
// called when isset() or empty() on undefined property
public function __isset(string $name): bool {
return isset($this->data[$name]);
}
// called when object is used as string
public function __toString(): string {
return json_encode($this->data);
}
// called when object is called as function
public function __invoke(string $arg): string {
return "Called with: $arg";
}
// called on clone
public function __clone(): void {
$this->data = []; // reset on clone
}
}
$m = new Magic();
$m->foo = "bar"; // __set
echo $m->foo; // __get -> "bar"
echo $m; // __toString -> {"foo":"bar"}
echo $m("test"); // __invoke네임스페이스 & 오토로딩
네임스페이스는 클래스 이름 충돌을 방지합니다 — Java의 패키지와 같습니다. 네임스페이스는 첫 번째 명령문이어야 합니다. use는 클래스를 임포트합니다(선택적 별칭: use Foo\Bar as B). PSR-4 오토로딩은 네임스페이스를 파일 경로에 매핑: App\Models\User → src/Models/User.php. Composer의 오토로더(require 'vendor/autoload.php')가 이를 자동으로 처리합니다. 현대 PHP에는 항상 네임스페이스를 사용하세요. 문자열의 \\는 이스케이프된 백슬래시(네임스페이스 구분자)입니다.
<?php
// file: src/Models/User.php
namespace App\Models;
use App\Database\Connection;
use App\Exceptions\UserNotFoundException;
class User {
private Connection $db;
public function __construct(Connection $db) {
$this->db = $db;
}
public function find(int $id): ?self {
// ...
throw new UserNotFoundException("User $id not found");
}
}
// composer.json (PSR-4 autoloading)
// {
// "autoload": {
// "psr-4": { "App\\": "src/" }
// }
// }
// usage
use App\Models\User;
$user = new User($db);웹, 폼 & 파일 I/O
폼 처리 & 검증
항상 서버 측에서 검증 — 클라이언트 측 검증은 보안이 아닌 UX용입니다. filter_input/filter_var와 FILTER_VALIDATE_*는 잘못된 입력에 대해 false를 반환합니다. 검증 전에 문자열을 trim하세요. 데이터베이스 삽입에는 준비된 문장을 사용하세요. CSRF 토큰은 크로스 사이트 요청 위조를 방지 — 세션당 생성하고 POST에서 검증합니다. bin2hex(random_bytes(32))는 암호학적으로 안전한 토큰을 생성합니다. 사용자 입력을 절대 신뢰하지 마세요 — 검증, 살균, 이스케이프하세요.
<?php
// process POST form
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$name = trim($_POST['name'] ?? '');
$email = filter_input(INPUT_POST, 'email', FILTER_VALIDATE_EMAIL);
$age = filter_var($_POST['age'] ?? 0, FILTER_VALIDATE_INT);
$errors = [];
if (empty($name)) {
$errors[] = "Name is required";
} elseif (strlen($name) > 100) {
$errors[] = "Name too long";
}
if ($email === false) {
$errors[] = "Valid email required";
}
if ($age === false || $age < 18) {
$errors[] = "Must be 18+";
}
if (empty($errors)) {
// process valid data
// save to database, redirect, etc.
header("Location: /success");
exit;
}
}
// CSRF token
session_start();
$token = bin2hex(random_bytes(32));
$_SESSION['csrf'] = $token;
// in form: <input type="hidden" name="csrf" value="<?= $token ?>">세션 & 쿠키
세션은 서버 측에 데이터를 저장합니다(세션 ID 쿠키로 식별). session_start()는 출력 전에 호출해야 합니다(또는 ob_start() 사용). 세션에 최소한의 데이터를 저장 — 서버 메모리를 소비합니다. 쿠키의 경우 항상 secure(HTTPS만), httponly(XSS 접근 방지), samesite(CSRF 보호)를 설정하세요. 세션을 적절히 파괴: 변수 해제, 세션 파괴, 쿠키 삭제. 확장 가능한 앱의 경우 파일 대신 Redis/데이터베이스 기반 세션 핸들러를 사용하세요.
<?php
// start session (must be before any output)
session_start();
// set session data
$_SESSION['user_id'] = 42;
$_SESSION['username'] = "Alice";
$_SESSION['login_time'] = time();
// read session data
$userId = $_SESSION['user_id'] ?? null;
// check if logged in
function isLoggedIn(): bool {
return isset($_SESSION['user_id']);
}
// destroy session
session_unset(); // clear variables
session_destroy(); // destroy session
setcookie(session_name(), '', time() - 3600, '/'); // clear cookie
// cookies
setcookie("theme", "dark", [
'expires' => time() + 86400 * 30, // 30 days
'path' => '/',
'secure' => true, // HTTPS only
'httponly' => true, // not accessible via JS
'samesite' => 'Strict', // CSRF protection
]);
// read cookie
$theme = $_COOKIE['theme'] ?? 'light';파일 I/O
file_get_contents/file_put_contents는 작은 파일에 편리합니다. 큰 파일의 경우 fopen/fread/fwrite와 스트림을 사용하세요. fgetcsv/fputcsv는 CSV 형식을 처리합니다(따옴표/이스케이핑 포함). json_decode에 true를 전달하면 연관 배열을 반환합니다(기본은 객체). 항상 file_exists를 확인하고 에러를 처리하세요(권한, 디스크 가득 참). 파일 업로드의 경우 보안을 위해 move_uploaded_file()을 사용하세요. 동시에 쓸 때 flock()으로 파일을 잠그세요.
<?php
// read entire file
$content = file_get_contents("data.txt");
$lines = file("data.txt", FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
// write file
file_put_contents("output.txt", "Hello World");
file_put_contents("log.txt", "entry\n", FILE_APPEND); // append
// CSV
$csv = fopen("data.csv", "r");
while (($row = fgetcsv($csv)) !== false) {
print_r($row); // array of column values
}
fclose($csv);
// write CSV
$out = fopen("export.csv", "w");
fputcsv($out, ["Name", "Email", "Age"]);
fputcsv($out, ["Alice", "[email protected]", 30]);
fclose($out);
// JSON
$data = json_decode(file_get_contents("config.json"), true); // assoc array
file_put_contents("config.json", json_encode($data, JSON_PRETTY_PRINT));
// file info
file_exists("data.txt"); // bool
filesize("data.txt"); // bytes
filemtime("data.txt"); // modification timestamp
is_dir("folder"); // bool파일 업로드
파일 업로드는 $_POST가 아닌 $_FILES를 통해 옵니다. 항상 검증: 에러 코드 확인, finfo로 MIME 타입 확인($_FILES['type']은 클라이언트 제공이며 위조 가능하므로 사용하지 마세요), 크기 제한, 안전한 파일명 생성(원본 이름을 절대 신뢰하지 마세요). move_uploaded_file()은 보안 함수 — HTTP POST를 통해 업로드되었는지 확인합니다. 웹 루트 외부에 저장하거나 직접 접근을 방지하기 위해 PHP를 통해 서브하세요. 업로드를 맬웨어 스캔하는 것을 고려하세요.
<?php
// HTML: <form method="POST" enctype="multipart/form-data">
// <input type="file" name="document">
// <input type="submit">
// </form>
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$file = $_FILES['document'];
// $file contains:
// ['name'] => original filename
// ['type'] => MIME type (unreliable!)
// ['tmp_name'] => temporary path
// ['error'] => UPLOAD_ERR_OK (0) on success
// ['size'] => file size in bytes
if ($file['error'] !== UPLOAD_ERR_OK) {
die("Upload error: " . $file['error']);
}
// validate
$allowedTypes = ['application/pdf', 'image/jpeg', 'image/png'];
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mimeType = $finfo->file($file['tmp_name']);
if (!in_array($mimeType, $allowedTypes)) {
die("Invalid file type");
}
if ($file['size'] > 5 * 1024 * 1024) { // 5MB
die("File too large");
}
// move to permanent location
$dest = "uploads/" . uniqid() . "_" . $file['name'];
move_uploaded_file($file['tmp_name'], $dest);
echo "Uploaded to: $dest";
}cURL & HTTP 요청
cURL은 PHP의 표준 HTTP 클라이언트 — HTTPS, 리다이렉트, 쿠키, 인증을 처리합니다. 항상 CURLOPT_RETURNTRANSFER을 설정하여 응답을 문자열로 받으세요(그렇지 않으면 출력됨). 멈추지 않도록 타임아웃을 설정하세요. 단순한 요청의 경우 file_get_contents와 stream_context가 작동하지만 기능이 부족합니다. 프로덕션의 경우 Guzzle(composer require guzzlehttp/guzzle)이나 Symfony HTTP Client를 사용하세요 — 더 나은 API, 재시도 로직, PSR-18 준수를 제공합니다.
<?php
// GET request
$ch = curl_init("https://api.example.com/users");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode === 200) {
$data = json_decode($response, true);
print_r($data);
}
// POST request with JSON
$ch = curl_init("https://api.example.com/users");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode(['name' => 'Alice']),
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'Authorization: Bearer ' . $token,
],
]);
$response = curl_exec($ch);
curl_close($ch);
// simpler: file_get_contents with context
$context = stream_context_create([
'http' => [
'method' => 'POST',
'header' => "Content-Type: application/json\r\n",
'content' => json_encode(['name' => 'Alice']),
],
]);
$result = file_get_contents("https://api.example.com/users", false, $context);데이터베이스 (PDO)
PDO 연결 & 기본
PDO(PHP Data Objects)는 표준 데이터베이스 추상화 계층 — MySQL, PostgreSQL, SQLite 등을 지원합니다. 적절한 에러 처리를 위해 ERRMODE_EXCEPTION을, 실제 준비된 문장을 위해 ATTR_EMULATE_PREPARES=false를 항상 설정하세요. FETCH_ASSOC은 연관 배열을 반환(FETCH_OBJ는 객체, FETCH_CLASS는 클래스에 매핑). 완전한 Unicode 지원(이모지 포함)을 위해 항상 utf8mb4 문자셋을 사용하세요. 싱글톤이나 DI 컨테이너에 연결을 저장하세요.
<?php
// connect (always use exception mode)
$dsn = "mysql:host=localhost;dbname=test;charset=utf8mb4";
$pdo = new PDO($dsn, "username", "password", [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false, // use real prepared statements
]);
// simple query
$stmt = $pdo->query("SELECT * FROM users LIMIT 5");
$users = $stmt->fetchAll(); // array of associative arrays
// fetch one row
$user = $pdo->query("SELECT * FROM users WHERE id = 1")->fetch();
// fetch column
$count = $pdo->query("SELECT COUNT(*) FROM users")->fetchColumn();
// execute (no params)
$pdo->exec("DELETE FROM logs WHERE created_at < '2023-01-01'");
$deleted = $pdo->rowCount(); // affected rows준비된 문장 (SQL 인젝션 방지)
준비된 문장은 사용자 입력이 있는 모든 쿼리에 필수 — SQL 구조와 데이터를 분리하여 인젝션을 불가능하게 합니다. 위치는 ?로, 명명된 매개변수는 :name으로 사용하세요. IN 절의 경우 자리 표시자 문자열을 동적으로 빌드해야 합니다(하지만 값은 여전히 매개변수화됨). lastInsertId()는 마지막 자동 증가 값을 반환합니다. 이스케이핑 함수로도 사용자 입력을 SQL에 연결하지 마세요. 이는 PHP의 #1 보안 규칙입니다.
<?php
// prepared statements: THE way to prevent SQL injection
$stmt = $pdo->prepare("SELECT * FROM users WHERE email = ? AND status = ?");
$stmt->execute([$email, 'active']);
$user = $stmt->fetch();
// named parameters (more readable)
$stmt = $pdo->prepare(
"INSERT INTO users (name, email, age) VALUES (:name, :email, :age)"
);
$stmt->execute([
':name' => 'Alice',
':email' => '[email protected]',
':age' => 30,
]);
$id = $pdo->lastInsertId(); // get auto-increment ID
// IN clause with prepared statements
$ids = [1, 2, 3, 4];
$placeholders = implode(',', array_fill(0, count($ids), '?'));
$stmt = $pdo->prepare("SELECT * FROM users WHERE id IN ($placeholders)");
$stmt->execute($ids);
$users = $stmt->fetchAll();
// NEVER do this (SQL injection!):
// $pdo->query("SELECT * FROM users WHERE name = '$_GET[name]'");트랜잭션 & 에러 처리
트랜잭션은 원자성을 보장 — 모든 작업이 성공하거나 모두 실패합니다. beginTransaction/commit/rollBack으로 작업 단위를 감쌉니다. 항상 트랜잭션을 try/catch로 감싸고 예외 발생 시 롤백하세요. PDO는 에러 시 PDOException을 던집니다(ERRMODE_EXCEPTION 사용). 잠금 경합을 줄이기 위해 트랜잭션을 짧게 유지하세요. 중첩 트랜잭션의 경우 저장점이나 트랜잭션 매니저를 사용하세요. 트랜잭션을 열어두지 마세요 — 항상 커밋하거나 롤백하세요.
<?php
try {
$pdo->beginTransaction();
$pdo->prepare("UPDATE accounts SET balance = balance - ? WHERE id = ?")
->execute([$amount, $fromId]);
$pdo->prepare("UPDATE accounts SET balance = balance + ? WHERE id = ?")
->execute([$amount, $toId]);
// log transaction
$pdo->prepare("INSERT INTO transfers (from_id, to_id, amount) VALUES (?, ?, ?)")
->execute([$fromId, $toId, $amount]);
$pdo->commit();
echo "Transfer complete";
} catch (PDOException $e) {
$pdo->rollBack(); // undo all changes
error_log("Transfer failed: " . $e->getMessage());
throw new RuntimeException("Transfer failed", 0, $e);
}
// check if in transaction
if ($pdo->inTransaction()) {
$pdo->commit();
}데이터 가져오기 패턴
사용 사례에 맞는 가져오기 모드를 선택하세요. FETCH_ASSOC가 가장 일반적(열 이름이 있는 배열). FETCH_CLASS는 행을 객체로 매핑 — 도메인 모델에 적합. FETCH_KEY_PAIR는 id=>value 맵 생성(드롭다운용). FETCH_GROUP은 첫 번째 열로 행을 그룹화 — 일대다 관계에 유용. 큰 결과 집합의 경우 메모리를 절약하기 위해 fetchAll() 대신 루프에서 fetch()를 사용하세요. 완료 후 항상 $stmt->closeCursor()로 커서를 닫으세요.
<?php
$stmt = $pdo->prepare("SELECT id, name, email FROM users WHERE active = 1");
$stmt->execute();
// fetch modes
$row = $stmt->fetch(PDO::FETCH_ASSOC); // ['id' => 1, 'name' => 'Alice']
$row = $stmt->fetch(PDO::FETCH_NUM); // [1, 'Alice', '[email protected]']
$row = $stmt->fetch(PDO::FETCH_BOTH); // both (default)
$obj = $stmt->fetch(PDO::FETCH_OBJ); // stdClass with properties
// fetch all
$all = $stmt->fetchAll(PDO::FETCH_ASSOC);
// fetch into class
class User {
public int $id;
public string $name;
}
$stmt->setFetchMode(PDO::FETCH_CLASS, User::class);
$users = $stmt->fetchAll(); // array of User objects
// fetch key-value pairs
$pairs = $pdo->query("SELECT id, name FROM users")
->fetchAll(PDO::FETCH_KEY_PAIR); // [1 => 'Alice', 2 => 'Bob']
// fetch grouped
$grouped = $pdo->query("SELECT dept, name FROM employees")
->fetchAll(PDO::FETCH_GROUP | PDO::FETCH_ASSOC);
// ['IT' => [['name' => 'Alice']], 'HR' => [['name' => 'Bob']]]데이터베이스 모범 사례
리포지토리 패턴은 데이터 접근을 비즈니스 로직에서 분리 — 코드를 테스트 가능(PDO 모킹)하고 유지보수 가능하게 만듭니다. 의존성 주입으로 PDO 연결을 전달하세요. 쿼리당 새 PDO 연결을 만들지 마세요 — 단일 연결(또는 풀)을 재사용하세요. 고트래픽 앱의 경우 연결 풀러(MySQL은 ProxySQL, PostgreSQL은 PgBouncer)를 고려하세요. 항상 EXPLAIN으로 느린 쿼리를 프로파일링하고 적절한 인덱스를 추가하세요. 복잡한 도메인의 경우 ORM(Doctrine, Eloquent)을 고려하세요.
<?php
// 1. Connection singleton (or use DI container)
class Database {
private static ?PDO $instance = null;
public static function conn(): PDO {
if (self::$instance === null) {
self::$instance = new PDO(
"mysql:host=localhost;dbname=app;charset=utf8mb4",
"user", "pass",
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]
);
}
return self::$instance;
}
}
// 2. Repository pattern
class UserRepository {
public function __construct(private PDO $db) {}
public function findById(int $id): ?array {
$stmt = $this->db->prepare("SELECT * FROM users WHERE id = ?");
$stmt->execute([$id]);
$user = $stmt->fetch();
return $user ?: null;
}
}
// 3. Always close statements (or let them go out of scope)
// 4. Use LIMIT for queries that might return huge results
// 5. Index columns used in WHERE, JOIN, ORDER BY
// 6. Use EXPLAIN to analyze slow queries날짜/시간 & 보안
날짜 & 시간
PHP의 날짜 함수는 기본적으로 서버의 시간대를 사용 — 항상 date_default_timezone_set('Asia/Shanghai')를 설정하거나 DateTimeZone을 명시적으로 사용하세요. DateTime 클래스는 객체 지향적이며 시간대, 간격, 형식화를 절차적 함수보다 더 잘 처리합니다. strtotime()은 영어 날짜 설명('next Monday', '+1 month')을 파싱 — 편리하지만 월 경계에서 놀랄 수 있습니다. 날짜 계산에는 DateTime::diff()와 DateInterval을 사용하세요. 데이터베이스에는 항상 UTC로 날짜를 저장하세요.
<?php
// current time
$now = date('Y-m-d H:i:s'); // 2024-06-15 14:30:00
$timestamp = time(); // Unix timestamp
$dt = new DateTime(); // DateTime object
// formatting
echo date('Y-m-d'); // 2024-06-15
echo date('d/m/Y H:i:s'); // 15/06/2024 14:30:00
echo date('l, F j, Y'); // Saturday, June 15, 2024
// create from string
$dt = new DateTime('2024-01-15');
$dt = DateTime::createFromFormat('d/m/Y', '15/01/2024');
// modify dates
$dt->modify('+1 month');
$dt->modify('-2 days');
$tomorrow = date('Y-m-d', strtotime('tomorrow'));
$nextWeek = date('Y-m-d', strtotime('+1 week'));
// difference
$start = new DateTime('2024-01-01');
$end = new DateTime('2024-12-31');
$diff = $start->diff($end);
echo $diff->days; // 365
// timezone
$dt = new DateTime('now', new DateTimeZone('Asia/Shanghai'));
$dt->setTimezone(new DateTimeZone('UTC'));비밀번호 해싱 & 보안
password_hash()는 bcrypt(또는 가능한 경우 Argon2)와 자동 솔트 생성을 사용 — 절대 직접 해싱을 만들지 마세요. password_verify()는 해시와 비밀번호를 안전하게 확인(타이밍 공격 방지를 위한 상수 시간 비교). password_needs_rehash()는 비용 요소를 증가시킬 때 해시를 업그레이드할 수 있게 합니다. 랜덤 토큰(CSRF, API 키, 비밀번호 재설정)의 경우 예측 가능한 rand()나 mt_rand()가 아닌 항상 random_bytes()를 사용하세요. 메시지 인증에는 hash_hmac을 사용하세요.
<?php
// hash password (bcrypt by default)
$hash = password_hash("mypassword", PASSWORD_DEFAULT);
// $2y$10$... (includes algorithm, cost, salt)
// verify password
if (password_verify($input, $hash)) {
echo "Valid password";
}
// check if hash needs rehash (algorithm upgrade)
if (password_needs_rehash($hash, PASSWORD_DEFAULT)) {
$newHash = password_hash($input, PASSWORD_DEFAULT);
// update stored hash
}
// NEVER use md5() or sha1() for passwords!
// NEVER store plaintext passwords!
// generate secure random
$token = bin2hex(random_bytes(32)); // 64-char hex string
$bytes = random_bytes(16); // raw bytes
$int = random_int(1, 1000); // cryptographically secure
// hash for integrity (not passwords)
$checksum = hash('sha256', $data);
$hmac = hash_hmac('sha256', $data, $secretKey);출력 이스케이핑 & XSS 방지
XSS는 #1 웹 취약점 — 항상 컨텍스트에 따라 출력을 이스케이프하세요. HTML에는 htmlspecialchars()(ENT_QUOTES는 작은따옴표와 큰따옴표 모두 이스케이프). URL에는 urlencode(). JavaScript 컨텍스트에는 hex 플래그와 함께 json_encode(). 사용자 입력을 절대 신뢰하지 마세요 — 입력이 아닌 출력에서 이스케이프(다른 곳에서 원시 데이터가 필요할 수 있음). 심층 방어를 위해 Content-Security-Policy 헤더를 설정하세요. 기본적으로 자동 이스케이프하는 템플릿 엔진(Twig, Blade)을 고려하세요.
<?php
// XSS: Cross-Site Scripting — always escape output!
$name = $_GET['name']; // could be: <script>alert('xss')</script>
// HTML context
echo htmlspecialchars($name, ENT_QUOTES, 'UTF-8');
// converts < > " ' & to HTML entities
// in HTML template
?>
<p>Hello, <?= htmlspecialchars($name, ENT_QUOTES, 'UTF-8') ?></p>
<input value="<?= htmlspecialchars($value, ENT_QUOTES, 'UTF-8') ?>">
<?php
// URL context
$url = "https://example.com/search?q=" . urlencode($query);
// JavaScript context
$json = json_encode($data, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT);
?>
<script>
var data = <?= $json ?>;
</script>
<?php
// Content Security Policy header
header("Content-Security-Policy: default-src 'self'; script-src 'self'");JSON & API 응답
json_encode/decode는 표준 JSON 함수입니다. API 응답에는 항상 Content-Type: application/json을 설정하세요. 중국어/이모지를 읽을 수 있게 유지하려면 JSON_UNESCAPED_UNICODE를 사용하세요(그렇지 않으면 \uXXXX가 됨). json_decode에 true를 전달하면 연관 배열을 반환(PHP에서 더 일반적). 신뢰할 수 없는 JSON 디코딩 후 항상 json_last_error()를 확인하세요. REST API의 경우 적절한 HTTP 상태 코드(200, 201, 400, 404, 500)를 설정하고 일관된 응답 구조를 사용하세요.
<?php
// encode PHP array/object to JSON
$data = ['name' => 'Alice', 'age' => 30, 'hobbies' => ['reading', 'coding']];
$json = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
// {"name": "Alice", "age": 30, "hobbies": ["reading", "coding"]}
// decode JSON to PHP
$obj = json_decode($json); // stdClass object
$arr = json_decode($json, true); // associative array
// handle errors
$data = json_decode($badJson, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new RuntimeException("JSON error: " . json_last_error_msg());
}
// API response
header('Content-Type: application/json; charset=utf-8');
http_response_code(200);
echo json_encode([
'status' => 'success',
'data' => $users,
'meta' => ['page' => 1, 'total' => 100],
], JSON_UNESCAPED_UNICODE);
// API request handling
$input = json_decode(file_get_contents('php://input'), true);Composer & 의존성 관리
Composer는 PHP의 패키지 매니저 — 현대 PHP에 필수적입니다. require는 프로덕션 의존성을 지정; require-dev는 개발용(테스트 등). PSR-4 오토로딩은 네임스페이스를 디렉토리에 매핑합니다. 항상 composer.json과 composer.lock을 커밋하세요(정확한 버전 잠금). 프로덕션에서는 composer install(lock에서), 최신을 위해 composer update를 사용하세요. 인기 패키지: Monolog(로깅), Guzzle(HTTP), PHPUnit(테스트), Symfony 컴포넌트, Laravel 프레임워크. vendor/ 디렉토리를 절대 커밋하지 마세요.
<?php
// composer.json
// {
// "require": {
// "monolog/monolog": "^3.0",
// "guzzlehttp/guzzle": "^7.0"
// },
// "autoload": {
// "psr-4": {"App\\": "src/"}
// }
// }
// install: composer install
// update: composer update
// add: composer require monolog/monolog
// autoload (at the top of your app)
require 'vendor/autoload.php';
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
$log = new Logger('app');
$log->pushHandler(new StreamHandler('app.log', Logger::WARNING));
$log->warning('User not found', ['user_id' => 42]);
// environment variables (vlucas/phpdotenv)
$dotenv = Dotenv\Dotenv::createImmutable(__DIR__);
$dotenv->load();
$dbHost = $_ENV['DB_HOST'];REST API 개발
JSON 요청 & 응답 처리
REST API는 JSON을 교환합니다. 폼 제출($_POST 채움)과 달리 JSON 요청은 php://input에서 읽고 json_decode로 디코딩해야 합니다. 응답에는 항상 Content-Type: application/json을 설정하고 적절한 HTTP 상태 코드를 위해 http_response_code()를 사용하세요. 모든 입력을 검증 — json_decode는 예상 구조를 보장하지 않습니다. 안전한 접근을 위해 널 병합 연산자(??)를 사용하세요. JSON_PRETTY_PRINT는 디버깅에 유용하지만 더 작은 페이로드를 위해 프로덕션에서는 생략하세요.
// Get JSON from request body (not $_POST for JSON!)
$json = file_get_contents('php://input');
$data = json_decode($json, true); // true = associative array
if (json_last_error() !== JSON_ERROR_NONE) {
http_response_code(400);
echo json_encode(['error' => 'Invalid JSON']);
exit;
}
// Process the data
$name = $data['name'] ?? '';
$email = filter_var($data['email'] ?? '', FILTER_VALIDATE_EMAIL);
// Return JSON response
header('Content-Type: application/json');
http_response_code(200); // 200 OK, 201 Created, 400 Bad Request, etc.
echo json_encode([
'success' => true,
'data' => ['id' => 1, 'name' => $name],
'message' => 'User created'
], JSON_PRETTY_PRINT);라우팅 & HTTP 메서드
REST API는 HTTP 메서드를 CRUD 작업에 매핑: GET(읽기), POST(생성), PUT/PATCH(수정), DELETE(삭제). 라우팅은 메서드 + URL 경로를 핸들러에 매칭합니다. 매개변수화된 라우트(예: /api/users/42)에는 preg_match를 사용하세요. 프로덕션에서는 더 깔끔한 라우팅, 미들웨어, 의존성 주입을 위해 라우터 라이브러리(FastRoute, Symfony Routing)나 프레임워크(Laravel, Slim)를 사용하세요. 항상 적절한 HTTP 상태 코드 반환: 200(OK), 201(Created), 204(No Content), 400(Bad Request), 404(Not Found), 500(Server Error).
// Simple REST router based on method + path
$method = $_SERVER['REQUEST_METHOD'];
$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$path = trim($path, '/');
// Route table: method => [pattern => handler]
switch (true) {
case ($method === 'GET' && $path === 'api/users'):
echo json_encode(getAllUsers());
break;
case ($method === 'GET' && preg_match('#^api/users/(\d+)$#', $path, $m)):
echo json_encode(getUser((int)$m[1]));
break;
case ($method === 'POST' && $path === 'api/users'):
$data = json_decode(file_get_contents('php://input'), true);
echo json_encode(createUser($data));
http_response_code(201);
break;
case ($method === 'PUT' && preg_match('#^api/users/(\d+)$#', $path, $m)):
$data = json_decode(file_get_contents('php://input'), true);
echo json_encode(updateUser((int)$m[1], $data));
break;
case ($method === 'DELETE' && preg_match('#^api/users/(\d+)$#', $path, $m)):
deleteUser((int)$m[1]);
http_response_code(204); // No Content
break;
default:
http_response_code(404);
echo json_encode(['error' => 'Not found']);
}API 인증 (JWT)
JWT는 상태 없는 인증을 가능하게 — 서버가 세션을 저장할 필요가 없습니다. 토큰은 비밀 키로 서명된 페이로드(사용자 ID, 만료)를 포함합니다. 클라이언트는 Authorization 헤더(Bearer 토큰)로 토큰을 전송합니다. 서버는 서명을 확인하여 토큰이 변조되지 않았는지 보장합니다. JWT는 API와 마이크로서비스에 적합(공유 세션 저장소 불필요). 하지만 JWT는 만료 전에 취소할 수 없습니다 — 짧은 만료 시간과 리프레시 토큰 전략을 사용하세요. 프로덕션에서는 직접 암호화를 구현하지 말고 firebase/php-jwt 라이브러리를 사용하세요. JWT 페이로드에 민감한 데이터를 절대 저장하지 마세요 — base64로 인코딩된 것이지 암호화되지 않았습니다.
// JWT (JSON Web Token) authentication flow
// 1. Login: verify credentials, issue JWT
function login($email, $password) {
$user = findUserByEmail($email);
if ($user && password_verify($password, $user['password_hash'])) {
// Create JWT: header.payload.signature
$header = base64url_encode(json_encode(['alg' => 'HS256', 'typ' => 'JWT']));
$payload = base64url_encode(json_encode([
'user_id' => $user['id'],
'exp' => time() + 3600 // expires in 1 hour
]));
$signature = hash_hmac('sha256', "$header.$payload", SECRET_KEY, true);
$jwt = "$header.$payload." . base64url_encode($signature);
return $jwt;
}
return null;
}
// 2. Verify JWT on protected routes
function verifyJWT($token) {
$parts = explode('.', $token);
if (count($parts) !== 3) return false;
[$header, $payload, $signature] = $parts;
$expected = base64url_encode(
hash_hmac('sha256', "$header.$payload", SECRET_KEY, true)
);
if (!hash_equals($expected, $signature)) return false;
$data = json_decode(base64url_decode($payload), true);
return ($data['exp'] ?? 0) > time() ? $data : false;
}
// Use firebase/php-jwt library in production!입력 검증 & 살균
입력 검증은 API 보안에 중요합니다. PHP의 filter_var는 내장 검증기(FILTER_VALIDATE_EMAIL, FILTER_VALIDATE_INT, FILTER_VALIDATE_URL)와 min/max 범위 같은 옵션을 제공합니다. 항상 서버 측에서 검증 — 클라이언트 측 검증은 보안이 아닌 UX용입니다. HTML 출력 시 XSS를 방지하기 위해 htmlspecialchars로 문자열을 살균하세요. JSON API의 경우 검증 에러에 대해 설명적인 메시지와 함께 422(Unprocessable Entity)를 반환하세요. 복잡한 규칙의 경우 검증 라이브러리(Respect/Validation, Symfony Validator)를 고려하세요. 사용자 입력을 절대 신뢰하지 마세요 — 타입, 길이, 형식, 비즈니스 규칙을 검증하세요.