Learn/flight_vs_laravel
Flight vs Laravel
Laravel이란?
Laravel은 모든 종소리와 장식을 갖춘 풀 기능 프레임워크로, 놀라운 개발자 중심 생태계를 가지고 있지만, 성능과 복잡성 측면에서 비용이 듭니다. Laravel의 목표는 개발자가 최고 수준의 생산성을 가지도록 하고, 일반적인 작업을 쉽게 만드는 것입니다. Laravel은 풀 기능의 엔터프라이즈 웹 애플리케이션을 구축하려는 개발자에게 훌륭한 선택입니다. 이는 성능과 복잡성 측면에서 일부 trade-off를 동반합니다. Laravel의 기초를 배우는 것은 쉽지만, 프레임워크에 숙달되는 데는 시간이 걸릴 수 있습니다.
또한 Laravel 모듈이 너무 많아서 개발자들은 문제를 해결하는 유일한 방법이 이러한 모듈을 통하는 것처럼 느껴지지만, 실제로는 다른 라이브러리를 사용하거나 직접 코드를 작성할 수 있습니다.
Flight와 비교한 장점
- Laravel은 일반적인 문제를 해결하는 데 사용할 수 있는 개발자와 모듈의 거대한 생태계를 가지고 있습니다.
- Laravel은 데이터베이스와 상호 작용하는 데 사용할 수 있는 풀 기능 ORM을 가지고 있습니다.
- Laravel은 프레임워크를 배우는 데 사용할 수 있는 엄청난 양의 문서와 튜토리얼을 가지고 있습니다. 이는 세부 사항을 파고드는 데 좋을 수 있지만, 지나치게 많아서 나쁠 수도 있습니다.
- Laravel은 애플리케이션을 보호하는 데 사용할 수 있는 내장 인증 시스템을 가지고 있습니다.
- Laravel은 프레임워크를 배우는 데 사용할 수 있는 팟캐스트, 컨퍼런스, 미팅, 비디오 및 기타 리소스를 가지고 있습니다.
- Laravel은 풀 기능의 엔터프라이즈 웹 애플리케이션을 구축하려는 숙련된 개발자를 대상으로 합니다.
Flight와 비교한 단점
- Laravel은 Flight보다 후드 아래에서 훨씬 더 많은 일이 일어나며, 이는 성능 측면에서 극적인 비용을 초래합니다. 자세한 내용은 TechEmpower 벤치마크를 참조하세요.
- Flight는 가볍고 빠르며 사용하기 쉬운 웹 애플리케이션을 구축하려는 개발자를 대상으로 합니다.
- Flight는 단순성과 사용 용이성을 목표로 합니다.
- Flight의 핵심 기능 중 하나는 이전 버전 호환성을 최대한 유지하려 한다는 것입니다. Laravel은 주요 버전 간에 많은 좌절를 초래합니다.
- Flight는 프레임워크의 세계에 처음 발을 들이는 개발자를 위한 것입니다.
- Flight는 의존성이 없지만, Laravel은 끔찍할 정도로 많은 의존성을 가지고 있습니다.
- Flight도 엔터프라이즈 수준 애플리케이션을 할 수 있지만, Laravel만큼 보일러플레이트 코드가 많지 않습니다. 개발자가 조직화하고 잘 구조화된 상태를 유지하기 위해 더 많은 규율이 필요할 것입니다.
- Flight는 개발자에게 애플리케이션에 대한 더 많은 제어를 제공하지만, Laravel은 장면 뒤에 많은 마법이 있어서 좌절스러울 수 있습니다.
Learn/migrating_to_v3
v3로 마이그레이션
대부분의 경우 뒤로 호환성이 유지되었지만, v2에서 v3로 마이그레이션할 때 알아야 할 몇 가지 변경 사항이 있습니다. 설계 패턴과 너무 많이 충돌하는 변경 사항이 있어서 일부 조정이 필요했습니다.
출력 버퍼링 동작
v3.5.0
출력 버퍼링은 PHP 스크립트에서 생성된 출력을 클라이언트로 보내기 전에 PHP 내부 버퍼에 저장하는 프로세스입니다. 이를 통해 클라이언트로 보내기 전에 출력을 수정할 수 있습니다.
MVC 애플리케이션에서 컨트롤러는 "관리자" 역할을 하며 뷰가 무엇을 하는지 관리합니다. 컨트롤러 외부(또는 Flight의 경우 때때로 익명 함수)에서 출력이 생성되는 것은 MVC 패턴을 깨뜨립니다. 이 변경은 MVC 패턴에 더 잘 맞추고 프레임워크를 더 예측 가능하고 사용하기 쉽게 만들기 위한 것입니다.
v2에서는 출력 버퍼링이 자체 출력 버퍼를 일관되게 닫지 않는 방식으로 처리되어 단위 테스트와 스트리밍을 더 어렵게 만들었습니다. 대부분의 사용자에게 이 변경은 실제로 영향을 미치지 않을 수 있습니다. 그러나 콜러블과 컨트롤러 외부(예: 훅)에서 콘텐츠를 에코 출력하는 경우 문제가 발생할 가능성이 큽니다. 훅에서 콘텐츠를 에코 출력하거나 프레임워크가 실제로 실행되기 전에 에코 출력하는 것은 과거에는 작동했을 수 있지만 앞으로는 작동하지 않을 것입니다.
문제가 발생할 수 있는 곳
// index.php
require 'vendor/autoload.php';
// just an example
define('START_TIME', microtime(true));
function hello() {
echo 'Hello World';
}
Flight::map('hello', 'hello');
Flight::after('hello', function(){
// this will actually be fine
echo '<p>This Hello World phrase was brought to you by the letter "H"</p>';
});
Flight::before('start', function(){
// things like this will cause an error
echo '<html><head><title>My Page</title></head><body>';
});
Flight::route('/', function(){
// this is actually just fine
echo 'Hello World';
// This should be just fine as well
Flight::hello();
});
Flight::after('start', function(){
// this will cause an error
echo '<div>Your page loaded in '.(microtime(true) - START_TIME).' seconds</div></body></html>';
});
v2 렌더링 동작 켜기
기존 코드를 v3와 작동하도록 재작성하지 않고 그대로 유지할 수 있나요? 네, 가능합니다! flight.v2.output_buffering 구성 옵션을 true로 설정하여 v2 렌더링 동작을 켤 수 있습니다. 이는 기존 렌더링 동작을 계속 사용할 수 있게 해주지만, 앞으로는 이를 수정하는 것이 권장됩니다. 프레임워크의 v4에서는 이 기능이 제거될 것입니다.
// index.php
require 'vendor/autoload.php';
Flight::set('flight.v2.output_buffering', true);
Flight::before('start', function(){
// Now this will be just fine
echo '<html><head><title>My Page</title></head><body>';
});
// more code
디스패처 변경 사항
v3.7.0
Dispatcher의 정적 메서드를 직접 호출한 경우, 예를 들어 Dispatcher::invokeMethod(), Dispatcher::execute() 등이라면 코드를 업데이트하여 이러한 메서드를 직접 호출하지 않도록 해야 합니다. Dispatcher는 의존성 주입 컨테이너를 더 쉽게 사용할 수 있도록 더 객체 지향적으로 변환되었습니다. Dispatcher와 유사하게 메서드를 호출해야 한다면 $result = $class->$method(...$params);나 call_user_func_array()를 수동으로 사용할 수 있습니다.
halt() stop() redirect() 및 error() 변경 사항
v3.10.0
3.10.0 이전의 기본 동작은 헤더와 응답 본문을 모두 지우는 것이었습니다. 이는 응답 본지만 지우도록 변경되었습니다. 헤더도 지워야 한다면 Flight::response()->clear()를 사용할 수 있습니다.
Learn/configuration
구성
개요
Flight는 애플리케이션의 필요에 맞게 프레임워크의 다양한 측면을 구성할 수 있는 간단한 방법을 제공합니다. 일부는 기본적으로 설정되어 있지만 필요에 따라 재정의할 수 있습니다. 또한 애플리케이션 전반에 걸쳐 사용할 고유 변수를 설정할 수 있습니다.
명확하고 계층화된 구성(파일 기본값 + 환경 비밀값)은 AI 코딩 도구에도 도움이 됩니다. 에이전트가 컨트롤러 내부에서 $_ENV 읽기를 임의로 만들지 않고 리터럴 값과 비밀값을 각각 한 곳에서 알 수 있기 때문입니다.
이해
set 메서드를 통해 구성 값을 설정하여 Flight의 특정 동작을 사용자 지정할 수 있습니다.
Flight::set('flight.log_errors', true);
구조화된 앱(skeleton 포함)에서는 일반적으로 app/config/config.php에서 프로젝트 설정을 로드한 다음 관련 키를 Engine에 적용합니다(예: flight.base_url, flight.views.path). 또한 모든 곳에서 전역 변수를 읽는 대신 작은 구성 객체를 컨트롤러에 주입할 수 있습니다. 이는 테스트와 AGENTS.md를 따르는 에이전트에 더 친숙합니다.
기본 사용법
Flight 구성 옵션
다음은 사용 가능한 모든 구성 설정 목록입니다.
- flight.base_url
?string- Flight가 하위 디렉터리에서 실행 중일 때 요청의 기본 URL을 재정의합니다. (기본값: null) - flight.case_sensitive
bool- URL에 대해 대소문자 구분 일치를 사용합니다. (기본값: false) - flight.handle_errors
bool- Flight가 모든 오류를 내부적으로 처리하도록 허용합니다. (기본값: true) - flight.log_errors
bool- 오류를 웹 서버의 오류 로그 파일에 기록합니다. (기본값: false)- Tracy를 설치한 경우 Tracy는 이 구성이 아닌 Tracy 구성에 따라 오류를 기록합니다.
- flight.debug
bool- 오류 발생 시 브라우저에 자세한 오류 정보(예외 메시지, 코드, 스택 추적)를 출력합니다. (기본값: false)- 프로덕션 환경에서는 절대 활성화하지 마세요 — 내부 애플리케이션 세부 정보가 유출됩니다. 로컬 개발 또는 스테이징 환경에서만 사용하세요.
false로 설정하면 일반적인500 Internal Server Error가 대신 표시됩니다. 서버 측에서 오류를 캡처하려면flight.log_errors와 함께 사용하세요.
- flight.allow_method_override
bool-X-HTTP-Method-Override요청 헤더 또는 POST 본문의_method필드를 통해 HTTP 메서드를 재정의할 수 있도록 허용합니다. (기본값: true)- HTML 폼 기반 메서드 스푸핑이 필요 없는 애플리케이션의 경우 이 값을
false로 설정하는 것이 좋습니다. 클라이언트가 일반 POST 폼을 통해DELETE또는PUT요청을 위조하는 것을 방지할 수 있습니다. - 자세한 내용은 보안을 참조하세요.
- HTML 폼 기반 메서드 스푸핑이 필요 없는 애플리케이션의 경우 이 값을
- flight.views.path
string- 뷰 템플릿 파일이 포함된 디렉터리입니다. (기본값: ./views) - flight.views.extension
string- 뷰 템플릿 파일 확장자입니다. (기본값:.php; 공식 skeleton은 Twig를 사용할 때 이 값을.twig로 설정합니다) - flight.content_length
bool-Content-Length헤더를 설정합니다. (기본값: true)- Tracy를 사용하는 경우 Tracy가 올바르게 렌더링되도록 이 값을 false로 설정해야 합니다.
- flight.v2.output_buffering
bool- 레거시 출력 버퍼링을 사용합니다. v3로 마이그레이션을 참조하세요. (기본값: false)
로더 구성
로더에 대한 추가 구성 설정도 있습니다. 이를 통해 클래스 이름에 _가 포함된 클래스를 자동 로드할 수 있습니다.
// 밑줄이 있는 클래스 로딩 활성화
// 기본값은 true
Loader::$v2ClassLoading = false;
자동 로드는 네임스페이스와 일치하는 폴더 대소문자에 따라 달라진다는 점을 기억하세요. 특히 skeleton의 App\ + app/Controller/ 구조에서 그렇습니다.
프로젝트 구성 및 .env (skeleton 패턴)
Flight 코어는 .env 파일을 요구하지 않습니다. 많은 앱이 PHP 구성 배열만 사용합니다. 공식 skeleton은 Runway가 리터럴 구성을 안전하게 다시 쓸 수 있도록 하면서 비밀값이 git에 포함되지 않도록 구성을 계층화합니다:
.env/ 실제 환경 — 비밀값 및 배포 재정의(gitignore 처리됨).app/config/config.php— 리터럴 PHP 배열 기본값(config_sample.php에서 복사). 이 파일 내에는$_ENV[...]표현식을 사용하지 않는 것이 좋습니다.runway config:set같은 도구는 이 파일을 정적 값으로 다시 쓸 수 있으며 비밀값이 파일에 포함될 수 있습니다.- 부트스트랩에서 병합 — 매핑된 키의 경우 env가 우선합니다. 앱 코드는 컨트롤러에서
$_ENV를 사용하는 대신 구성 객체나$app->get()을 읽습니다.
config_sample.php / config.php의 예시 구조(간소화됨):
<?php
// 리터럴만 포함 — 비밀값은 skeleton 워크플로우에서 .env에 있어야 합니다.
return [
'app' => [
'env' => 'development',
'debug' => true,
'base_url' => '/',
'timezone' => 'UTC',
],
'database' => [
'driver' => 'sqlite', // 또는 mysql, 또는 비활성화하려면 ''
'host' => 'localhost',
'dbname' => '',
'user' => '',
'password' => '',
'file_path' => __DIR__ . '/../../database.sqlite',
],
// ...
];
# .env.example → .env (skeleton)
APP_ENV=development
APP_DEBUG=true
FLIGHT_BASE_URL=/
DB_DRIVER=sqlite
# DB_PASSWORD=...
이러한 분리는 AI 친화적인 프로젝트를 위해 의도적으로 설계되었습니다. 지침은 “기본값은 config.php에, 비밀값은 .env에, Config / Engine을 주입하세요—컨트롤러에서 임의로 env 접근을 만들지 마세요”라고 말할 수 있습니다. 기존 앱은 .env를 완전히 무시하고 단일 구성 파일을 유지할 수 있습니다.
변수
Flight를 사용하면 애플리케이션 어디서나 사용할 수 있도록 변수를 저장할 수 있습니다.
// 변수 저장
Flight::set('id', 123);
// 애플리케이션의 다른 곳에서
$id = Flight::get('id');
변수가 설정되었는지 확인하려면 다음을 수행할 수 있습니다:
if (Flight::has('id')) {
// 무언가를 수행
}
변수를 지우려면 다음을 수행할 수 있습니다:
// id 변수를 지웁니다.
Flight::clear('id');
// 모든 변수를 지웁니다.
Flight::clear();
참고: 변수를 설정할 수 있다고 해서 반드시 사용해야 하는 것은 아닙니다. 이 기능은 드물게 사용하세요. 그 이유는 여기에 저장된 모든 것은 전역 변수가 되기 때문입니다. 전역 변수는 애플리케이션 어디에서나 변경될 수 있어 버그를 추적하기 어렵게 만들기 때문에 좋지 않습니다. 또한 단위 테스트와 같은 것을 복잡하게 만들 수 있습니다. 컨트롤러에 필요한 서비스와 구성은 생성자 주입(skeleton + Dice 설정에서처럼)을 사용하는 것이 좋습니다.
오류 및 예외
모든 오류와 예외는 Flight에 포착되어 error 메서드로 전달됩니다. (flight.handle_errors가 true로 설정된 경우.)
기본 동작은 일부 오류 정보와 함께 일반적인 HTTP 500 Internal Server Error 응답을 보내는 것입니다.
자신의 필요에 따라 이 동작을 재정의할 수 있습니다:
Flight::map('error', function (Throwable $error) {
// 오류 처리
echo $error->getTraceAsString();
});
기본적으로 오류는 웹 서버에 기록되지 않습니다. 구성을 변경하여 활성화할 수 있습니다:
Flight::set('flight.log_errors', true);
404 찾을 수 없음
URL을 찾을 수 없으면 Flight는 notFound 메서드를 호출합니다. 기본 동작은 간단한 메시지와 함께 HTTP 404 Not Found 응답을 보내는 것입니다.
자신의 필요에 따라 이 동작을 재정의할 수 있습니다:
Flight::map('notFound', function () {
// 찾을 수 없음 처리
});
같이 보기
- 설치 - Skeleton 구성,
.env, 부트스트랩 구조. - 자동 로드 - 네임스페이스와 폴더 대소문자.
- Flight 확장 - Flight의 핵심 기능을 확장하고 사용자 지정하는 방법.
- 단위 테스트 - Flight 애플리케이션에 대한 단위 테스트를 작성하는 방법.
- AI 및 개발자 경험 -
AGENTS.md와 일관된 프로젝트 지침. - Tracy - 고급 오류 처리 및 디버깅을 위한 플러그인.
- Tracy 확장 기능 - Tracy를 Flight와 통합하기 위한 확장 기능.
- APM - 애플리케이션 성능 모니터링 및 오류 추적을 위한 플러그인.
- 보안 - 보안 강화 플래그 및 비밀값 처리.
문제 해결
- 구성의 모든 값을 확인하는 데 문제가 있는 경우
var_dump(Flight::get());을 실행할 수 있습니다. - Runway 또는 배포 도구가
config.php를 다시 썼다면 비밀값이 커밋되지 않았는지 확인하세요. skeleton 패턴을 사용할 때는 비밀값을.env또는 실제 환경에 유지하세요.
변경 로그
- 문서 – skeleton 스타일 구성 /
.env계층화 및 새 프로젝트의 Twig 뷰 확장자 기본값을 문서화. - v3.18.1 -
flight.debug및flight.allow_method_override구성 옵션 추가. - v3.5.0 - 레거시 출력 버퍼링 동작을 지원하는
flight.v2.output_buffering구성 추가. - v2.0 - 핵심 구성 추가.
Learn/ai
Flight와 AI 및 개발자 경험
개요
Flight는 AI 코딩 도구와 함께 작동하도록 설계되었습니다. 도구와 싸우지 않습니다. 작고 예측 가능한 API, 공식 스켈레톤의 명확한 앱 레이아웃, 그리고 프로젝트별 지침 파일 덕분에 GitHub Copilot, Cursor, Windsurf, Claude Code, Gemini 같은 어시스턴트가 직접 작성하는 것과 동일한 패턴을 따를 수 있습니다.
LLM 공급자 연결 및 프로젝트 지침 생성을 위한 내장 Runway 명령을 통해 Flight는 매번 동일한 컨텍스트를 붙여넣지 않고도 일관되고 관련성 높은 도움을 얻을 수 있도록 지원합니다.
이해
AI 코딩 어시스턴트는 프로젝트의 컨텍스트, 규칙, 목표를 이해할 때 가장 유용합니다. Flight의 AI 도우미를 사용하면 다음을 할 수 있습니다:
- 널리 사용되는 LLM 공급자(OpenAI, Grok, Claude 등)에 프로젝트를 연결합니다.
- 프로젝트별 지침을 생성하고 업데이트하여 모든 사람이 동일한 안내를 받을 수 있습니다.
- 손으로 작성한 코드와 AI 생성 코드를 하나의 레이아웃으로 유지합니다(특히 스켈레톤을 사용할 때).
이 기능들은 Flight 핵심 CLI(Runway 경유)와 함께 제공되며 공식 flightphp/skeleton 스타터에 미리 연결되어 있습니다.
스켈레톤이 AI를 위해 제공하는 것
공식 스타터는 AGENTS.md를 AI 도구의 기준 문서(source of truth) 로 취급합니다:
| 파일 | 역할 |
|---|---|
AGENTS.md (프로젝트 루트) |
전역 규칙, 부트 흐름, 네임스페이스, DI, "하지 말아야 할 것" |
app/, migrations/, tests/ 등 아래의 범위 지정된 AGENTS.md |
해당 트리에서 작업할 때 유용한 가볍고 폴더별 팁 |
SECURITY.md |
비밀, 헤더, XSS/SQL, 보고—보안은 의도적이고 별도로 유지됩니다. |
스켈레톤에는 Copilot / Cursor / Gemini / Windsurf용 별도 하우스 스타일 파일이 없습니다. 어시스턴트를 루트 AGENTS.md로 안내하고(범위 파일로의 링크를 따라가도록 하세요) 사람은 이 파일들을 완전히 무시하고 README를 사용하면 됩니다. 레이아웃은 어느 쪽이든 동일합니다.
문서는 API를 가르치고, 스켈레톤은 레이아웃을 가르칩니다. 이 문서의 짧은
Flight::예제는 학습에 좋습니다. 스켈레톤 앱에서는 컨트롤러 내부에서 정적 퍼사드 대신App\…클래스, 생성자 주입,$this->app을 선호하세요. 설치 및 자동 로딩을 참조하세요.
기본 사용법
LLM 자격 증명 설정
ai:init 명령은 프로젝트를 LLM 공급자에 연결하는 과정을 안내합니다.
php runway ai:init
다음 항목을 입력하라는 메시지가 표시됩니다:
- 공급자 선택(OpenAI, Grok, Claude 등)
- API 키 입력
- 기본 URL 및 모델 이름 설정
이렇게 하면 이후 LLM 요청(예: 지침 생성)에 사용할 자격 증명이 생성됩니다.
예시:
Welcome to AI Init!
Which LLM API do you want to use? [1] openai, [2] grok, [3] claude: 1
Enter the base URL for the LLM API [https://api.openai.com]:
Enter your API key for openai: sk-...
Enter the model name you want to use (e.g. gpt-4, claude-3-opus, etc) [gpt-4o]:
Credentials saved to .runway-creds.json
프로젝트별 AI 지침 생성
ai:generate-instructions 명령은 당신의 프로젝트에 맞춰 AI 코딩 어시스턴트를 위한 지침을 생성하거나 업데이트합니다.
php runway ai:generate-instructions
몇 가지 질문(설명, 데이터베이스, 템플릿, 보안, 팀 규모 등)에 답하게 됩니다. Flight는 LLM 공급자를 사용하여 지침을 생성하고 주로 다음 위치에 기록합니다:
- 프로젝트 루트의
AGENTS.md(도구에 구애받지 않으며, 공식 스켈레톤과 대부분의 최신 에이전트가 기대하는 형식)
CLI 버전과 옵션에 따라 이 명령은 이전 워크플로를 위한 도구별 사본(예: Copilot, Cursor, Windsurf, Gemini 규칙 파일)을 작성할 수도 있습니다. 스켈레톤에서 새 프로젝트를 시작하는 경우 app/ 아래에 유지하는 범위 지정 AGENTS.md 파일과 함께 AGENTS.md 를 단일 정보 원본으로 취급하세요. 다섯 개의 서로 다른 지침 파일을 수동으로 관리하지 마세요.
예시:
Please describe what your project is for? My awesome API
What database are you planning on using? MySQL
What HTML templating engine will you plan on using (if any)? twig
Is security an important element of this project? (y/n) y
...
AI instructions updated successfully.
이제 AI 도구는 일반적인 PHP 튜토리얼이 아니라 실제 스택과 레이아웃에 맞는 코드를 제안할 수 있습니다.
고급 사용법
- 명령 옵션으로 자격 증명이나 출력 경로를 사용자 지정하세요(각 명령의
--help참조). - 도우미는 OpenAI 호환 API를 제공하는 모든 LLM 공급자와 함께 작동합니다.
- 프로젝트가 발전함에 따라
ai:generate-instructions를 다시 실행하여 에이전트가 동기화된 상태를 유지하세요. - 스켈레톤에서는 보안 정책을
SECURITY.md에, 코딩 레이아웃을AGENTS.md에 유지하여 두 문서 모두 잡동사니가 되지 않도록 하세요. - 에이전트가 API 세부 정보를 필요로 할 때는 docs.flightphp.com과 Flight MCP 서버를 선호하고, 생성된 메서드는
vendor/flightphp/core와 대조하여 확인하세요.
참고 항목
- Flight Skeleton –
AGENTS.md, Twig, SimplePdo, Dice가 AI 친화적 구조로 연결된 공식 스타터 - 설치 – 권장
create-project레이아웃 - 자동 로딩 – 폴더 대소문자가 네임스페이스와 일치(
App\Controller↔app/Controller/) - Runway CLI –
ai:*및 스캐폴딩 명령을 지원하는 CLI - 보안 – 에이전트(및 인간)가 약화시키지 말아야 할 안전한 기본값
문제 해결
- ".runway-creds.json이 없습니다"라는 메시지가 표시되면 먼저
php runway ai:init를 실행하세요. - API 키가 유효하고 선택한 모델에 액세스할 수 있는지 확인하세요.
- 지침이 업데이트되지 않으면 프로젝트 디렉토리의 파일 권한을 확인하세요.
- 에이전트가 잘못된 Flight API나 잘못된 폴더 레이아웃을 생성하면 루트
AGENTS.md및 이 문서 사이트를 안내하세요.app/아래 코드는 스켈레톤 레이아웃이 우선입니다.
변경 로그
- v3.18.4 –
ai:generate-instructions가 프로젝트 지침을 프로젝트 루트의AGENTS.md에 작성합니다. - v3.16.0 – AI 통합을 위한
ai:init및ai:generate-instructionsCLI 명령 추가.
Learn/unit_testing_and_solid_principles
이 기사는 원래 2015년에 Airpair에서 출판되었습니다. 모든 크레딧은 Airpair와 원래 이 기사를 작성한 Brian Fenton에게 돌아갑니다. 웹사이트는 더 이상 이용할 수 없으며, 기사는 Wayback Machine 내에만 존재합니다. 이 기사는 PHP 커뮤니티의 학습 및 교육 목적으로 사이트에 추가되었습니다.
1 설정 및 구성
1.1 최신 상태 유지
가장 먼저 강조할 점은, 야생에서 사용되는 PHP 설치의 놀라울 정도로 적은 수가 최신 상태이거나 최신으로 유지된다는 것입니다. 이는 공유 호스팅 제한, 변경되지 않은 기본 설정, 또는 업그레이드 테스트를 위한 시간/예산 부족 때문일 수 있습니다. 그래서 가장 명확한 최선의 관행은 항상 PHP의 최신 버전(이 기사 작성 시 5.6.x)을 사용하는 것입니다. 게다가, PHP 자체와 사용 중인 확장이나 공급자 라이브러리의 정기적인 업그레이드를 계획하는 것도 중요합니다. 업그레이드는 새로운 언어 기능, 향상된 속도, 낮은 메모리 사용량, 그리고 보안 업데이트를 제공합니다. 업그레이드를 더 자주 수행할수록, 그 과정이 덜 고통스러워집니다.
1.2 합리적인 기본 설정
PHP는 php.ini.development 및 php.ini.production 파일을 통해 기본적으로 괜찮은 기본 설정을 제공하지만, 더 나아질 수 있습니다. 예를 들어, 날짜/시간대를 설정하지 않습니다. 이는 배포 관점에서 의미가 있지만, 설정되지 않으면 날짜/시간 관련 함수를 호출할 때마다 E_WARNING 오류가 발생합니다. 아래는 추천 설정입니다:
- date.timezone - 지원되는 시간대 목록에서 선택하세요.
- session.savepath - 세션을 파일로 사용하고 다른 저장 처리기를 사용하지 않는 경우, 이것을 /tmp 외부로 설정하세요. /tmp를 그대로 두면 공유 호스팅 환경에서 위험할 수 있습니다. /tmp_는 일반적으로 권한이 넓게 열려 있기 때문에, sticky-bit가 설정되어 있어도 이 디렉터리의 내용을 나열할 수 있는 사람은 모든 활성 세션 ID를 알 수 있습니다.
- session.cookie_secure - PHP 코드를 HTTPS로 제공하는 경우, 이것을 활성화하세요.
- session.cookie_httponly - PHP 세션 쿠키가 JavaScript를 통해 접근되지 않도록 설정하세요.
- 더 많은 것... iniscan 같은 도구를 사용하여 구성의 일반적인 취약점을 테스트하세요.
1.3 확장
사용하지 않을 확장(예: 데이터베이스 드라이버)은 비활성화하거나 활성화하지 않는 것이 좋습니다. 활성화된 확장을 확인하려면, phpinfo() 명령을 실행하거나 명령줄에서 실행하세요.
$ php -i
정보는 동일하지만, phpinfo()에는 HTML 형식이 추가됩니다. CLI 버전은 grep으로 특정 정보를 찾기 쉽습니다. 예:
$ php -i | grep error_log
이 방법의 주의점: 웹에 노출된 버전과 CLI 버전의 PHP 설정이 다를 수 있습니다.
2 Composer 사용
현대 PHP를 작성하는 최선의 관행 중 하나는 더 적게 작성하는 것입니다. 프로그래밍을 잘하기 위한 가장 좋은 방법은 실제로 해보는 것이지만, 라우팅, 기본 입력 유효성 검사 라이브러리, 단위 변환, 데이터베이스 추상화 레이어 등 이미 PHP 영역에서 해결된 많은 문제가 있습니다. Packagist에 가서 둘러보세요. 해결하려는 문제의 상당 부분이 이미 작성되고 테스트된 것을 발견할 수 있습니다.
모든 코드를 스스로 작성하는 유혹이 있을 수 있지만(학습 경험으로 자신의 프레임워크나 라이브러리를 작성하는 데 문제가 없음), Not Invented Here라는 감정을 억제하고 시간을 절약하세요. PIE 원칙을 따르세요 - Proudly Invented Elsewhere. 또한, 자신의 것을 작성하기로 선택했다면, 기존 제품과 크게 다르거나 더 나은 기능을 하지 않는 한 출시하지 마세요.
Composer는 PHP의 패키지 관리자로, Python의 pip, Ruby의 gem, Node의 npm과 유사합니다. JSON 파일을 정의하여 코드의 종속성을 나열하면, 필요한 코드 번들을 다운로드하고 설치하여 요구 사항을 해결합니다.
2.1 Composer 설치
이것이 로컬 프로젝트라고 가정하고, 현재 프로젝트 전용 Composer 인스턴스를 설치하세요. 프로젝트 디렉터리로 이동하여 실행하세요:
$ curl -sS https://getcomposer.org/installer | php
이 명령처럼 다운로드를 스크립트 해석기(sh, ruby, php 등)로 직접 전달하는 것은 보안 위험입니다. 설치 코드를 읽고 실행하기 전에 안심하세요.
편의를 위해 (composer install을 php composer.phar install보다 입력하기를 선호한다면), 전역으로 단일 Composer 복사본을 설치하려면 이 명령을 사용하세요:
$ mv composer.phar /usr/local/bin/composer
$ chmod +x composer
파일 권한에 따라 sudo를 사용해야 할 수 있습니다.
2.2 Composer 사용
Composer는 두 가지 주요 종속성 카테고리를 관리합니다: "require"와 "require-dev". "require"로 나열된 종속성은 모든 곳에 설치되지만, "require-dev" 종속성은 특별히 요청될 때만 설치됩니다. 보통 이것들은 활발한 개발 중에 사용되는 도구로, PHP_CodeSniffer와 같은 것입니다. 아래는 Guzzle, 인기 있는 HTTP 라이브러리를 설치하는 예입니다.
$ php composer.phar require guzzle/guzzle
개발 목적으로만 도구를 설치하려면, --dev 플래그를 추가하세요:
$ php composer.phar require --dev 'sebastian/phpcpd'
이것은 PHP Copy-Paste Detector를 개발 전용 종속성으로 설치합니다.
2.3 Install vs update
처음 composer install을 실행하면, composer.json 파일에 기반하여 필요한 라이브러리와 그 종속성을 설치합니다. 완료되면, composer는 composer.lock 파일을 생성합니다. 이 파일은 composer가 찾은 종속성과 그 정확한 버전, 해시를 포함합니다. 이후 composer install을 실행할 때, 이 잠금 파일을 확인하고 해당 정확한 버전을 설치합니다.
composer update은 다릅니다. composer.lock 파일(존재하는 경우)을 무시하고, _composer.json_의 제약 조건을 만족하는 가장 최신 버전의 각 종속성을 찾습니다. 완료되면 새 composer.lock 파일을 작성합니다.
2.4 Autoloading
composer install과 composer update은 autoloader를 생성하여, 방금 설치한 라이브러리를 사용하기 위한 필요한 파일의 위치를 PHP에 알려줍니다. 사용하려면, 이 줄을 추가하세요(보통 모든 요청에서 실행되는 부트스트랩 파일에):
require 'vendor/autoload.php';
3 좋은 디자인 원칙 따르기
3.1 SOLID
SOLID는 좋은 객체 지향 소프트웨어 디자인의 다섯 가지 핵심 원칙을 상기시키는 기억 장치입니다.
3.1.1 S - 단일 책임 원칙
이 원칙은 클래스가 하나의 책임만 가져야 하며, 다른 말로 하면 변경될 단 하나의 이유만 가져야 한다고 말합니다. 이는 작은 도구를 많이 사용하고 한 가지를 잘 수행하는 Unix 철학과 잘 맞습니다. 하나의 일만 하는 클래스는 테스트하고 디버그하기 훨씬 쉽고, 놀라움을 주지 않습니다. Validator 클래스의 메서드 호출이 DB 레코드를 업데이트하는 것을 원하지 않습니다. 아래는 ActiveRecord pattern에 기반한 애플리케이션에서 흔히 볼 수 있는 SRP 위반 예입니다.
class Person extends Model
{
public $name;
public $birthDate;
protected $preferences;
public function getPreferences() {}
public function save() {}
}
이것은 기본적인 entity 모델입니다. 하지만 이 중 하나의 것이 여기에 속하지 않습니다. 엔티티 모델의 유일한 책임은 그것이 나타내는 엔티티와 관련된 행동일 뿐, 자신을 영속화하는 책임은 가져서는 안 됩니다.
class Person extends Model
{
public $name;
public $birthDate;
protected $preferences;
public function getPreferences() {}
}
class DataStore
{
public function save(Model $model) {}
}
이것이 더 낫습니다. Person 모델은 하나의 일만 하고, 저장 행동은 영속성 객체로 이동되었습니다. 또한 Model만 타입 힌팅한 점에 주목하세요. L과 D 부분에서 다시 다룰 것입니다.
3.1.2 O - 개방 폐쇄 원칙
이 원칙을 요약하는 훌륭한 테스트가 있습니다: 구현할 기능, 아마도 최근에 작업한 또는 작업 중인 기능을 생각하세요. 기존 코드베이스에서 이 기능을 오직 새 클래스를 추가하는 것만으로, 기존 클래스를 변경하지 않고 구현할 수 있나요? 구성과 배선 코드는 약간 예외지만, 대부분의 시스템에서 이것은 놀라울 정도로 어렵습니다. 다형성 디스패치를 많이 의존해야 하며, 대부분의 코드베이스는 그렇게 설정되어 있지 않습니다. 관심이 있으시면, polymorphism and writing code without Ifs에 대한 좋은 Google 토크가 YouTube에 있습니다. 보너스로, 이 토크는 Miško Hevery, AngularJs의 제작자로 알려진 사람이 합니다.
3.1.3 L - Liskov 대체 원칙
이 원칙은 Barbara Liskov의 이름을 따서 명명되었으며, 아래와 같습니다:
"프로그램의 객체는 하위 타입의 인스턴스로 대체할 수 있어야 하며, 프로그램의 정확성을 변경하지 않아야 합니다."
이것은 잘 들리지만, 예를 통해 더 명확히 설명됩니다.
abstract class Shape
{
public function getHeight();
public function setHeight($height);
public function getLength();
public function setLength($length);
}
이것은 기본적인 네 변 모양을 나타냅니다. 특별한 것은 없습니다.
class Square extends Shape
{
protected $size;
public function getHeight() {
return $this->size;
}
public function setHeight($height) {
$this->size = $height;
}
public function getLength() {
return $this->size;
}
public function setLength($length) {
$this->size = $length;
}
}
첫 번째 모양인 Square입니다. 간단한 모양입니다. 생성자가 차원을 설정한다고 가정할 수 있지만, 이 구현에서 길이와 높이가 항상 같다는 것을 볼 수 있습니다. Square는 그저 그렇습니다.
class Rectangle extends Shape
{
protected $height;
protected $length;
public function getHeight() {
return $this->height;
}
public function setHeight($height) {
$this->height = $height;
}
public function getLength() {
return $this->length;
}
public function setLength($length) {
$this->length = $length;
}
}
여기에는 다른 모양이 있습니다. 여전히 같은 메서드 서명이지만, 네 변 모양이지만, 서로 대체하려고 시작하면 어떨까요? 이제 Shape의 높이를 변경하면, 모양의 길이가 일치한다고 가정할 수 없게 됩니다. Square 모양을 제공할 때 사용자와 맺은 계약을 위반했습니다.
이것은 LSP 위반의 교과서적인 예이며, 타입 시스템을 최적으로 사용하기 위해 이 유형의 원칙이 필요합니다. duck typing조차도 기본 행동이 다르다는 것을 알려주지 않으며, 그것이 깨지는 것을 보지 않고는 알 수 없기 때문에, 처음부터 다르지 않도록 하는 것이 가장 좋습니다.
3.1.3 I - 인터페이스 분리 원칙
이 원칙은 하나의 큰 인터페이스보다 많은 작은, 세밀한 인터페이스를 선호합니다. 인터페이스는 "이 클래스 중 하나"가 아닌 행동에 기반해야 합니다. PHP에 포함된 인터페이스를 생각하세요. Traversable, Countable, Serializable 등입니다. 그것들은 객체가 가진 능력을 광고할 뿐, 상속된 것이 아닙니다. 그래서 인터페이스를 작게 유지하세요. 30개의 메서드가 있는 인터페이스를 원하지 않습니다. 3개가 더 좋은 목표입니다.
3.1.4 D - 의존성 역전 원칙
이것은 Dependency Injection에 대해 다른 곳에서 들어보셨을 수 있지만, 의존성 역전과 의존성 주입은 완전히 같은 것이 아닙니다. 의존성 역전은 시스템의 세부 사항이 아닌 추상화에 의존해야 한다고 말합니다. 일상적으로 이것이 무엇을 의미하나요?
코드 전체에 mysqli_query()를 직접 사용하지 말고, DataStore->query() 같은 것을 사용하세요.
이 원칙의 핵심은 추상화에 관한 것입니다. "데이터베이스 어댑터 사용"이라고 말하는 것처럼, mysqli_query 같은 직접 호출에 의존하지 마세요. mysqli_query를 절반의 클래스에서 직접 사용하면, 모든 것을 데이터베이스에 직접 연결합니다. MySQL에 대해 긍정적이거나 부정적인 것이 아니지만, 이 유형의 낮은 수준 세부 사항은 한 곳에 숨겨져야 하며, 그 기능은 일반적인 래퍼를 통해 노출되어야 합니다.
이것이 다소 진부한 예일 수 있지만, 제품을 프로덕션에 배포한 후 데이터베이스 엔진을 완전히 변경하는 경우는 매우 드물기 때문에 선택했습니다. 또한, 고정된 데이터베이스를 사용하더라도, 그 추상 래퍼 객체는 버그 수정, 행동 변경, 또는 원하는 기능을 구현할 수 있게 합니다. 또한, 낮은 수준 호출에서는 불가능한 단위 테스트를 가능하게 합니다.
4 객체 캘리세니즘
이 원칙에 대한 완전한 다이빙은 아니지만, 처음 두 가지는 쉽게 기억할 수 있고, 좋은 가치를 제공하며, 거의 모든 코드베이스에 즉시 적용할 수 있습니다.
4.1 메서드당 들여쓰기 레벨 하나만
이것은 메서드를 더 작은 청크로 분해하여, 더 명확하고 자문서화된 코드를 남기는 데 도움이 됩니다. 들여쓰기 레벨이 많을수록, 메서드가 더 많은 일을 하고, 작업 중에 추적해야 할 상태가 많아집니다.
바로 사람들은 이 점에 반대할 수 있지만, 이것은 가이드라인/휴리스틱일 뿐, 엄격한 규칙이 아닙니다. PHP_CodeSniffer 규칙을 이로 강제할 생각은 없습니다(비록 사람들이 했지만).
빠른 샘플을 실행해 보겠습니다:
public function transformToCsv($data)
{
$csvLines = array();
$csvLines[] = implode(',', array_keys($data[0]));
foreach ($data as $row) {
if (!$row) {
continue;
}
$csvLines[] = implode(',', $row);
}
return $csvLines;
}
이 코드는 나쁘지 않지만(기술적으로 올바르고, 테스트 가능 등), 더 명확하게 만들 수 있습니다. 여기서 중첩 레벨을 줄이기 위해 어떻게 할까요?
먼저 foreach 루프의 내용을 간단히 해야 합니다(또는 완전히 제거).
if (!$row) {
continue;
}
이 부분은 쉽습니다. 비어 있는 행을 무시하는 것뿐입니다. 루프에 도달하기 전에 내장 PHP 함수를 사용하여 이 과정을 단축할 수 있습니다.
$data = array_filter($data);
foreach ($data as $row) {
$csvLines[] = implode(',', $row);
}
이제 단일 중첩 레벨이 있습니다. 하지만 이것을 보면, 배열의 각 항목에 함수를 적용하는 것뿐입니다. foreach 루프조차 필요하지 않습니다.
$data = array_filter($data);
$csvLines = array_map(function($row) {
return implode(',', $row);
}, $data);
이제 중첩이 전혀 없고, 코드는 더 빠를 수 있습니다. 루핑을 네이티브 C 함수로 하기 때문입니다. 쉼표를 implode에 전달하기 위해 약간의 트릭을 사용해야 하지만, 이전 단계에서 멈추는 것이 더 이해하기 쉽다고 주장할 수 있습니다.
4.2 else 사용 피하기
이것은 두 가지 주요 아이디어를 다룹니다. 첫 번째는 메서드에서 여러 반환 문입니다. 메서드의 결과를 결정할 충분한 정보가 있으면, 그 결정 후 반환하세요. 두 번째는 Guard Clauses로 알려진 아이디어입니다. 이것은 기본적으로 메서드 상단 근처에서 검증 검사와 조기 반환을 결합합니다. 보여드리겠습니다.
public function addThreeInts($first, $second, $third) {
if (is_int($first)) {
if (is_int($second)) {
if (is_int($third)) {
$sum = $first + $second + $third;
} else {
return null;
}
} else {
return null;
}
} else {
return null;
}
return $sum;
}
이것은 간단합니다. 3개의 정수를 더하고 결과를 반환하거나, 매개변수가 정수가 아니면 null을 반환합니다. AND 연산자로 모든 검사를 한 줄에 결합할 수 있지만, 중첩 if/else 구조가 코드를 따라가기 더 어렵다는 것을 볼 수 있습니다. 이제 이 예를 보세요.
public function addThreeInts($first, $second, $third) {
if (!is_int($first)) {
return null;
}
if (!is_int($second)) {
return null;
}
if (!is_int($third)) {
return null;
}
return $first + $second + $third;
}
이 예는 훨씬 따라가기 쉽습니다. 여기서 guard clauses를 사용하여 매개변수에 대한 초기 주장을 검증하고, 통과하지 않으면 메서드를 즉시 종료합니다. 또한 합계를 추적하는 중간 변수를 더 이상 가지지 않습니다. 이미 행복한 경로에 있음을 확인했으므로, 할 일을 하세요. 모든 검사를 하나의 if로 할 수 있지만, 원칙은 명확합니다.
5 단위 테스트
단위 테스트는 코드의 행동을 확인하는 작은 테스트를 작성하는 관행입니다. 거의 항상 코드와 같은 언어(이 경우 PHP)로 작성되며, 언제든지 실행할 수 있을 만큼 빠릅니다. 코드 개선을 위한 매우 유용한 도구입니다. 코드가 예상대로 작동하는지 확인하는 명백한 이점 외에도, 단위 테스트는 디자인 피드백을 제공할 수 있습니다. 테스트하기 어려운 코드 조각은 종종 디자인 문제를 드러냅니다. 또한 회귀에 대한 안전망을 제공하여, 더 자주 리팩터링하고 코드를 더 깨끗한 디자인으로 진화시킬 수 있습니다.
5.1 도구
PHP에는 여러 단위 테스트 도구가 있지만, 단연코 가장 일반적인 것은 PHPUnit입니다. PHAR 파일을 직접 다운로드하거나, composer로 설치할 수 있습니다. 다른 모든 것에 composer를 사용하므로, 그 방법을 보여드리겠습니다. 또한, PHPUnit는 프로덕션에 배포되지 않을 가능성이 크므로, dev 종속성으로 설치하세요:
composer require --dev phpunit/phpunit
5.2 테스트는 사양
코드의 단위 테스트에서 가장 중요한 역할은 코드가 해야 할 일을 실행 가능한 사양으로 제공하는 것입니다. 테스트 코드가 잘못되었거나 코드에 버그가 있더라도, 시스템이 해야 할 일에 대한 지식은 귀중합니다.
5.3 먼저 테스트 작성
코드 전에 테스트를 작성한 경우와 코드 후에 작성한 경우를 본 적이 있다면, 그 차이가 뚜렷합니다. "후" 테스트는 클래스의 구현 세부 사항에 훨씬 더 관심이 있고, 좋은 라인 커버리지를 확보하는 데 초점을 맞춥니다. 반면, "전" 테스트는 원하는 외부 행동을 확인하는 데 더 관심이 있습니다. 이것이 단위 테스트에서 실제로 원하는 것입니다. 클래스 내부가 변경되면 구현 중심 테스트는 리팩터링을 더 어렵게 만듭니다.
5.4 좋은 단위 테스트의 특징
좋은 단위 테스트는 다음과 같은 특징을 공유합니다:
- 빠름 - 밀리초 단위로 실행되어야 합니다.
- 네트워크 접근 없음 - 무선 끄거나 unplug 해도 모든 테스트가 통과해야 합니다.
- 파일 시스템 접근 제한 - 속도와 다른 환경에 배포할 때의 유연성을 높입니다.
- 데이터베이스 접근 없음 - 비용이 큰 설정과 해체 활동을 피합니다.
- 한 번에 하나의 것만 테스트 - 단위 테스트는 실패할 단 하나의 이유만 가져야 합니다.
- 잘 명명됨 - 5.2를 참조하세요.
- 대부분 가짜 객체 - 단위 테스트에서 "실제" 객체는 테스트 중인 객체와 간단한 값 객체일 뿐입니다. 나머지는 test double 형태여야 합니다.
이 중 일부를 위반할 이유가 있지만, 일반적인 지침으로 유용합니다.
5.5 테스트가 고통스러울 때
단위 테스트는 나쁜 디자인의 고통을 앞당겨 느끼게 합니다 - Michael Feathers
단위 테스트를 작성할 때, 클래스를 실제로 사용해야 합니다. 테스트를 끝에 작성하거나, 더 나쁘게는 QA나 누구에게 코드 던지면, 클래스의 실제 행동에 대한 피드백을 얻지 못합니다. 테스트를 작성 중이고 클래스가 사용하기 어려우면, 작성 중에 알 수 있습니다. 이것은 거의 가장 저렴한 수정 시기입니다.
클래스가 테스트하기 어렵다면, 디자인 결함입니다. 다른 결함이 다르게 나타나지만, mocking을 많이 해야 한다면 클래스가 너무 많은 종속성을 가지거나 메서드가 너무 많은 일을 할 수 있습니다. 각 테스트에 많은 설정을 해야 한다면, 메서드가 너무 많은 일을 할 가능성이 큽니다. 행동을 실행하기 위해 복잡한 테스트 시나리오를 작성해야 한다면, 클래스의 메서드가 너무 많은 일을 할 수 있습니다. 사적인 메서드와 상태를 테스트하기 위해 파고들어야 한다면, 다른 클래스가 나오려고 할 수 있습니다. 단위 테스트는 "iceberg classes"를 드러내는 데 매우 좋습니다. 클래스의 80%가 보호되거나 사적인 코드에 숨겨져 있습니다. 이전에 가능한 한 많이 보호하는 것을 좋아했지만, 이제 각 클래스가 너무 많은 책임을 지고 있다는 것을 깨달았고, 진짜 해결책은 클래스를 더 작은 조각으로 나누는 것입니다.
Brian Fenton 작성 - Brian Fenton은 중서부와 Bay Area에서 8년 동안 PHP 개발자였으며, 현재 Thismoment에 있습니다. 그는 코드 장인 정신과 디자인 원칙에 초점을 맞춥니다. 블로그 www.brianfenton.us, Twitter @brianfenton. 아이를 키우는 데 바쁘지 않을 때는 음식, 맥주, 게임, 학습을 즐깁니다.
Learn/security
보안
개요
웹 애플리케이션에서 보안은 매우 중요합니다. 애플리케이션이 안전하고 사용자의 데이터가 안전하게 보호되도록 해야 합니다. Flight는 웹 애플리케이션을 보호하는 데 도움이 되는 여러 기능을 제공합니다.
공식 스켈레톤에는 전용 SECURITY.md 와 보안 헤더 미들웨어도 포함되어 있어, AI 코딩 도구 (및 사람)가 일반적인 코딩 스타일을 다루는 AGENTS.md와 분리된 곳에서 시크릿, 헤더, XSS/SQL 규칙을 한곳에 모아 관리할 수 있습니다.
이해하기
웹 애플리케이션을 구축할 때 알아야 할 일반적인 보안 위협이 여러 가지 있습니다. 가장 흔한 위협은 다음과 같습니다:
- 크로스 사이트 요청 위조(CSRF)
- 크로스 사이트 스크립팅(XSS)
- SQL 인젝션
- 교차 출처 리소스 공유(CORS)
템플릿은 기본적으로 출력을 이스케이프하여 XSS를 방지합니다(Twig와 Latte는 이렇게 동작하므로 이 이점을 활용하세요). 세션은 아래 설명된 대로 사용자 세션에 CSRF 토큰을 저장하여 CSRF를 방지하는 데 도움이 될 수 있습니다. PDO에서 준비된 문(prepared statements)을 사용하거나 SimplePdo의 헬퍼를 사용하면 SQL 인젝션을 예방할 수 있습니다. CORS는 Flight::start()가 호출되기 전에 간단한 훅으로 처리할 수 있습니다.
이 모든 방법이 함께 작동하여 웹 애플리케이션을 안전하게 유지합니다. 보안 모범 사례를 배우고 이해하는 것이 항상 최우선이어야 합니다. 페이지가 로드되기 위해 단순히 "CSP를 비활성화"하거나 헤더를 약화시키라고 AI 어시스턴트에게 요청해서는 안 됩니다. 그로 인한 트레이드오프를 이해하지 않고서는 말이죠.
기본 사용법
헤더
HTTP 헤더는 웹 애플리케이션을 보호하는 가장 쉬운 방법 중 하나입니다. 헤더를 사용하여 클릭재킹, XSS 및 기타 공격을 방지할 수 있습니다. 애플리케이션에 이러한 헤더를 추가하는 방법은 여러 가지가 있습니다.
헤더의 보안을 확인할 수 있는 훌륭한 웹사이트 두 곳은 securityheaders.com과 observatory.mozilla.org입니다. 아래 코드를 설정한 후 이 두 웹사이트에서 헤더가 제대로 작동하는지 쉽게 확인할 수 있습니다.
스켈레톤에는 App\Middleware\SecurityHeadersMiddleware가 포함되어 있습니다(요청별 nonce를 사용한 CSP, 프레임 옵션, HSTS 등). 헤더를 비활성화하기보다는 이를 명시적으로 확장하는 것을 선호하세요.
직접 추가하기
Flight\Response 객체의 header 메서드를 사용하여 이러한 헤더를 수동으로 추가할 수 있습니다.
// X-Frame-Options 헤더를 설정하여 클릭재킹 방지
Flight::response()->header('X-Frame-Options', 'SAMEORIGIN');
// Content-Security-Policy 헤더를 설정하여 XSS 방지
// 참고: 이 헤더는 매우 복잡해질 수 있으므로
// 애플리케이션에 맞는 예제를 인터넷에서 참고해야 합니다.
Flight::response()->header("Content-Security-Policy", "default-src 'self'");
// X-XSS-Protection 헤더를 설정하여 XSS 방지
Flight::response()->header('X-XSS-Protection', '1; mode=block');
// X-Content-Type-Options 헤더를 설정하여 MIME 스니핑 방지
Flight::response()->header('X-Content-Type-Options', 'nosniff');
// Referrer-Policy 헤더를 설정하여 전송되는 리퍼러 정보의 양을 제어
Flight::response()->header('Referrer-Policy', 'no-referrer-when-downgrade');
// Strict-Transport-Security 헤더를 설정하여 HTTPS 강제
Flight::response()->header('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload');
// Permissions-Policy 헤더를 설정하여 사용할 수 있는 기능과 API 제어
Flight::response()->header('Permissions-Policy', 'geolocation=()');
이 헤더들은 routes.php 또는 index.php 파일 상단에 추가할 수 있습니다.
필터로 추가하기
다음과 같이 필터/훅으로 추가할 수도 있습니다:
// 필터에서 헤더 추가
Flight::before('start', function() {
Flight::response()->header('X-Frame-Options', 'SAMEORIGIN');
Flight::response()->header("Content-Security-Policy", "default-src 'self'");
Flight::response()->header('X-XSS-Protection', '1; mode=block');
Flight::response()->header('X-Content-Type-Options', 'nosniff');
Flight::response()->header('Referrer-Policy', 'no-referrer-when-downgrade');
Flight::response()->header('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload');
Flight::response()->header('Permissions-Policy', 'geolocation=()');
});
미들웨어로 추가하기
이 헤더를 적용할 라우트를 가장 유연하게 지정할 수 있는 미들웨어 클래스로 추가할 수도 있습니다. 일반적으로 이러한 헤더는 모든 HTML 및 API 응답에 적용되어야 합니다.
스켈레톤 스타일 경로 및 네임스페이스(폴더 대소문자는 App\Middleware와 일치):
// app/Middleware/SecurityHeadersMiddleware.php
namespace App\Middleware;
use flight\Engine;
class SecurityHeadersMiddleware
{
protected Engine $app;
public function __construct(Engine $app)
{
$this->app = $app;
}
public function before(array $params): void
{
$response = $this->app->response();
// 인라인 스크립트가 있을 때 부트스트랩의 CSP nonce를 사용하는 것이 좋습니다. (스켈레톤은 csp_nonce를 설정합니다)
$nonce = $this->app->get('csp_nonce');
$csp = $nonce
? "default-src 'self'; script-src 'self' 'nonce-{$nonce}'; style-src 'self' 'nonce-{$nonce}'"
: "default-src 'self'";
$response->header('X-Frame-Options', 'SAMEORIGIN');
$response->header('Content-Security-Policy', $csp);
$response->header('X-XSS-Protection', '1; mode=block');
$response->header('X-Content-Type-Options', 'nosniff');
$response->header('Referrer-Policy', 'no-referrer-when-downgrade');
$response->header('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload');
$response->header('Permissions-Policy', 'geolocation=()');
}
}
// app/config/routes.php — 빈 문자열 그룹 = 모든 라우트에 대한 전역 미들웨어
use App\Middleware\SecurityHeadersMiddleware;
use flight\net\Router;
$router->group('', function (Router $router) {
$router->get('/users', [ \App\Controller\UserController::class, 'getUsers' ]);
// 더 많은 라우트
}, [SecurityHeadersMiddleware::class]);
이전 프로젝트에서는 여전히 app/middlewares 및 app\middlewares를 사용할 수 있습니다. 폴더가 일치하면 작동합니다. 새 스켈레톤 앱은 app/Middleware/ 및 App\Middleware 를 사용합니다. 자동 로딩을 참조하세요.
크로스 사이트 요청 위조(CSRF)
크로스 사이트 요청 위조(CSRF)는 악성 웹사이트가 사용자의 브라우저를 이용해 귀하의 웹사이트에 요청을 보내게 만드는 공격 유형입니다. 이를 통해 사용자가 인지하지 못한 상태에서 귀하의 웹사이트에서 작업을 수행할 수 있습니다. Flight에는 내장된 CSRF 보호 메커니즘이 없지만 미들웨어를 사용하여 쉽게 직접 구현할 수 있습니다.
설정
먼저 CSRF 토큰을 생성하여 사용자 세션에 저장해야 합니다. 그런 다음 이 토큰을 양식에서 사용하고 양식이 제출될 때 확인할 수 있습니다. 세션 관리를 위해 flightphp/session 플러그인을 사용하겠습니다.
// CSRF 토큰을 생성하여 사용자 세션에 저장
// (세션 객체를 생성하여 Flight에 연결했다고 가정)
// 자세한 내용은 세션 문서를 참조하세요.
Flight::register('session', flight\Session::class);
// 세션당 하나의 토큰만 생성하면 됩니다. (즉, 동일 사용자의 여러 탭과 요청에서도 작동)
if(Flight::session()->get('csrf_token') === null) {
Flight::session()->set('csrf_token', bin2hex(random_bytes(32)) );
}
기본 PHP Flight 템플릿 사용
<!-- 양식에서 CSRF 토큰 사용 -->
<form method="post">
<input type="hidden" name="csrf_token" value="<?= Flight::session()->get('csrf_token') ?>">
<!-- 다른 양식 필드 -->
</form>
Twig 사용 (스켈레톤 기본값)
Twig 함수를 등록하거나 토큰을 모든 양식 뷰에 전달하세요. 전역 변수와 양식 필드를 사용한 최소 예제입니다:
// Twig를 구성할 때 (예: services.php)
$twig->addGlobal('csrf_token', $app->session()->get('csrf_token'));
{# app/views/form.twig #}
<form method="post">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
{# 다른 필드 #}
</form>
Latte 사용
Latte 템플릿에서 CSRF 토큰을 출력하는 사용자 정의 함수를 설정할 수도 있습니다.
Flight::map('render', function(string $template, array $data, ?string $block): void {
$latte = new Latte\Engine;
// 기타 구성...
// CSRF 토큰을 출력하는 사용자 정의 함수 설정
$latte->addFunction('csrf', function() {
$csrfToken = Flight::session()->get('csrf_token');
return new \Latte\Runtime\Html('<input type="hidden" name="csrf_token" value="' . $csrfToken . '">');
});
$latte->render($finalPath, $data, $block);
});
이제 Latte 템플릿에서 csrf() 함수를 사용하여 CSRF 토큰을 출력할 수 있습니다.
<form method="post">
{csrf()}
<!-- 다른 양식 필드 -->
</form>
CSRF 토큰 확인
CSRF 토큰은 여러 방법으로 확인할 수 있습니다.
미들웨어
// app/Middleware/CsrfMiddleware.php
namespace App\Middleware;
use flight\Engine;
class CsrfMiddleware
{
protected Engine $app;
public function __construct(Engine $app)
{
$this->app = $app;
}
public function before(array $params): void
{
if($this->app->request()->method == 'POST') {
$token = $this->app->request()->data->csrf_token;
if($token !== $this->app->session()->get('csrf_token')) {
$this->app->halt(403, 'Invalid CSRF token');
}
}
}
}
// routes.php
use App\Middleware\CsrfMiddleware;
$router->group('', function ($router) {
$router->get('/users', [ \App\Controller\UserController::class, 'getUsers' ]);
// 더 많은 라우트
}, [CsrfMiddleware::class]);
이벤트 필터
// 이 미들웨어는 요청이 POST인지 확인하고, POST라면 CSRF 토큰이 유효한지 확인합니다.
Flight::before('start', function() {
if(Flight::request()->method == 'POST') {
// 양식 값에서 csrf 토큰 가져오기
$token = Flight::request()->data->csrf_token;
if($token !== Flight::session()->get('csrf_token')) {
Flight::halt(403, 'Invalid CSRF token');
// 또는 JSON 응답의 경우
Flight::jsonHalt(['error' => 'Invalid CSRF token'], 403);
}
}
});
크로스 사이트 스크립팅(XSS)
크로스 사이트 스크립팅(XSS)은 악성 양식 입력이 귀하의 웹사이트에 코드를 주입할 수 있는 공격 유형입니다. 이러한 기회는 대부분 최종 사용자가 작성하는 양식 값에서 발생합니다. 사용자의 출력을 절대 신뢰하지 마세요! 항상 모든 사용자가 최고의 해커라고 가정하세요. 그들은 악성 JavaScript나 HTML을 귀하의 페이지에 주입할 수 있습니다. 이 코드는 사용자 정보를 탈취하거나 귀하의 웹사이트에서 작업을 수행하는 데 사용될 수 있습니다. Flight의 뷰 클래스 또는 Twig나 Latte와 같은 템플릿 엔진을 사용하면 XSS 공격을 방지하기 위해 출력을 쉽게 이스케이프할 수 있습니다.
// 사용자가 이름으로 이 값을 사용하려 한다고 가정
$name = '<script>alert("XSS")</script>';
// 이것은 출력을 이스케이프합니다.
Flight::view()->set('name', $name);
// 출력 결과: <script>alert("XSS")</script>
// Twig(스켈레톤 기본값)와 Latte는 기본적으로 자동 이스케이프됩니다. — 원시 PHP echo보다 선호하세요.
Flight::render('template', ['name' => $name]);
// Twig: {{ name }} → 이스케이프됨
// 콘텐츠가 완전히 신뢰할 수 있는 경우가 아니라면 |raw / 이스케이프되지 않은 출력은 피하세요.
SQL 인젝션
SQL 인젝션은 악성 사용자가 데이터베이스에 SQL 코드를 주입할 수 있는 공격 유형입니다. 이는 데이터베이스에서 정보를 탈취하거나 데이터베이스에서 작업을 수행하는 데 사용될 수 있습니다. 다시 말하지만 사용자의 입력을 절대 신뢰하지 마세요! 항상 그들이 유혈을 목적으로 한다고 가정하세요. 준비된 문(prepared statements)을 사용하세요. — SimplePdo 헬퍼는 이를 기본 경로로 만듭니다.
// Flight::db()가 SimplePdo로 등록되어 있다고 가정 (또는 컨트롤러에 SimplePdo 주입)
$statement = Flight::db()->prepare('SELECT * FROM users WHERE username = :username');
$statement->execute([':username' => $username]);
$users = $statement->fetchAll();
// SimplePdo (권장) — 바인딩 매개변수를 사용한 원라이너
$users = Flight::db()->fetchAll('SELECT * FROM users WHERE username = :username', [ 'username' => $username ]);
// ? 플레이스홀더를 사용한 동일한 방식
$users = Flight::db()->fetchAll('SELECT * FROM users WHERE username = ?', [ $username ]);
스켈레톤 스타일 컨트롤러에서는 Flight::db()보다는 SimplePdo를 생성자 주입하는 것이 테스트와 AI 생성 코드의 일관성을 유지하는 데 좋습니다 (DIC).
안전하지 않은 예제
아래는 왜 SQL 준비된 문을 사용하여 다음과 같은 무해해 보이는 예제로부터 보호하는지 보여줍니다:
// 최종 사용자가 웹 양식을 작성합니다.
// 해커는 양식 값에 다음과 같은 내용을 입력합니다:
$username = "' OR 1=1; -- ";
$sql = "SELECT * FROM users WHERE username = '$username' LIMIT 5";
$users = Flight::db()->fetchAll($sql);
// 쿼리가 구성된 후 다음과 같이 보입니다.
// SELECT * FROM users WHERE username = '' OR 1=1; -- LIMIT 5
// 이상해 보이지만 작동하는 유효한 쿼리입니다. 사실
// 이것은 모든 사용자를 반환하는 매우 흔한 SQL 인젝션 공격입니다.
var_dump($users); // 이러면 데이터베이스의 모든 사용자가 덤프됩니다. 단일 사용자 이름만이 아니라.
시크릿과 구성
- 시크릿은 커밋된
config.php샘플이 아닌.env(또는 실제 환경)에 두세요. - 스켈레톤 규칙:
config.php에는 리터럴 기본값을 두고, 부트스트랩에서 env를 병합합니다. 컨트롤러 내부에서$_ENV를 읽지 말고 구성(config)을 주입하세요. 구성을 참조하세요. - API 키, DB 비밀번호, 세션 암호화 키를 커밋하지 마세요. AI 도구가 안전하지 않은 단축 경로를 만들지 않도록
SECURITY.md를 가리키세요.
JSONP 콜백 검증
Flight의 Flight::jsonp() 메서드를 사용하는 경우, Flight는 JSONP 콜백 매개변수 이름을 엄격한 허용 목록 정규식(/^[A-Za-z_$][\w$.]{0,127}$/)으로 검증합니다. 이 패턴과 일치하지 않는 콜백 이름은 Flight가 예외를 발생시켜 악성 콜백 값을 통한 임의 JavaScript 주입을 방지합니다.
이 검증은 내장되어 있으며 추가 구성이 필요 없지만, JSONP 엔드포인트에서 예기치 않은 오류를 디버깅할 때 알아두면 유용합니다.
CORS
교차 출처 리소스 공유(CORS)는 웹 페이지의 많은 리소스(예: 글꼴, JavaScript 등)가 리소스가 원래 제공된 도메인 외부의 다른 도메인에서 요청될 수 있도록 하는 메커니즘입니다. Flight에는 내장된 기능이 없지만 Flight::start() 메서드가 호출되기 전에 실행되는 훅으로 쉽게 처리할 수 있습니다.
// app/Utils/CorsUtil.php (스켈레톤: PascalCase Utils 폴더 → App\Utils)
namespace App\Utils;
use flight\Engine;
class CorsUtil
{
protected Engine $app;
public function __construct(Engine $app)
{
$this->app = $app;
}
public function set(array $params = []): void
{
$request = $this->app->request();
$response = $this->app->response();
if ($request->getVar('HTTP_ORIGIN') !== '') {
$this->allowOrigins();
$response->header('Access-Control-Allow-Credentials', 'true');
$response->header('Access-Control-Max-Age', '86400');
}
if ($request->method === 'OPTIONS') {
if ($request->getVar('HTTP_ACCESS_CONTROL_REQUEST_METHOD') !== '') {
$response->header(
'Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH, OPTIONS, HEAD'
);
}
if ($request->getVar('HTTP_ACCESS_CONTROL_REQUEST_HEADERS') !== '') {
$response->header(
"Access-Control-Allow-Headers",
$request->getVar('HTTP_ACCESS_CONTROL_REQUEST_HEADERS')
);
}
$response->status(200);
$response->send();
exit;
}
}
private function allowOrigins(): void
{
// 허용할 호스트를 여기에서 사용자 정의하세요.
$allowed = [
'capacitor://localhost',
'ionic://localhost',
'http://localhost',
'http://localhost:4200',
'http://localhost:8080',
'http://localhost:8100',
];
$request = $this->app->request();
if (in_array($request->getVar('HTTP_ORIGIN'), $allowed, true) === true) {
$response = $this->app->response();
$response->header("Access-Control-Allow-Origin", $request->getVar('HTTP_ORIGIN'));
}
}
}
// 부트스트랩 / 라우트 — start 전에 실행
$app = Flight::app();
$cors = new \App\Utils\CorsUtil($app);
$app->before('start', [ $cors, 'set' ]);
Flight 구성 강화
Flight는 보안에 직접적인 영향을 미치는 몇 가지 엔진 설정을 노출합니다. 이를 올바르게 설정하는 것은 애플리케이션을 강화하는 가장 쉬운 방법 중 하나입니다.
flight.allow_method_override
기본적으로 Flight는 클라이언트가 X-HTTP-Method-Override 헤더 또는 POST 본문의 _method 필드를 사용하여 요청의 HTTP 메서드를 재정의할 수 있도록 허용합니다. 이는 GET/POST만 보낼 수 있는 HTML 양식에 유용하지만, 이를 예상하지 않은 경우 위험할 수 있습니다. 공격자가 일반 양식을 통해 DELETE 또는 PUT 요청을 위조할 수 있습니다.
애플리케이션이 이 동작에 의존하지 않는 경우(예: 모든 HTTP 동사를 보낼 수 있는 최신 클라이언트 또는 JavaScript 프런트엔드가 사용하는 API를 구축하는 경우) 비활성화해야 합니다:
// index.php 또는 부트스트랩 파일에서 Flight::start() 이전에
Flight::set('flight.allow_method_override', false);
기본값은 이전 버전과의 호환성을 위해 true이지만, 오버라이드 기능이 명시적으로 필요하지 않은 모든 애플리케이션에서는 false로 설정하는 것을 강력히 권장합니다.
flight.debug
Flight에는 처리되지 않은 예외가 발생할 때 브라우저에 상세 오류 정보(예외 메시지, 코드, 전체 스택 추적)가 표시되는지 여부를 제어하는 flight.debug 설정이 있습니다. 기본값은 false이며, 이 경우 일반적인 500 Internal Server Error 메시지만 표시됩니다. 내부 세부 정보가 클라이언트에 유출되지 않습니다.
프로덕션 서버에서는 절대 활성화하지 마세요. 로컬 또는 스테이징 환경에서만 사용하세요:
// 로컬 개발에서만 안전 — 프로덕션에서는 절대 금지
Flight::set('flight.debug', true);
flight.debug가 false(기본값)인 경우에도 flight.log_errors를 활성화하여 오류를 캡처할 수 있습니다:
// 클라이언트에 노출하지 않고 서버 측에서 오류 기록
Flight::set('flight.debug', false);
Flight::set('flight.log_errors', true);
권장 프로덕션 구성
// index.php 또는 앱 구성 / 부트스트랩에서 적용
Flight::set('flight.allow_method_override', false);
Flight::set('flight.debug', false);
Flight::set('flight.log_errors', true);
오류 처리
프로덕션에서는 공격자에게 정보를 유출하지 않도록 민감한 오류 세부 정보를 숨기세요. 프로덕션에서는 display_errors를 0으로 설정하고 오류를 표시하는 대신 기록하세요.
// bootstrap.php 또는 index.php에서
// 이것을 app/config/config.php에 추가
$environment = ENVIRONMENT;
if ($environment === 'production') {
ini_set('display_errors', 0); // 오류 표시 비활성화
ini_set('log_errors', 1); // 대신 오류 기록
ini_set('error_log', '/path/to/error.log');
}
// 라우트 또는 컨트롤러에서
// 제어된 오류 응답에는 Flight::halt() 사용
Flight::halt(403, 'Access denied');
입력 정화(Sanitization)
사용자 입력을 절대 신뢰하지 마세요. 처리하기 전에 filter_var를 사용하여 정화함으로써 악성 데이터가 들어오는 것을 방지하세요. 앱 코드에서 원시 $_GET / $_POST보다는 $app->request()(또는 Flight::request())를 통해 입력을 읽는 것을 선호하세요.
// $_POST['input'] 및 $_POST['email']이 포함된 $_POST 요청을 가정
// 문자열 입력 정화
$clean_input = filter_var(Flight::request()->data->input, FILTER_SANITIZE_STRING);
// 이메일 정화
$clean_email = filter_var(Flight::request()->data->email, FILTER_SANITIZE_EMAIL);
비밀번호 해싱
PHP 내장 함수인 password_hash와 password_verify를 사용하여 비밀번호를 안전하게 저장하고 안전하게 검증하세요. 비밀번호는 절대 평문으로 저장해서는 안 되며, 되돌릴 수 있는 방법으로 암호화해서도 안 됩니다. 해싱은 데이터베이스가 침해되더라도 실제 비밀번호가 보호되도록 보장합니다.
$password = Flight::request()->data->password;
// 비밀번호를 저장할 때 해시 (예: 회원가입 중)
$hashed_password = password_hash($password, PASSWORD_DEFAULT);
// 비밀번호 검증 (예: 로그인 중)
if (password_verify($password, $stored_hash)) {
// 비밀번호 일치
}
속도 제한
캐시를 사용하여 요청 속도를 제한함으로써 무차별 대입 공격이나 서비스 거부 공격으로부터 보호하세요.
// flightphp/cache가 설치 및 등록되어 있다고 가정
// 필터에서 flightphp/cache 사용
Flight::before('start', function() {
$cache = Flight::cache();
$ip = Flight::request()->ip;
$key = "rate_limit_{$ip}";
$attempts = (int) $cache->retrieve($key);
if ($attempts >= 10) {
Flight::halt(429, 'Too many requests');
}
$cache->set($key, $attempts + 1, 60); // 60초 후 리셋
});
참고 항목
- 세션 - 사용자 세션을 안전하게 관리하는 방법.
- 템플릿 - Twig/Latte 자동 이스케이프 및 XSS.
- SimplePdo - 준비된 문을 사용하는 데이터베이스 헬퍼.
- PdoWrapper - 더 이상 사용되지 않음; 새 코드에서는 SimplePdo를 사용하세요.
- 미들웨어 - 보안 헤더 추가 과정을 단순화하는 미들웨어 사용 방법.
- 구성 -
.envvs 리터럴 구성, 프로덕션 플래그. - AI 및 개발자 경험 - 에이전트를 위해 보안 정책을
SECURITY.md에 유지. - 응답 - 보안 헤더로 HTTP 응답을 사용자 정의하는 방법.
- 요청 - 사용자 입력 처리 및 정화 방법.
- filter_var - 입력 정화를 위한 PHP 함수.
- password_hash - 안전한 비밀번호 해싱을 위한 PHP 함수.
- password_verify - 해시된 비밀번호 검증을 위한 PHP 함수.
문제 해결
- 위의 "참고 항목" 섹션을 참조하여 Flight Framework 구성 요소와 관련된 문제 해결 정보를 확인하세요.
- CSP가 스크립트를 차단하는 경우 nonce(스켈레톤 패턴)를 추가하거나 특정 출처를 허용 목록에 추가하세요. 계획 없이
script-src *로 설정하지 마세요.
변경 로그
- Docs – 스켈레톤
App\Middleware, Twig CSRF/XSS 참고, SimplePdo, 시크릿/.env, AI 친화적인 프로젝트를 위한SECURITY.md. - v3.18.1 -
flight.allow_method_override,flight.debug, JSONP 콜백 검증을 다루는 Flight 구성 강화 섹션 추가. - v3.1.0 - CORS, 오류 처리, 입력 정화, 비밀번호 해싱, 속도 제한에 대한 섹션 추가.
- v2.0 - XSS 방지를 위한 기본 뷰 이스케이프 추가.
Learn/routing
라우팅
개요
Flight PHP에서 라우팅은 URL 패턴을 콜백 함수나 클래스 메서드에 매핑하여 빠르고 간단한 요청 처리를 가능하게 합니다. 최소한의 오버헤드, 초보자 친화적인 사용법, 외부 의존성 없이 확장 가능하도록 설계되었습니다.
이해하기
라우팅은 HTTP 요청을 애플리케이션 로직에 연결하는 핵심 메커니즘입니다. 라우트를 정의하면 함수, 클래스 메서드 또는 컨트롤러 액션을 통해 서로 다른 URL이 특정 코드를 실행하도록 지정할 수 있습니다. Flight의 라우팅 시스템은 유연하며 기본 패턴, 명명된 매개변수, 정규식, 의존성 주입 및 리소스풀 라우팅과 같은 고급 기능을 지원합니다. 이 접근 방식은 코드를 체계적이고 유지 관리하기 쉽게 유지하면서 초보자에게 빠르고 간단하며 고급 사용자에게 확장 가능합니다.
참고: 라우팅에 대해 더 자세히 알고 싶으신가요? 더 깊이 있는 설명은 "프레임워크가 왜 필요한가?" 페이지를 확인하세요.
기본 사용법
간단한 라우트 정의하기
Flight의 기본 라우팅은 URL 패턴을 콜백 함수 또는 클래스와 메서드 배열과 매칭하여 수행됩니다.
Flight::route('/', function(){
echo 'hello world!';
});
라우트는 정의된 순서대로 매칭됩니다. 요청과 일치하는 첫 번째 라우트가 호출됩니다.
함수를 콜백으로 사용하기
콜백은 호출 가능한 모든 객체가 될 수 있습니다. 따라서 일반 함수를 사용할 수 있습니다:
function hello() {
echo 'hello world!';
}
Flight::route('/', 'hello');
클래스와 메서드를 컨트롤러로 사용하기
클래스의 메서드(정적 또는 비정적)를 사용할 수도 있습니다:
class GreetingController {
public function hello() {
echo 'hello world!';
}
}
Flight::route('/', [ 'GreetingController','hello' ]);
// 또는
Flight::route('/', [ GreetingController::class, 'hello' ]); // 선호되는 방법
// 또는
Flight::route('/', [ 'GreetingController::hello' ]);
// 또는
Flight::route('/', [ 'GreetingController->hello' ]);
또는 먼저 객체를 생성한 다음 메서드를 호출할 수도 있습니다:
use flight\Engine;
// GreetingController.php
class GreetingController
{
protected Engine $app
public function __construct(Engine $app) {
$this->app = $app;
$this->name = 'John Doe';
}
public function hello() {
echo "Hello, {$this->name}!";
}
}
// index.php
$app = Flight::app();
$greeting = new GreetingController($app);
Flight::route('/', [ $greeting, 'hello' ]);
참고: 기본적으로 프레임워크 내에서 컨트롤러가 호출되면 의존성 주입 컨테이너를 통해 지정하지 않는 한
flight\Engine클래스가 항상 주입됩니다.
메서드별 라우팅
기본적으로 라우트 패턴은 모든 요청 메서드와 매칭됩니다. URL 앞에 식별자를 배치하여 특정 메서드에 응답할 수 있습니다.
Flight::route('GET /', function () {
echo 'I received a GET request.';
});
Flight::route('POST /', function () {
echo 'I received a POST request.';
});
// 라우트를 생성하는 데 Flight::get()을 사용할 수 없습니다.
// 이는 변수를 가져오는 메서드이지 라우트를 만드는 메서드가 아닙니다.
Flight::post('/', function() { /* 코드 */ });
Flight::patch('/', function() { /* 코드 */ });
Flight::put('/', function() { /* 코드 */ });
Flight::delete('/', function() { /* 코드 */ });
| 구분자를 사용하여 여러 메서드를 단일 콜백에 매핑할 수도 있습니다:
Flight::route('GET|POST /', function () {
echo 'I received either a GET or a POST request.';
});
HEAD 및 OPTIONS 요청 특별 처리
Flight는 HEAD 및 OPTIONS HTTP 요청에 대한 기본 제공 처리를 제공합니다:
HEAD 요청
- HEAD 요청은
GET요청과 동일하게 처리되지만, Flight는 응답 본문을 클라이언트에 보내기 전에 자동으로 제거합니다. - 즉,
GET라우트를 정의하면 동일한 URL에 대한 HEAD 요청은 HTTP 표준에 따라 헤더만 반환하고 본문은 포함하지 않습니다.
Flight::route('GET /info', function() {
echo 'This is some info!';
});
// /info에 대한 HEAD 요청은 동일한 헤더를 반환하지만 본문은 없습니다.
OPTIONS 요청
OPTIONS 요청은 정의된 모든 라우트에 대해 Flight가 자동으로 처리합니다.
- OPTIONS 요청을 받으면 Flight는
204 No Content상태와 해당 라우트에서 지원하는 모든 HTTP 메서드를 나열하는Allow헤더로 응답합니다. - OPTIONS에 대한 별도 라우트를 정의할 필요가 없습니다.
// 다음과 같이 정의된 라우트의 경우:
Flight::route('GET|POST /users', function() { /* ... */ });
// /users에 대한 OPTIONS 요청은 다음과 같이 응답합니다:
//
// Status: 204 No Content
// Allow: GET, POST, HEAD, OPTIONS
라우터 객체 사용하기
추가로 사용할 수 있는 헬퍼 메서드가 포함된 Router 객체를 가져올 수 있습니다:
$router = Flight::router();
// Flight::route()와 동일하게 모든 메서드를 매핑합니다
$router->map('/', function() {
echo 'hello world!';
});
// GET 요청
$router->get('/users', function() {
echo 'users';
});
$router->post('/users', function() { /* 코드 */});
$router->put('/users/update/@id', function() { /* 코드 */});
$router->delete('/users/@id', function() { /* 코드 */});
$router->patch('/users/@id', function() { /* 코드 */});
정규식(Regex)
라우트에서 정규식을 사용할 수 있습니다:
Flight::route('/user/[0-9]+', function () {
// /user/1234와 일치합니다.
});
이 방법도 사용할 수 있지만, 명명된 매개변수 또는 명명된 매개변수와 정규식을 함께 사용하는 것이 더 읽기 쉽고 유지 관리하기 쉬우므로 권장됩니다.
명명된 매개변수
라우트에서 명명된 매개변수를 지정하면 콜백 함수로 전달됩니다. 이것은 무엇보다 라우트의 가독성을 위한 것입니다. 아래 중요한 주의 사항 섹션을 참조하세요.
Flight::route('/@name/@id', function (string $name, string $id) {
echo "hello, $name ($id)!";
});
: 구분자를 사용하여 명명된 매개변수에 정규식을 포함할 수도 있습니다:
Flight::route('/@name/@id:[0-9]{3}', function (string $name, string $id) {
// /bob/123과 일치합니다.
// 그러나 /bob/12345와는 일치하지 않습니다.
});
참고: 위치 매개변수와 함께 정규식 그룹
()을 매칭하는 것은 지원되지 않습니다. 예::'\(
중요한 주의 사항
위 예제에서는 @name이 변수 $name과 직접 연결된 것처럼 보이지만 실제로는 그렇지 않습니다. 콜백 함수의 매개변수 순서가 무엇이 전달되는지를 결정합니다. 콜백 함수에서 매개변수 순서를 바꾸면 변수도 바뀝니다. 예시:
Flight::route('/@name/@id', function (string $id, string $name) {
echo "hello, $name ($id)!";
});
그리고 /bob/123 URL로 이동하면 출력은 hello, 123 (bob)!이 됩니다.
라우트와 콜백 함수를 설정할 때 주의하세요!
선택적 매개변수
세그먼트를 괄호로 묶어 매칭에 선택적인 명명된 매개변수를 지정할 수 있습니다.
Flight::route(
'/blog(/@year(/@month(/@day)))',
function(?string $year, ?string $month, ?string $day) {
// 다음 URL과 일치합니다:
// /blog/2012/12/10
// /blog/2012/12
// /blog/2012
// /blog
}
);
일치하지 않는 선택적 매개변수는 NULL로 전달됩니다.
와일드카드 라우팅
매칭은 개별 URL 세그먼트에 대해서만 수행됩니다. 여러 세그먼트를 매칭하려면 * 와일드카드를 사용할 수 있습니다.
Flight::route('/blog/*', function () {
// /blog/2000/02/01과 일치합니다.
});
모든 요청을 단일 콜백으로 라우팅하려면 다음을 수행할 수 있습니다:
Flight::route('*', function () {
// 작업 수행
});
404 Not Found 핸들러
기본적으로 URL을 찾을 수 없으면 Flight는 매우 단순하고 평범한 HTTP 404 Not Found 응답을 보냅니다. 더 사용자 정의된 404 응답을 원하면 map 메서드로 자신만의 notFound 메서드를 매핑할 수 있습니다:
Flight::map('notFound', function() {
$url = Flight::request()->url;
// 사용자 정의 템플릿으로 Flight::render()를 사용할 수도 있습니다.
$output = <<<HTML
<h1>My Custom 404 Not Found</h1>
<h3>The page you have requested {$url} could not be found.</h3>
HTML;
$this->response()
->clearBody()
->status(404)
->write($output)
->send();
});
메서드 Not Found 핸들러
기본적으로 URL은 찾았지만 메서드가 허용되지 않으면 Flight는 매우 단순하고 평범한 HTTP 405 Method Not Allowed 응답을 보냅니다(예: Method Not Allowed. Allowed Methods are: GET, POST). 또한 해당 URL에 허용된 메서드와 함께 Allow 헤더도 포함됩니다.
더 사용자 정의된 405 응답을 원하면 map 메서드로 자신만의 methodNotFound 메서드를 매핑할 수 있습니다:
use flight\net\Route;
Flight::map('methodNotFound', function(Route $route) {
$url = Flight::request()->url;
$methods = implode(', ', $route->methods);
// 사용자 정의 템플릿으로 Flight::render()를 사용할 수도 있습니다.
$output = <<<HTML
<h1>My Custom 405 Method Not Allowed</h1>
<h3>The method you have requested for {$url} is not allowed.</h3>
<p>Allowed Methods are: {$methods}</p>
HTML;
$this->response()
->clearBody()
->status(405)
->setHeader('Allow', $methods)
->write($output)
->send();
});
고급 사용법
라우트에서의 의존성 주입
컨테이너(PSR-11, PHP-DI, Dice 등)를 통해 의존성 주입을 사용하려면, 해당 기능을 사용할 수 있는 라우트 유형은 객체를 직접 생성하거나 컨테이너를 사용하여 객체를 생성하는 경우, 또는 호출할 클래스와 메서드를 문자열로 정의하는 경우뿐입니다. 자세한 내용은 의존성 주입 페이지를 참조하세요.
간단한 예시입니다:
use flight\database\SimplePdo;
// Greeting.php
class Greeting
{
protected SimplePdo $db;
public function __construct(SimplePdo $db) {
$this->db = $db;
}
public function hello(int $id) {
// $this->db로 무언가를 처리합니다.
$name = $this->db->fetchField("SELECT name FROM users WHERE id = ?", [ $id ]);
echo "Hello, world! My name is {$name}!";
}
}
// index.php
// 필요한 매개변수로 컨테이너를 설정합니다.
// PSR-11에 대한 자세한 내용은 의존성 주입 페이지를 참조하세요.
$dice = new \Dice\Dice();
// '$dice = '로 변수를 재할당하는 것을 잊지 마세요!!!!!
$dice = $dice->addRule(SimplePdo::class, [
'shared' => true,
'constructParams' => [
'mysql:host=localhost;dbname=test',
'root',
'password'
]
]);
// 컨테이너 핸들러를 등록합니다.
Flight::registerContainerHandler(function($class, $params) use ($dice) {
return $dice->create($class, $params);
});
// 일반적인 라우트처럼 사용합니다.
Flight::route('/hello/@id', [ 'Greeting', 'hello' ]);
// 또는
Flight::route('/hello/@id', 'Greeting->hello');
// 또는
Flight::route('/hello/@id', 'Greeting::hello');
Flight::start();
다음 라우트로 실행 전달하기
더 이상 사용되지 않음
콜백 함수에서 true를 반환하여 실행을 다음 일치하는 라우트로 전달할 수 있습니다.
Flight::route('/user/@name', function (string $name) {
// 어떤 조건을 확인합니다.
if ($name !== "Bob") {
// 다음 라우트로 계속 진행합니다.
return true;
}
});
Flight::route('/user/*', function () {
// 이 라우트가 호출됩니다.
});
이제 이와 같은 복잡한 사용 사례를 처리하려면 미들웨어를 사용하는 것이 권장됩니다.
라우트 별칭(Alias)
라우트에 별칭을 할당하면 나중에 앱에서 해당 별칭을 호출하여 코드에서 동적으로 URL을 생성할 수 있습니다(예: HTML 템플릿의 링크, 리디렉션 URL 생성).
Flight::route('/users/@id', function($id) { echo 'user:'.$id; }, false, 'user_view');
// 또는
Flight::route('/users/@id', function($id) { echo 'user:'.$id; })->setAlias('user_view');
// 나중에 코드 어딘가에서
class UserController {
public function update() {
// 사용자 저장 코드...
$id = $user['id']; // 예: 5
$redirectUrl = Flight::getUrl('user_view', [ 'id' => $id ]); // '/users/5'를 반환합니다.
Flight::redirect($redirectUrl);
}
}
이 기능은 URL이 변경되는 경우 특히 유용합니다. 위 예제에서 사용자가 /admin/users/@id로 이동했다고 가정해 봅시다. 라우트에 별칭이 있으면 코드에서 모든 이전 URL을 찾아 변경할 필요가 없습니다. 별칭이 이제 위 예제처럼 /admin/users/5를 반환하기 때문입니다.
라우트 별칭은 그룹에서도 계속 작동합니다:
Flight::group('/users', function() {
Flight::route('/@id', function($id) { echo 'user:'.$id; }, false, 'user_view');
// 또는
Flight::route('/@id', function($id) { echo 'user:'.$id; })->setAlias('user_view');
});
라우트 정보 검사
일치하는 라우트 정보를 검사하려면 두 가지 방법이 있습니다:
Flight::router()객체의executedRoute속성을 사용할 수 있습니다.- 라우트 메서드의 세 번째 매개변수로
true를 전달하여 라우트 객체를 콜백에 전달하도록 요청할 수 있습니다. 라우트 객체는 항상 콜백 함수에 전달되는 마지막 매개변수입니다.
executedRoute
Flight::route('/', function() {
$route = Flight::router()->executedRoute;
// $route로 무언가를 처리합니다.
// 매칭된 HTTP 메서드 배열
$route->methods;
// 명명된 매개변수 배열
$route->params;
// 일치하는 정규식
$route->regex;
// URL 패턴에 사용된 '*'의 내용을 포함합니다.
$route->splat;
// URL 경로를 표시합니다.... 정말 필요하다면 말이죠.
$route->pattern;
// 이 라우트에 할당된 미들웨어를 표시합니다.
$route->middleware;
// 이 라우트에 할당된 별칭을 표시합니다.
$route->alias;
});
참고:
executedRoute속성은 라우트가 실행된 후에만 설정됩니다. 라우트가 실행되기 전에 접근하면NULL입니다. 미들웨어에서도 executedRoute를 사용할 수 있습니다!
라우트 정의에 true 전달하기
Flight::route('/', function(\flight\net\Route $route) {
// 매칭된 HTTP 메서드 배열
$route->methods;
// 명명된 매개변수 배열
$route->params;
// 일치하는 정규식
$route->regex;
// URL 패턴에 사용된 '*'의 내용을 포함합니다.
$route->splat;
// URL 경로를 표시합니다.... 정말 필요하다면 말이죠.
$route->pattern;
// 이 라우트에 할당된 미들웨어를 표시합니다.
$route->middleware;
// 이 라우트에 할당된 별칭을 표시합니다.
$route->alias;
}, true);// <-- 이 true 매개변수가 그렇게 만듭니다.
라우트 그룹화 및 미들웨어
관련된 라우트를 함께 그룹화해야 하는 경우가 있습니다(예: /api/v1). group 메서드를 사용하여 이 작업을 수행할 수 있습니다:
Flight::group('/api/v1', function () {
Flight::route('/users', function () {
// /api/v1/users와 일치합니다.
});
Flight::route('/posts', function () {
// /api/v1/posts와 일치합니다.
});
});
그룹의 그룹을 중첩할 수도 있습니다:
Flight::group('/api', function () {
Flight::group('/v1', function () {
// Flight::get()은 변수를 가져오는 것이지 라우트를 설정하는 것이 아닙니다! 아래 객체 컨텍스트를 참조하세요.
Flight::route('GET /users', function () {
// GET /api/v1/users와 일치합니다.
});
Flight::post('/posts', function () {
// POST /api/v1/posts와 일치합니다.
});
Flight::put('/posts/1', function () {
// PUT /api/v1/posts와 일치합니다.
});
});
Flight::group('/v2', function () {
// Flight::get()은 변수를 가져오는 것이지 라우트를 설정하는 것이 아닙니다! 아래 객체 컨텍스트를 참조하세요.
Flight::route('GET /users', function () {
// GET /api/v2/users와 일치합니다.
});
});
});
객체 컨텍스트로 그룹화
Engine 객체를 사용하여 다음과 같이 라우트 그룹화를 계속 사용할 수 있습니다:
$app = Flight::app();
$app->group('/api/v1', function (Router $router) {
// $router 변수를 사용합니다.
$router->get('/users', function () {
// GET /api/v1/users와 일치합니다.
});
$router->post('/posts', function () {
// POST /api/v1/posts와 일치합니다.
});
});
참고: 이것이
$router객체로 라우트와 그룹을 정의하는 선호되는 방법입니다.
미들웨어로 그룹화
라우트 그룹에 미들웨어를 할당할 수도 있습니다:
Flight::group('/api/v1', function () {
Flight::route('/users', function () {
// /api/v1/users와 일치합니다.
});
}, [ MyAuthMiddleware::class ]); // 인스턴스를 사용하려면 [ new MyAuthMiddleware() ] 로도 가능합니다.
자세한 내용은 그룹 미들웨어 페이지를 참조하세요.
리소스 라우팅
resource 메서드를 사용하여 리소스에 대한 일련의 라우트를 생성할 수 있습니다. 이는 RESTful 규칙을 따르는 라우트 세트를 생성합니다.
리소스를 생성하려면 다음을 수행하세요:
Flight::resource('/users', UsersController::class);
그러면 내부적으로 다음 라우트가 생성됩니다:
[
'index' => 'GET /users',
'create' => 'GET /users/create',
'store' => 'POST /users',
'show' => 'GET /users/@id',
'edit' => 'GET /users/@id/edit',
'update' => 'PUT /users/@id',
'destroy' => 'DELETE /users/@id'
]
그리고 컨트롤러는 다음 메서드를 사용합니다:
class UsersController
{
public function index(): void
{
}
public function show(string $id): void
{
}
public function create(): void
{
}
public function store(): void
{
}
public function edit(string $id): void
{
}
public function update(string $id): void
{
}
public function destroy(string $id): void
{
}
}
참고:
runway를 사용하여php runway routes명령으로 새로 추가된 라우트를 확인할 수 있습니다.
리소스 라우트 사용자 정의
리소스 라우트를 구성하는 몇 가지 옵션이 있습니다.
별칭 기본값 (Alias Base)
aliasBase를 구성할 수 있습니다. 기본적으로 별칭은 지정된 URL의 마지막 부분입니다. 예를 들어 /users/는 aliasBase가 users가 됩니다. 이러한 라우트가 생성되면 별칭은 users.index, users.create 등이 됩니다. 별칭을 변경하려면 aliasBase를 원하는 값으로 설정하세요.
Flight::resource('/users', UsersController::class, [ 'aliasBase' => 'user' ]);
Only 및 Except
only 및 except 옵션을 사용하여 생성하려는 라우트를 지정할 수도 있습니다.
// 이 메서드들만 허용 목록으로 지정하고 나머지는 차단합니다.
Flight::resource('/users', UsersController::class, [ 'only' => [ 'index', 'show' ] ]);
// 이 메서드들만 차단 목록으로 지정하고 나머지는 허용합니다.
Flight::resource('/users', UsersController::class, [ 'except' => [ 'create', 'store', 'edit', 'update', 'destroy' ] ]);
기본적으로 허용 목록 및 차단 목록 옵션이므로 생성하려는 라우트를 지정할 수 있습니다.
미들웨어
resource 메서드로 생성된 각 라우트에서 실행할 미들웨어를 지정할 수도 있습니다.
Flight::resource('/users', UsersController::class, [ 'middleware' => [ MyAuthMiddleware::class ] ]);
스트리밍 응답
이제 stream() 또는 streamWithHeaders()를 사용하여 응답을 클라이언트로 스트리밍할 수 있습니다. 대용량 파일 전송, 장기 실행 프로세스 또는 대용량 응답 생성에 유용합니다. 라우트 스트리밍은 일반 라우트와 약간 다르게 처리됩니다.
참고: 스트리밍 응답은
flight.v2.output_buffering이false로 설정된 경우에만 사용할 수 있습니다.
수동 헤더로 스트리밍
라우트에서 stream() 메서드를 사용하여 응답을 클라이언트로 스트리밍할 수 있습니다. 이 경우 클라이언트에 무언가를 출력하기 전에 모든 헤더를 직접 설정해야 합니다. 이는 PHP의 header() 함수 또는 Flight::response()->setRealHeader() 메서드로 수행됩니다.
Flight::route('/@filename', function($filename) {
$response = Flight::response();
// 당연히 경로 등을 정리해야 합니다.
$fileNameSafe = basename($filename);
// 라우트가 실행된 후 여기에 추가 헤더를 설정해야 한다면
// 아무것도 출력하기 전에 정의해야 합니다.
// 모든 헤더는 header() 함수 또는
// Flight::response()->setRealHeader() 호출이어야 합니다.
header('Content-Disposition: attachment; filename="'.$fileNameSafe.'"');
// 또는
$response->setRealHeader('Content-Disposition: attachment; filename="'.$fileNameSafe.'"');
$filePath = '/some/path/to/files/'.$fileNameSafe;
if (!is_readable($filePath)) {
Flight::halt(404, 'File not found');
}
// 원한다면 콘텐츠 길이를 직접 설정합니다.
header('Content-Length: '.filesize($filePath));
// 또는
$response->setRealHeader('Content-Length: '.filesize($filePath));
// 파일을 읽으면서 클라이언트로 스트리밍합니다.
readfile($filePath);
// 이것이 핵심 마법 코드입니다.
})->stream();
헤더와 함께 스트리밍
streamWithHeaders() 메서드를 사용하여 스트리밍을 시작하기 전에 헤더를 설정할 수도 있습니다.
Flight::route('/stream-users', function() {
// 원하는 추가 헤더를 여기에 추가할 수 있습니다.
// header() 또는 Flight::response()->setRealHeader()를 사용해야 합니다.
// 데이터를 가져오는 방식은 자유입니다. 예시일 뿐입니다...
$users_stmt = Flight::db()->query("SELECT id, first_name, last_name FROM users");
echo '{';
$user_count = count($users);
while($user = $users_stmt->fetch(PDO::FETCH_ASSOC)) {
echo json_encode($user);
if(--$user_count > 0) {
echo ',';
}
// 데이터를 클라이언트로 보내려면 이 코드가 필요합니다.
ob_flush();
}
echo '}';
// 이렇게 하면 스트리밍을 시작하기 전에 헤더를 설정할 수 있습니다.
})->streamWithHeaders([
'Content-Type' => 'application/json',
'Content-Disposition' => 'attachment; filename="users.json"',
// 선택적 상태 코드, 기본값은 200입니다.
'status' => 200
]);
같이 보기
- 미들웨어 - 인증, 로깅 등에 라우트와 함께 미들웨어 사용하기.
- 의존성 주입 - 라우트에서 객체 생성 및 관리를 단순화합니다.
- 프레임워크가 왜 필요한가? - Flight와 같은 프레임워크 사용의 이점 이해하기.
- 확장하기 -
notFound메서드를 포함하여 Flight를 자신만의 기능으로 확장하는 방법. - php.net: preg_match - 정규식 매칭을 위한 PHP 함수.
문제 해결
- 라우트 매개변수는 이름이 아닌 순서로 매칭됩니다. 콜백 매개변수 순서가 라우트 정의와 일치하는지 확인하세요.
Flight::get()은 라우트를 정의하지 않습니다. 라우팅에는Flight::route('GET /...')또는 그룹 내의 Router 객체 컨텍스트(예:$router->get(...))를 사용하세요.executedRoute속성은 라우트가 실행된 후에만 설정됩니다. 실행 전에는 NULL입니다.- 스트리밍은 레거시 Flight 출력 버퍼링 기능을 비활성화해야 합니다 (
flight.v2.output_buffering = false). - 의존성 주입의 경우 특정 라우트 정의만 컨테이너 기반 인스턴스 생성을 지원합니다.
404 Not Found 또는 예기치 않은 라우트 동작
404 Not Found 오류가 표시되는 경우(하지만 정말로 라우트가 존재하고 오타가 아니라고 확신하는 경우) 라우트 엔드포인트에서 값을 echo하지 않고 반환하고 있기 때문일 수 있습니다. 그 이유는 의도적이지만 일부 개발자에게는 문제가 될 수 있습니다.
Flight::route('/hello', function(){
// 이렇게 하면 404 Not Found 오류가 발생할 수 있습니다.
return 'Hello World';
});
// 원하는 방식
Flight::route('/hello', function(){
echo 'Hello World';
});
그 이유는 반환 출력을 "다음 라우트로 이동" 신호로 처리하는 라우터의 특수 메커니즘 때문입니다. 이 동작은 라우팅 섹션에 문서화되어 있습니다.
변경 로그
- v3: 리소스 라우팅, 라우트 별칭, 스트리밍 지원, 라우트 그룹 및 미들웨어 지원이 추가되었습니다.
- v1: 대부분의 기본 기능이 제공됩니다.
Learn/learn
Flight 알아보기
Flight는 PHP를 위한 빠르고, 간단하며, 확장 가능한 프레임워크입니다. 매우 다재다능하며 모든 종류의 웹 애플리케이션을 구축하는 데 사용할 수 있습니다. 단순함을 염두에 두고 제작되었으며, 인간과 AI 코딩 어시스턴트 모두가 이해하고 사용하기 쉽게 작성되었습니다.
참고:
Flight::를 정적 변수로 사용하는 예제와$app->Engine 객체를 사용하는 예제가 있습니다. 둘 다 서로 교환하여 사용할 수 있습니다. 컨트롤러/미들웨어에서$app및$this->app은 Flight 팀이 권장하는 방식입니다(그리고 공식 스켈레톤 +AGENTS.md가 새 프로젝트에 대해 표준화한 방식입니다).
핵심 구성 요소
라우팅
웹 애플리케이션의 라우트를 관리하는 방법을 알아보세요. 라우트 그룹화, 라우트 매개변수 및 미들웨어도 포함됩니다.
미들웨어
애플리케이션에서 요청과 응답을 필터링하기 위해 미들웨어를 사용하는 방법을 알아보세요.
자동 로드
자체 클래스를 자동 로드하는 방법을 알아보세요. 폴더 대소문자는 네임스페이스와 일치해야 합니다. 스켈레톤은 App\과 app/Controller/ 같은 PascalCase 폴더를 사용합니다.
요청
애플리케이션에서 요청과 응답을 처리하는 방법을 알아보세요.
응답
사용자에게 응답을 보내는 방법을 알아보세요.
HTML 템플릿
기본 제공 PHP 뷰뿐만 아니라 Twig(스켈레톤 기본값), Latte 또는 다른 엔진으로 HTML을 렌더링하는 방법을 알아보세요.
보안
일반적인 보안 위협으로부터 애플리케이션을 보호하는 방법을 알아보세요.
구성
애플리케이션에 맞게 프레임워크를 구성하는 방법을 알아보세요.
이벤트 매니저
이벤트 시스템을 사용하여 애플리케이션에 사용자 정의 이벤트를 추가하는 방법을 알아보세요.
Flight 확장
프레임워크에 자신만의 메서드와 클래스를 추가하여 확장하는 방법을 알아보세요.
메서드 훅과 필터링
메서드 및 내부 프레임워크 메서드에 이벤트 훅을 추가하는 방법을 알아보세요.
의존성 주입 컨테이너(DIC)
의존성 주입 컨테이너(DIC)를 사용하여 애플리케이션의 의존성을 관리하는 방법을 알아보세요.
유틸리티 클래스
컬렉션
컬렉션은 데이터를 보관하고 쉽게 사용할 수 있도록 배열 또는 객체로 접근할 수 있게 해줍니다.
JSON 래퍼
JSON 인코딩 및 디코딩을 일관되게 만드는 몇 가지 간단한 함수를 제공합니다.
SimplePdo
PDO는 때때로 필요 이상으로 골치 아플 수 있습니다. SimplePdo는 insert(), update(), delete(), transaction()과 같은 편리한 메서드를 제공하여 데이터베이스 작업을 훨씬 쉽게 만들어 주는 현대적인 PDO 헬퍼 클래스입니다.
PdoWrapper (더 이상 사용되지 않음)
원래 PDO 래퍼는 v3.18.0부터 더 이상 사용되지 않습니다. 대신 SimplePdo를 사용하세요.
업로드 파일 핸들러
업로드된 파일을 관리하고 영구 위치로 이동하는 데 도움이 되는 간단한 클래스입니다.
중요한 개념
프레임워크를 사용하는 이유는 무엇인가요?
프레임워크를 사용해야 하는 이유에 대한 짧은 문서입니다. 프레임워크를 사용하기 전에 프레임워크 사용의 이점을 이해하는 것이 좋습니다.
또한 @lubiana가 훌륭한 튜토리얼을 만들었습니다. Flight에 대해 구체적으로 자세히 다루지는 않지만, 이 가이드는 프레임워크와 관련된 주요 개념과 그것들이 왜 유용한지 이해하는 데 도움이 될 것입니다. 튜토리얼은 여기에서 찾을 수 있습니다.
다른 프레임워크와 Flight 비교
Laravel, Slim, Fat-Free 또는 Symfony와 같은 다른 프레임워크에서 Flight로 마이그레이션하는 경우, 이 페이지는 두 프레임워크 간의 차이점을 이해하는 데 도움이 됩니다.
기타 주제
유닛 테스트
이 가이드를 따라 Flight 코드를 견고하게 유닛 테스트하는 방법을 알아보세요.
AI 및 개발자 경험
Flight는 코딩 LLM과 함께 사용하도록 설계되었습니다: AGENTS.md, Runway ai:* 명령, 그리고 에이전트가 패턴을 유지할 수 있는 명확한 스켈레톤 레이아웃을 제공합니다.
v2 -> v3 마이그레이션
대부분 이전 버전과의 호환성이 유지되었지만, v2에서 v3로 마이그레이션할 때 알아야 할 몇 가지 변경 사항이 있습니다.
Learn/unit_testing
단위 테스트
개요
Flight에서의 단위 테스트는 애플리케이션이 예상대로 작동하는지 확인하고, 버그를 조기에 발견하며, 코드베이스를 더 쉽게 유지보수할 수 있게 도와줍니다. Flight는 가장 널리 사용되는 PHP 테스트 프레임워크인 PHPUnit과 원활하게 작동하도록 설계되었습니다.
이해하기
단위 테스트는 애플리케이션의 작은 부분(컨트롤러나 서비스 등)을 격리된 상태에서 검사합니다. Flight에서 이는 전역 상태나 실제 외부 서비스에 의존하지 않고, 라우트, 컨트롤러, 로직이 다양한 입력에 어떻게 반응하는지 테스트하는 것을 의미합니다.
핵심 원칙:
- 구현이 아닌 동작을 테스트하세요: 코드가 무엇을 하는지에 집중하고, 어떻게 하는지에는 집중하지 마세요.
- 전역 상태를 피하세요:
Flight::set()이나Flight::get()대신 의존성 주입을 사용하세요. - 외부 서비스를 목(mock) 처리하세요: 데이터베이스나 메일러 같은 것들은 테스트 더블로 대체하세요.
- 테스트를 빠르고 집중적으로 유지하세요: 단위 테스트는 실제 데이터베이스나 API를 사용하지 않아야 합니다.
기본 사용법
PHPUnit 설정하기
- Composer로 PHPUnit을 설치합니다:
composer require --dev phpunit/phpunit - 프로젝트 루트에
tests디렉토리를 만듭니다. composer.json에 테스트 스크립트를 추가합니다:"scripts": { "test": "phpunit --configuration phpunit.xml" }phpunit.xml파일을 만듭니다:<?xml version="1.0" encoding="UTF-8"?> <phpunit bootstrap="vendor/autoload.php"> <testsuites> <testsuite name="Flight Tests"> <directory>tests</directory> </testsuite> </testsuites> </phpunit>
이제 composer test 명령으로 테스트를 실행할 수 있습니다.
간단한 라우트 핸들러 테스트하기
이메일을 검증하는 라우트가 있다고 가정해 보겠습니다:
// index.php
$app->route('POST /register', [ UserController::class, 'register' ]);
// UserController.php
class UserController {
protected $app;
public function __construct(flight\Engine $app) {
$this->app = $app;
}
public function register() {
$email = $this->app->request()->data->email;
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
return $this->app->json(['status' => 'error', 'message' => 'Invalid email']);
}
return $this->app->json(['status' => 'success', 'message' => 'Valid email']);
}
}
이 컨트롤러에 대한 간단한 테스트입니다:
use PHPUnit\Framework\TestCase;
use flight\Engine;
class UserControllerTest extends TestCase {
public function testValidEmailReturnsSuccess() {
$app = new Engine();
$app->request()->data->email = 'test@example.com';
$controller = new UserController($app);
$controller->register();
$response = $app->response()->getBody();
$output = json_decode($response, true);
$this->assertEquals('success', $output['status']);
$this->assertEquals('Valid email', $output['message']);
}
public function testInvalidEmailReturnsError() {
$app = new Engine();
$app->request()->data->email = 'invalid-email';
$controller = new UserController($app);
$controller->register();
$response = $app->response()->getBody();
$output = json_decode($response, true);
$this->assertEquals('error', $output['status']);
$this->assertEquals('Invalid email', $output['message']);
}
}
팁:
$app->request()->data를 사용하여 POST 데이터를 시뮬레이션하세요.- 테스트에서
Flight::정적 메서드를 사용하지 말고$app인스턴스를 사용하세요.
테스트 가능한 컨트롤러를 위한 의존성 주입 사용하기
데이터베이스나 메일러 같은 의존성을 컨트롤러에 주입하면 테스트에서 쉽게 목(mock) 처리할 수 있습니다:
use flight\database\SimplePdo;
class UserController {
protected $app;
protected $db;
protected $mailer;
public function __construct($app, $db, $mailer) {
$this->app = $app;
$this->db = $db;
$this->mailer = $mailer;
}
public function register() {
$email = $this->app->request()->data->email;
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
return $this->app->json(['status' => 'error', 'message' => 'Invalid email']);
}
$this->db->runQuery('INSERT INTO users (email) VALUES (?)', [$email]);
$this->mailer->sendWelcome($email);
return $this->app->json(['status' => 'success', 'message' => 'User registered']);
}
}
목(mock)을 사용한 테스트:
use PHPUnit\Framework\TestCase;
class UserControllerDICTest extends TestCase {
public function testValidEmailSavesAndSendsEmail() {
$mockDb = $this->createMock(flight\database\SimplePdo::class);
$mockDb->method('runQuery')->willReturn(true);
$mockMailer = new class {
public $sentEmail = null;
public function sendWelcome($email) { $this->sentEmail = $email; return true; }
};
$app = new flight\Engine();
$app->request()->data->email = 'test@example.com';
$controller = new UserController($app, $mockDb, $mockMailer);
$controller->register();
$response = $app->response()->getBody();
$result = json_decode($response, true);
$this->assertEquals('success', $result['status']);
$this->assertEquals('User registered', $result['message']);
$this->assertEquals('test@example.com', $mockMailer->sentEmail);
}
}
고급 사용법
- 목(Mocking): PHPUnit의 내장 목(mock)이나 익명 클래스를 사용하여 의존성을 대체하세요.
- 컨트롤러 직접 테스트: 새로운
Engine과 목(mock) 의존성으로 컨트롤러를 인스턴스화하세요. - 과도한 목(mock) 사용을 피하세요: 가능하면 실제 로직이 실행되도록 하고, 외부 서비스만 목(mock) 처리하세요.
참조
- 단위 테스트 가이드 - 단위 테스트 모범 사례에 대한 종합 가이드입니다.
- 의존성 주입 컨테이너 - DIC를 사용하여 의존성을 관리하고 테스트 용이성을 향상시키는 방법입니다.
- 확장하기 - 자신만의 헬퍼를 추가하거나 핵심 클래스를 재정의하는 방법입니다.
- SimplePdo - 데이터베이스 상호작용을 단순화하고 테스트에서 목(mock) 처리를 쉽게 합니다.
- 요청(Requests) - Flight에서 HTTP 요청을 처리하는 방법입니다.
- 응답(Responses) - 사용자에게 응답을 보내는 방법입니다.
- 단위 테스트와 SOLID 원칙 - SOLID 원칙이 단위 테스트를 어떻게 개선할 수 있는지 알아보세요.
문제 해결
- 코드와 테스트에서 전역 상태(
Flight::set(),$_SESSION등)를 사용하지 마세요. - 테스트가 느리다면 통합 테스트를 작성하고 있을 수 있습니다. 외부 서비스를 목(mock) 처리하여 단위 테스트를 빠르게 유지하세요.
- 테스트 설정이 복잡하다면 의존성 주입을 사용하도록 코드를 리팩터링하는 것을 고려하세요.
변경 이력
- v3.15.0 - 의존성 주입 및 목(mock) 처리에 대한 예제가 추가되었습니다.
Learn/flight_vs_symfony
플라이트 대 시미포니
시미포니란 무엇인가요?
Symfony은 웹 프로젝트용 PHP 프레임워크 및 재사용 가능한 PHP 구성 요소 세트입니다.
최고의 PHP 응용프로그램이 구축되는 표준 기반입니다. 50 개의 독립형 구성 요소 중에서 필요한 애플리케이션에 사용할 수 있습니다.
PHP 웹 애플리케이션의 생성 및 유지 관리 속도를 높이세요. 반복되는 코딩 작업을 줄이고 코드를 제어하는 강력함을 누려보세요.
플라이트와 비교한 장점
- 시미포니는 공통 문제를 해결하는 데 사용할 수 있는 개발자 및 모듈의 거대한 생태계를 보유하고 있습니다.
- 시미포니에는 데이터베이스와 상호 작용할 수 있는 완전한 기능의 ORM(Doctrine)이 있습니다.
- 시미포니에는 프레임워크 학습에 사용할 수 있는 방대한 양의 문서 및 자습서가 있습니다.
- 시미포니에는 프레임워크 학습에 사용할 수 있는 팟캐스트, 컨퍼런스, 미팅, 비디오 및 기타 리소스가 있습니다.
- 시미포니는 풀 기능을 갖춘 엔터프라이즈 웹 애플리케이션을 구축하려는 경험이 풍부한 개발자를 위해 고안되었습니다.
플라이트와 비교한 단점
- 시미포니는 플라이트보다 하위 수준에서 수행되는 작업이 훨씬 많습니다. 이는 성능 측면에서 극적인 비용이 발생합니다. 자세한 내용은 TechEmpower benchmarks를 참조하세요.
- 플라이트는 가볍고 빠르며 사용하기 쉬운 웹 애플리케이션을 구축하려는 개발자를 대상으로 합니다.
- 플라이트는 단순함과 사용 편의성을 지향합니다.
- 플라이트의 핵심 기능 중 하나는 되도록 역 호환성을 유지하려고 합니다.
- 플라이트에는 의존성이 없지만, 시미포니에는 많은 의존성이 있습니다.
- 플라이트는 처음 프레임워크의 세계로 진입하는 개발자들을 대상으로 합니다.
- 플라이트는 엔터프라이즈 수준의 애플리케이션도 수행할 수 있지만, 시미포니만큼 많은 예제와 자습서가 제공되지 않습니다. 또한, 개발자가 조직화하고 잘 구조화하는 데 더 많은 dis선이 필요합니다.
- 플라이트는 개발자에게 애플리케이션에 대한 더 많은 제어권을 제공하는 반면, 시미포니는 종종 백그라운드에서 어떤 마법을 실행할 수도 있습니다.
Learn/flight_vs_another_framework
다른 프레임워크와 Flight 비교
다른 프레임워크(Laravel, Slim, Fat-Free, 또는 Symfony와 같은)에서 Flight로 이관하는 경우, 이 페이지에서 두 프레임워크 간의 차이를 이해하는 데 도움이 될 것입니다.
Laravel
Laravel은 모든 기능을 갖추고 있으며 탁월한 개발자 중심 생태계를 갖춘 완전한 기능을 제공하는 프레임워크이지만, 성능과 복잡성 면에서 비용이 발생합니다.
Slim
Slim은 Flight와 유사한 미니멀한 프레임워크입니다. 가벼우며 사용하기 쉽도록 설계되었지만, Flight보다 약간 더 복잡할 수 있습니다.
Fat-Free
Fat-Free은 훨씬 작은 패키지에 전체 스택 프레임워크가 포함되어 있습니다. 모든 도구가 갖춰져 있지만, 어떤 프로젝트는 불필요한 복잡성을 초래할 수 있는 데이터 아키텍처를 가지고 있습니다.
Symfony
Symfony는 모듈식 엔터프라이즈 레벨 프레임워크로서 유연하고 확장 가능하게 설계되었습니다. 작은 프로젝트나 신입 개발자에게는 Symfony가 약간 압도적일 수 있습니다.
Learn/pdo_wrapper
PdoWrapper PDO 도우미 클래스
경고
사용 중단됨:
PdoWrapper는 Flight v3.18.0부터 사용 중단되었습니다. 향후 버전에서 제거되지 않지만, 이전 호환성을 위해 유지될 것입니다. 동일한 기능에 더해 일반적인 데이터베이스 작업을 위한 추가 도우미 메서드를 제공하는 SimplePdo를 대신 사용하십시오.
개요
Flight의 PdoWrapper 클래스는 PDO를 사용하여 데이터베이스 작업을 수행하는 데 친근한 도우미입니다. 일반적인 데이터베이스 작업을 간소화하고, 결과를 가져오는 데 유용한 메서드를 추가하며, 결과를 Collections으로 반환하여 쉽게 접근할 수 있습니다. 또한 쿼리 로깅과 애플리케이션 성능 모니터링(APM)을 지원하여 고급 사용 사례를 처리합니다.
이해하기
PHP에서 PDO를 직접 사용하면 데이터베이스 작업이 다소 장황할 수 있습니다. PdoWrapper는 PDO를 확장하여 쿼리, 결과 가져오기, 결과 처리 작업을 훨씬 쉽게 만드는 메서드를 추가합니다. 준비된 문장과 가져오기 모드를 다루는 대신 일반적인 작업을 위한 간단한 메서드를 얻고, 모든 행이 Collection으로 반환되므로 배열 또는 객체 표기법을 사용할 수 있습니다.
PdoWrapper를 Flight에서 공유 서비스로 등록할 수 있으며, 그 후 앱의 어디서나 Flight::db()를 통해 사용할 수 있습니다.
기본 사용법
PDO 도우미 등록하기
먼저 Flight에 PdoWrapper 클래스를 등록하십시오:
Flight::register('db', \flight\database\PdoWrapper::class, [
'mysql:host=localhost;dbname=cool_db_name', 'user', 'pass', [
PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES \'utf8mb4\'',
PDO::ATTR_EMULATE_PREPARES => false,
PDO::ATTR_STRINGIFY_FETCHES => false,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
]
]);
이제 어디서나 Flight::db()를 사용하여 데이터베이스 연결을 얻을 수 있습니다.
쿼리 실행하기
runQuery()
function runQuery(string $sql, array $params = []): PDOStatement
INSERT, UPDATE 또는 결과를 수동으로 가져오고 싶을 때 사용하십시오:
$db = Flight::db();
$statement = $db->runQuery("SELECT * FROM users WHERE status = ?", ['active']);
while ($row = $statement->fetch()) {
// $row는 배열입니다
}
쓰기 작업에도 사용할 수 있습니다:
$db->runQuery("INSERT INTO users (name) VALUES (?)", ['Alice']);
$db->runQuery("UPDATE users SET name = ? WHERE id = ?", ['Bob', 1]);
fetchField()
function fetchField(string $sql, array $params = []): mixed
데이터베이스에서 단일 값을 가져옵니다:
$count = Flight::db()->fetchField("SELECT COUNT(*) FROM users WHERE status = ?", ['active']);
fetchRow()
function fetchRow(string $sql, array $params = []): Collection
Collection(배열/객체 접근)으로 단일 행을 가져옵니다:
$user = Flight::db()->fetchRow("SELECT * FROM users WHERE id = ?", [123]);
echo $user['name'];
// 또는
echo $user->name;
fetchAll()
function fetchAll(string $sql, array $params = []): array<Collection>
모든 행을 Collection 배열로 가져옵니다:
$users = Flight::db()->fetchAll("SELECT * FROM users WHERE status = ?", ['active']);
foreach ($users as $user) {
echo $user['name'];
// 또는
echo $user->name;
}
IN() 플레이스홀더 사용하기
IN() 절에서 단일 ?를 사용하고 배열 또는 쉼표로 구분된 문자열을 전달할 수 있습니다:
$ids = [1, 2, 3];
$users = Flight::db()->fetchAll("SELECT * FROM users WHERE id IN (?)", [$ids]);
// 또는
$users = Flight::db()->fetchAll("SELECT * FROM users WHERE id IN (?)", ['1,2,3']);
고급 사용법
쿼리 로깅 & APM
쿼리 성능을 추적하려면 등록 시 APM 추적을 활성화하십시오:
Flight::register('db', \flight\database\PdoWrapper::class, [
'mysql:host=localhost;dbname=cool_db_name', 'user', 'pass', [/* options */], true // 마지막 매개변수가 APM을 활성화합니다
]);
쿼리를 실행한 후 수동으로 로깅할 수 있지만, 활성화된 경우 APM이 자동으로 로깅합니다:
Flight::db()->logQueries();
이것은 연결 및 쿼리 메트릭스가 포함된 이벤트(flight.db.queries)를 발생시키며, Flight의 이벤트 시스템을 사용하여 이를 수신할 수 있습니다.
전체 예제
Flight::route('/users', function () {
// 모든 사용자 가져오기
$users = Flight::db()->fetchAll('SELECT * FROM users');
// 모든 사용자 스트리밍
$statement = Flight::db()->runQuery('SELECT * FROM users');
while ($user = $statement->fetch()) {
echo $user['name'];
}
// 단일 사용자 가져오기
$user = Flight::db()->fetchRow('SELECT * FROM users WHERE id = ?', [123]);
// 단일 값 가져오기
$count = Flight::db()->fetchField('SELECT COUNT(*) FROM users');
// 특수 IN() 구문
$users = Flight::db()->fetchAll('SELECT * FROM users WHERE id IN (?)', [[1,2,3,4,5]]);
$users = Flight::db()->fetchAll('SELECT * FROM users WHERE id IN (?)', ['1,2,3,4,5']);
// 새 사용자 삽입
Flight::db()->runQuery("INSERT INTO users (name, email) VALUES (?, ?)", ['Bob', 'bob@example.com']);
$insert_id = Flight::db()->lastInsertId();
// 사용자 업데이트
Flight::db()->runQuery("UPDATE users SET name = ? WHERE id = ?", ['Bob', 123]);
// 사용자 삭제
Flight::db()->runQuery("DELETE FROM users WHERE id = ?", [123]);
// 영향을 받은 행 수 가져오기
$statement = Flight::db()->runQuery("UPDATE users SET name = ? WHERE name = ?", ['Bob', 'Sally']);
$affected_rows = $statement->rowCount();
});
관련 자료
- Collections - 쉬운 데이터 접근을 위한 Collection 클래스 사용법을 알아보십시오.
문제 해결
- 데이터베이스 연결 오류가 발생하면 DSN, 사용자 이름, 비밀번호 및 옵션을 확인하십시오.
- 모든 행은 Collection으로 반환됩니다—일반 배열이 필요하면
$collection->getData()를 사용하십시오. IN (?)쿼리의 경우 배열 또는 쉼표로 구분된 문자열을 전달했는지 확인하십시오.
변경 로그
- v3.2.0 - 기본 쿼리 및 가져오기 메서드가 포함된 PdoWrapper의 초기 릴리스.
Learn/dependency_injection_container
의존성 주입 컨테이너
개요
의존성 주입 컨테이너(DIC)는 애플리케이션의 의존성을 관리할 수 있게 해주는 강력한 기능입니다. 또한 Flight가 AI 코딩 도구 및 단위 테스트와 잘 연동되는 가장 큰 이유 중 하나입니다. 컨트롤러가 전역 변수에 의존하는 대신 생성자에서 필요한 것을 주입받기 때문입니다.
이해
의존성 주입(DI)은 현대 PHP 프레임워크의 핵심 개념이며 객체의 생성과 구성을 관리하는 데 사용됩니다. DIC 라이브러리의 예로는 flightphp/container, Dice, Pimple, PHP-DI, league/container 등이 있습니다.
DIC는 클래스를 중앙 집중식으로 생성하고 관리하는 멋진 방법입니다. 동일한 객체를 여러 클래스(컨트롤러, 미들웨어, 커맨드 등)에 전달해야 할 때 유용합니다.
공식 flightphp/skeleton은 app/config/services.php에 Dice를 연결하고, 공유 flight\Engine 인스턴스를 대체하며, [App\Controller\HomeController::class, 'index'] 같은 라우트 대상을 처리합니다. 새 프로젝트에서는 사람과 에이전트가 동일한 위치를 편집할 수 있도록 이 패턴을 선호하세요.
기본 사용법
기존 방식은 다음과 같을 수 있습니다.
require 'vendor/autoload.php';
// 데이터베이스에서 사용자를 관리하는 클래스
class UserController {
protected PDO $pdo;
public function __construct(PDO $pdo) {
$this->pdo = $pdo;
}
public function view(int $id) {
$stmt = $this->pdo->prepare('SELECT * FROM users WHERE id = :id');
$stmt->execute(['id' => $id]);
print_r($stmt->fetch());
}
}
// routes.php 파일에서
$db = new PDO('mysql:host=localhost;dbname=test', 'user', 'pass');
$UserController = new UserController($db);
Flight::route('/user/@id', [ $UserController, 'view' ]);
// 다른 UserController 라우트들...
Flight::start();
위 코드를 보면 새로운 PDO 객체를 생성하여 UserController 클래스에 전달하고 있습니다. 이는 작은 애플리케이션에는 괜찮지만, 애플리케이션이 커질수록 동일한 PDO 객체를 여러 곳에서 생성하거나 전달하게 되는 상황이 발생합니다. 이때 DIC가 유용하게 사용됩니다.
다음은 DIC를 사용한 동일한 예제입니다 (Dice 사용):
require 'vendor/autoload.php';
// 위와 같은 클래스입니다. 변경된 것이 없습니다.
class UserController {
protected PDO $pdo;
public function __construct(PDO $pdo) {
$this->pdo = $pdo;
}
public function view(int $id) {
$stmt = $this->pdo->prepare('SELECT * FROM users WHERE id = :id');
$stmt->execute(['id' => $id]);
print_r($stmt->fetch());
}
}
// 새 컨테이너 생성
$container = new \Dice\Dice;
// 컨테이너에 PDO 객체 생성 방법을 알려주는 규칙 추가
// 아래처럼 반드시 자기 자신에게 다시 할당하는 것을 잊지 마세요!
$container = $container->addRule('PDO', [
// shared는 매번 동일한 객체가 반환된다는 의미입니다.
'shared' => true,
'constructParams' => ['mysql:host=localhost;dbname=test', 'user', 'pass' ]
]);
// 이것은 Flight가 사용할 수 있도록 컨테이너 핸들러를 등록합니다.
Flight::registerContainerHandler(function($class, $params) use ($container) {
return $container->create($class, $params);
});
// 이제 컨테이너를 사용하여 UserController를 생성할 수 있습니다.
Flight::route('/user/@id', [ UserController::class, 'view' ]);
Flight::start();
예제에 불필요한 코드가 많이 추가되었다고 생각할 수도 있습니다. 마법은 다른 컨트롤러가 PDO 객체를 필요로 할 때 드러납니다.
// 모든 컨트롤러의 생성자가 PDO 객체를 필요로 한다면
// 아래의 각 라우트는 자동으로 PDO 객체를 주입받습니다!!!
Flight::route('/company/@id', [ CompanyController::class, 'view' ]);
Flight::route('/organization/@id', [ OrganizationController::class, 'view' ]);
Flight::route('/category/@id', [ CategoryController::class, 'view' ]);
Flight::route('/settings', [ SettingsController::class, 'view' ]);
DIC를 활용하는 추가적인 이점은 단위 테스트가 훨씬 쉬워진다는 것입니다. 목(mock) 객체를 만들어 클래스에 전달할 수 있습니다. 이는 애플리케이션 테스트를 작성할 때 큰 이점이며, AI 어시스턴트가 컨트롤러를 생성할 때 생성자 주입은 따를 수 있는 명확하고 일관된 패턴을 제공합니다 (단위 테스트 가이드).
중앙 집중식 DIC 핸들러 만들기
앱 확장을 통해 서비스 파일에 중앙 집중식 DIC 핸들러를 만들 수 있습니다. 예시는 다음과 같습니다.
// services.php
// 새 컨테이너 생성
$container = new \Dice\Dice;
// 아래처럼 반드시 자기 자신에게 다시 할당하는 것을 잊지 마세요!
$container = $container->addRule('PDO', [
// shared는 매번 동일한 객체가 반환된다는 의미입니다.
'shared' => true,
'constructParams' => ['mysql:host=localhost;dbname=test', 'user', 'pass' ]
]);
// 이제 모든 객체를 생성할 수 있는 매핑 가능한 메서드를 만들 수 있습니다.
Flight::map('make', function($class, $params = []) use ($container) {
return $container->create($class, $params);
});
// 컨트롤러/미들웨어에 사용할 수 있도록 Flight가 인식하는 컨테이너 핸들러를 등록합니다.
Flight::registerContainerHandler(function($class, $params) {
return Flight::make($class, $params);
});
// 생성자에서 PDO 객체를 받는 다음 샘플 클래스가 있다고 가정해 봅시다.
class EmailCron {
protected PDO $pdo;
public function __construct(PDO $pdo) {
$this->pdo = $pdo;
}
public function send() {
// 이메일을 보내는 코드
}
}
// 마지막으로 의존성 주입을 사용하여 객체를 생성할 수 있습니다.
$emailCron = Flight::make(EmailCron::class);
$emailCron->send();
flightphp/container
Flight에는 의존성 주입을 처리하는 데 사용할 수 있는 간단한 PSR-11 호환 컨테이너를 제공하는 플러그인이 있습니다. 사용 방법에 대한 간단한 예제는 다음과 같습니다.
// 예를 들어 index.php
require 'vendor/autoload.php';
use flight\Container;
$container = new Container;
$container->set(PDO::class, fn(): PDO => new PDO('sqlite::memory:'));
Flight::registerContainerHandler([$container, 'get']);
class TestController {
private PDO $pdo;
function __construct(PDO $pdo) {
$this->pdo = $pdo;
}
function index() {
var_dump($this->pdo);
// 정상적으로 출력될 것입니다!
}
}
Flight::route('GET /', [TestController::class, 'index']);
Flight::start();
flightphp/container 고급 사용법
의존성을 재귀적으로 해결할 수도 있습니다. 예시는 다음과 같습니다.
<?php
require 'vendor/autoload.php';
use flight\Container;
class User {}
interface UserRepository {
function find(int $id): ?User;
}
class PdoUserRepository implements UserRepository {
private PDO $pdo;
function __construct(PDO $pdo) {
$this->pdo = $pdo;
}
function find(int $id): ?User {
// 구현 ...
return null;
}
}
$container = new Container;
$container->set(PDO::class, static fn(): PDO => new PDO('sqlite::memory:'));
$container->set(UserRepository::class, PdoUserRepository::class);
$userRepository = $container->get(UserRepository::class);
var_dump($userRepository);
/*
object(PdoUserRepository)#4 (1) {
["pdo":"PdoUserRepository":private]=>
object(PDO)#3 (0) {
}
}
*/
DICE
나만의 DIC 핸들러를 만들 수도 있습니다. PSR-11이 아닌 사용자 정의 컨테이너를 사용하려는 경우 유용합니다 (Dice). 이 작업을 수행하는 방법은 기본 사용법 섹션을 참조하세요.
또한 Flight를 사용할 때 삶을 더 편하게 만들어 주는 몇 가지 유용한 기본값이 있습니다.
Engine 인스턴스 ($app 주입에 필요)
컨트롤러나 미들웨어에서 flight\Engine을 타입 힌트로 사용하는 경우 Dice가 새 Engine을 생성해서는 안 됩니다. 부트스트랩에서 동일한 인스턴스를 대체하세요. 이것이 공식 스켈레톤이 하는 방식이며, AI 생성 컨트롤러를 위해 AGENTS.md가 기대하는 패턴입니다.
// 부트스트랩 또는 services.php의 어딘가
use flight\Engine;
use flight\database\SimplePdo;
$app = Flight::app(); // 또는 $engine = Flight::app();
$container = new \Dice\Dice;
$container = $container->addRule('*', [
'substitutions' => [
// 중요: 부트스트랩된 Engine을 재사용하세요. Dice가 `new Engine()`을 만들지 않도록 하세요.
Engine::class => $app,
// 새 코드에는 SimplePdo를 선호하세요.
// SimplePdo::class => $db,
// Config::class => $config,
// \Twig\Environment::class => $twig,
]
]);
$app->registerContainerHandler(function ($class, $params) use ($container) {
return $container->create($class, $params);
});
// 라우트가 아닌 코드를 위한 선택적 헬퍼
$app->map('make', function ($class, $params = []) use ($container) {
return $container->create($class, $params);
});
// app/Controller/MyController.php (스켈레톤 레이아웃 — 폴더 대소문자는 네임스페이스와 일치)
namespace App\Controller;
use flight\Engine;
class MyController
{
protected Engine $app;
public function __construct(Engine $app)
{
$this->app = $app;
}
public function index(): void
{
// 앱 계층에는 Flight:: 파사드가 없습니다 — 테스트가 쉽고 AI 도구에 더 명확합니다.
$this->app->render('welcome', ['message' => 'Hello']);
}
}
// app/config/routes.php
use App\Controller\MyController;
$router->get('/', [MyController::class, 'index']);
만약 Engine 대체를 생략하면 Dice가 두 번째 Engine을 생성할 수 있으며, 컨트롤러가 부트스트랩의 라우트, 설정, 매핑된 Twig render를 공유하지 못할 수 있습니다.
다른 공유 서비스 추가 (SimplePdo, Config, Twig)
use flight\database\SimplePdo;
use flight\Engine;
// services.php에서 $db, $config, $twig를 생성한 후:
$substitutions = [
Engine::class => $app,
SimplePdo::class => $db,
// App\Utils\Config::class => $config,
// \Twig\Environment::class => $twig,
];
$container = $container->addRule('*', [
'substitutions' => $substitutions,
]);
그러면 컨트롤러는 생성자에서 SimplePdo $db (또는 설정 타입)를 받을 수 있으며 Flight::db()를 호출할 필요가 없습니다. 이는 단위 테스트 지침 및 스켈레톤 하우스 스타일과 일치합니다.
다른 클래스 추가
컨테이너에 추가하려는 다른 클래스가 있다면, Dice에서는 컨테이너가 자동으로 해결해 주기 때문에 쉽습니다. 예시는 다음과 같습니다.
$container = new \Dice\Dice;
// 클래스에 의존성을 주입할 필요가 없다면
// 아무것도 정의할 필요가 없습니다!
Flight::registerContainerHandler(function($class, $params) use ($container) {
return $container->create($class, $params);
});
class MyCustomClass {
public function parseThing() {
return 'thing';
}
}
class UserController {
protected MyCustomClass $MyCustomClass;
public function __construct(MyCustomClass $MyCustomClass) {
$this->MyCustomClass = $MyCustomClass;
}
public function index() {
echo $this->MyCustomClass->parseThing();
}
}
Flight::route('/user', 'UserController->index');
PSR-11
Flight는 모든 PSR-11 호환 컨테이너를 사용할 수 있습니다. 즉, PSR-11 인터페이스를 구현하는 모든 컨테이너를 사용할 수 있습니다. 다음은 League의 PSR-11 컨테이너를 사용하는 예제입니다.
require 'vendor/autoload.php';
use flight\database\SimplePdo;
// 위와 동일한 UserController 아이디어이지만, 원시 PDO 대신 SimplePdo를 타입 힌트로 사용합니다.
$container = new \League\Container\Container();
$container->add(UserController::class)->addArgument(SimplePdo::class);
$container->add(SimplePdo::class)
->addArgument('mysql:host=localhost;dbname=test')
->addArgument('user')
->addArgument('pass');
Flight::registerContainerHandler($container);
Flight::route('/user', [ 'UserController', 'view' ]);
Flight::start();
이전 Dice 예제보다 조금 더 장황할 수 있지만, 여전히 동일한 이점으로 작업을 완수합니다!
참고 항목
- 설치 - 스켈레톤 레이아웃 및
services.php위치. - 자동 로딩 -
App\네임스페이스 및 폴더 대소문자. - Flight 확장 - 프레임워크를 확장하여 자신만의 클래스에 의존성 주입을 추가하는 방법을 알아보세요.
- 구성 - 애플리케이션에 맞게 Flight를 구성하는 방법을 알아보세요.
- 라우팅 - 애플리케이션의 라우트를 정의하는 방법과 의존성 주입이 컨트롤러와 함께 작동하는 방식을 알아보세요.
- 미들웨어 - 애플리케이션용 미들웨어를 만드는 방법과 의존성 주입이 미들웨어와 함께 작동하는 방식을 알아보세요.
- 단위 테스트 - 생성자 주입이
Flight::전역 변수보다 나은 이유. - AI 및 개발자 경험 - 인간과 에이전트를 위한 하나의 DI 패턴.
- SimplePdo - 주입에 선호되는 데이터베이스 헬퍼.
문제 해결
- 컨테이너에 문제가 있는 경우, 컨테이너에 올바른 클래스 이름을 전달하고 있는지 확인하세요.
Engine을 타입 힌트로 사용하지만 "빈" 앱을 받는 컨트롤러: Engine 대체를 추가하세요 (위 참조). Dice는 두 번째 Engine을new로 생성해서는 안 됩니다.App\Controller\…에 대한 클래스를 찾을 수 없는 경우:app/Controller/아래의 폴더 대소문자를 확인하세요 — 자동 로딩 참조.- 핸들러는
registerContainerHandler에서 생성된 객체를 반환해야 합니다 (return없이Flight::make()를 호출하지 마세요).
변경 로그
- 문서 – AI 친화적인 프로젝트를 위한 스켈레톤 Dice + Engine 대체, SimplePdo,
App\Controller레이아웃 문서화. - v3.7.0 - Flight에 DIC 핸들러를 등록하는 기능이 추가되었습니다.
Learn/middleware
미들웨어
개요
Flight는 라우트와 그룹 라우트 미들웨어를 지원합니다. 미들웨어는 라우트 콜백 전에 (또는 후에) 코드가 실행되는 애플리케이션의 일부입니다. 이는 코드에 API 인증 검사를 추가하거나 사용자가 라우트에 접근할 권한이 있는지 확인하는 훌륭한 방법입니다.
이해
미들웨어는 앱을 크게 단순화할 수 있습니다. 복잡한 추상 클래스 상속이나 메서드 오버라이드 대신, 미들웨어를 사용하면 사용자 지정 앱 로직을 라우트에 할당하여 라우트를 제어할 수 있습니다. 미들웨어를 샌드위치처럼 생각할 수 있습니다. 바깥쪽에 빵이 있고, 양상추, 토마토, 고기, 치즈 같은 레이어가 있습니다. 각 요청이 샌드위치를 한 입 베어 물 때 바깥 레이어를 먼저 먹고 핵심으로 들어가는 것처럼 상상해 보세요.
미들웨어가 작동하는 시각적 예시입니다. 그 다음에 이 기능이 어떻게 작동하는지 실용적인 예를 보여드리겠습니다.
사용자 요청 URL /api ---->
Middleware->before() 실행 ----->
/api에 연결된 Callable/메서드 실행 및 응답 생성 ------>
Middleware->after() 실행 ----->
사용자가 서버로부터 응답 받음
그리고 실용적인 예시입니다:
사용자가 URL /dashboard로 이동
LoggedInMiddleware->before() 실행
before()가 유효한 로그인 세션 확인
예: 아무것도 하지 않고 실행 계속
아니오: 사용자를 /login으로 리다이렉트
/api에 연결된 Callable/메서드 실행 및 응답 생성
LoggedInMiddleware->after()가 정의되지 않아 실행 계속
사용자가 서버로부터 대시보드 HTML 받음
실행 순서
미들웨어 함수는 라우트에 추가된 순서대로 실행됩니다. 실행은 Slim Framework가 이를 처리하는 방식과 유사합니다.
before() 메서드는 추가된 순서대로 실행되고, after() 메서드는 역순으로 실행됩니다.
예: Middleware1->before(), Middleware2->before(), Middleware2->after(), Middleware1->after().
기본 사용법
미들웨어를 익명 함수나 클래스(권장) 같은 모든 호출 가능한 메서드로 사용할 수 있습니다.
익명 함수
간단한 예시입니다:
Flight::route('/path', function() { echo ' Here I am!'; })->addMiddleware(function() {
echo 'Middleware first!';
});
Flight::start();
// 이는 "Middleware first! Here I am!"을 출력합니다.
주의: 익명 함수를 사용할 때 해석되는 유일한 메서드는
before()메서드입니다. 익명 클래스와 함께after()동작을 정의할 수 없습니다.
클래스 사용
미들웨어는 클래스(권장)로 등록할 수 있습니다. "after" 기능을 사용하려면 반드시 클래스를 사용해야 합니다.
class MyMiddleware {
public function before($params) {
echo 'Middleware first!';
}
public function after($params) {
echo 'Middleware last!';
}
}
$MyMiddleware = new MyMiddleware();
Flight::route('/path', function() { echo ' Here I am! '; })->addMiddleware($MyMiddleware);
// 또는 ->addMiddleware([ $MyMiddleware, $MyMiddleware2 ]);
Flight::start();
// 이는 "Middleware first! Here I am! Middleware last!"를 표시합니다.
미들웨어 클래스 이름만 정의하고 클래스를 인스턴스화할 수도 있습니다.
Flight::route('/path', function() { echo ' Here I am! '; })->addMiddleware(MyMiddleware::class);
주의: 미들웨어 이름만 전달하면 의존성 주입 컨테이너에 의해 자동으로 실행되며, 미들웨어는 필요한 매개변수로 실행됩니다. 의존성 주입 컨테이너가 등록되지 않은 경우, 기본적으로
__construct(Engine $app)에flight\Engine인스턴스를 전달합니다.
매개변수가 있는 라우트 사용
라우트에서 매개변수가 필요하면, 미들웨어 함수에 단일 배열로 전달됩니다. (function($params) { ... } 또는 public function before($params) { ... }). 이렇게 하는 이유는 매개변수를 그룹으로 구조화할 수 있고, 일부 그룹에서 매개변수가 다른 순서로 나타날 수 있어 잘못된 매개변수를 참조하여 미들웨어 함수가 깨질 수 있기 때문입니다. 이 방법으로 위치 대신 이름으로 접근할 수 있습니다.
use flight\Engine;
class RouteSecurityMiddleware {
protected Engine $app;
public function __construct(Engine $app) {
$this->app = $app;
}
public function before(array $params) {
$clientId = $params['clientId'];
// jobId가 전달될 수도 있고 안 될 수도 있음
$jobId = $params['jobId'] ?? 0;
// job ID가 없으면 조회할 필요가 없을 수 있음.
if($jobId === 0) {
return;
}
// 데이터베이스에서 일종의 조회 수행
$isValid = !!$this->app->db()->fetchField("SELECT 1 FROM client_jobs WHERE client_id = ? AND job_id = ?", [ $clientId, $jobId ]);
if($isValid !== true) {
$this->app->halt(400, 'You are blocked, muahahaha!');
}
}
}
// routes.php
$router->group('/client/@clientId/job/@jobId', function(Router $router) {
// 아래 그룹은 여전히 부모 미들웨어를 받음
// 하지만 매개변수는 미들웨어에서 단일 배열로 전달됨
$router->group('/job/@jobId', function(Router $router) {
$router->get('', [ JobController::class, 'view' ]);
$router->put('', [ JobController::class, 'update' ]);
$router->delete('', [ JobController::class, 'delete' ]);
// 더 많은 라우트...
});
}, [ RouteSecurityMiddleware::class ]);
미들웨어를 사용한 라우트 그룹화
라우트 그룹을 추가하면 그 그룹의 모든 라우트가 동일한 미들웨어를 갖게 됩니다. 헤더의 API 키를 확인하는 Auth 미들웨어로 여러 라우트를 그룹화해야 할 때 유용합니다.
// 그룹 메서드 끝에 추가
Flight::group('/api', function() {
// 이 "빈" 라우트는 실제로 /api와 일치
Flight::route('', function() { echo 'api'; }, false, 'api');
// 이는 /api/users와 일치
Flight::route('/users', function() { echo 'users'; }, false, 'users');
// 이는 /api/users/1234와 일치
Flight::route('/users/@id', function($id) { echo 'user:'.$id; }, false, 'user_view');
}, [ new ApiAuthMiddleware() ]);
모든 라우트에 글로벌 미들웨어를 적용하려면 "빈" 그룹을 추가할 수 있습니다:
// 그룹 메서드 끝에 추가
Flight::group('', function() {
// 이는 여전히 /users
Flight::route('/users', function() { echo 'users'; }, false, 'users');
// 그리고 이는 여전히 /users/1234
Flight::route('/users/@id', function($id) { echo 'user:'.$id; }, false, 'user_view');
}, [ ApiAuthMiddleware::class ]); // 또는 [ new ApiAuthMiddleware() ], 동일
일반적인 사용 사례
API 키 검증
/api 라우트를 보호하고 API 키가 올바른지 확인하려면 미들웨어로 쉽게 처리할 수 있습니다.
use flight\Engine;
class ApiMiddleware {
protected Engine $app;
public function __construct(Engine $app) {
$this->app = $app;
}
public function before(array $params) {
$authorizationHeader = $this->app->request()->getHeader('Authorization');
$apiKey = str_replace('Bearer ', '', $authorizationHeader);
// 데이터베이스에서 api 키 조회
$apiKeyHash = hash('sha256', $apiKey);
$hasValidApiKey = !!$this->db()->fetchField("SELECT 1 FROM api_keys WHERE hash = ? AND valid_date >= NOW()", [ $apiKeyHash ]);
if($hasValidApiKey !== true) {
$this->app->jsonHalt(['error' => 'Invalid API Key']);
}
}
}
// routes.php
$router->group('/api', function(Router $router) {
$router->get('/users', [ ApiController::class, 'getUsers' ]);
$router->get('/companies', [ ApiController::class, 'getCompanies' ]);
// 더 많은 라우트...
}, [ ApiMiddleware::class ]);
이제 모든 API 라우트가 설정한 API 키 검증 미들웨어로 보호됩니다! 라우터 그룹에 더 많은 라우트를 추가하면 즉시 동일한 보호를 받습니다!
로그인 검증
로그인한 사용자만 접근할 수 있는 일부 라우트를 보호하려면? 미들웨어로 쉽게 달성할 수 있습니다!
use flight\Engine;
class LoggedInMiddleware {
protected Engine $app;
public function __construct(Engine $app) {
$this->app = $app;
}
public function before(array $params) {
$session = $this->app->session();
if($session->get('logged_in') !== true) {
$this->app->redirect('/login');
exit;
}
}
}
// routes.php
$router->group('/admin', function(Router $router) {
$router->get('/dashboard', [ DashboardController::class, 'index' ]);
$router->get('/clients', [ ClientController::class, 'index' ]);
// 더 많은 라우트...
}, [ LoggedInMiddleware::class ]);
라우트 매개변수 검증
URL에서 값을 변경하여 접근하지 말아야 할 데이터에 접근하는 것을 사용자에게서 보호하려면? 미들웨어로 해결할 수 있습니다!
use flight\Engine;
class RouteSecurityMiddleware {
protected Engine $app;
public function __construct(Engine $app) {
$this->app = $app;
}
public function before(array $params) {
$clientId = $params['clientId'];
$jobId = $params['jobId'];
// 데이터베이스에서 일종의 조회 수행
$isValid = !!$this->app->db()->fetchField("SELECT 1 FROM client_jobs WHERE client_id = ? AND job_id = ?", [ $clientId, $jobId ]);
if($isValid !== true) {
$this->app->halt(400, 'You are blocked, muahahaha!');
}
}
}
// routes.php
$router->group('/client/@clientId/job/@jobId', function(Router $router) {
$router->get('', [ JobController::class, 'view' ]);
$router->put('', [ JobController::class, 'update' ]);
$router->delete('', [ JobController::class, 'delete' ]);
// 더 많은 라우트...
}, [ RouteSecurityMiddleware::class ]);
미들웨어 실행 처리
인증 미들웨어가 있고 사용자가 인증되지 않으면 로그인 페이지로 리다이렉트하려고 가정해 보세요. 몇 가지 옵션이 있습니다:
- 미들웨어 함수에서 false를 반환하면 Flight가 자동으로 403 Forbidden 오류를 반환하지만, 사용자 지정이 없습니다.
Flight::redirect()를 사용하여 사용자를 로그인 페이지로 리다이렉트할 수 있습니다.- 미들웨어 내에서 사용자 지정 오류를 생성하고 라우트 실행을 중단할 수 있습니다.
간단하고 직관적
간단한 return false; 예시입니다:
class MyMiddleware {
public function before($params) {
$hasUserKey = Flight::session()->exists('user');
if ($hasUserKey === false) {
return false;
}
// true이므로 모든 것이 계속 진행됨
}
}
리다이렉트 예시
사용자를 로그인 페이지로 리다이렉트하는 예시입니다:
class MyMiddleware {
public function before($params) {
$hasUserKey = Flight::session()->exists('user');
if ($hasUserKey === false) {
Flight::redirect('/login');
exit;
}
}
}
사용자 지정 오류 예시
API를 구축 중이므로 JSON 오류를 발생시켜야 한다고 가정해 보세요. 이렇게 할 수 있습니다:
class MyMiddleware {
public function before($params) {
$authorization = Flight::request()->getHeader('Authorization');
if(empty($authorization)) {
Flight::jsonHalt(['error' => 'You must be logged in to access this page.'], 403);
// 또는
Flight::json(['error' => 'You must be logged in to access this page.'], 403);
exit;
// 또는
Flight::halt(403, json_encode(['error' => 'You must be logged in to access this page.']);
}
}
}
관련 항목
- 라우팅 - 라우트를 컨트롤러에 매핑하고 뷰를 렌더링하는 방법.
- 요청 - 들어오는 요청 처리 방법 이해.
- 응답 - HTTP 응답 사용자 지정 방법.
- 의존성 주입 - 라우트에서 객체 생성 및 관리 단순화.
- 프레임워크를 왜 사용할까? - Flight 같은 프레임워크 사용 이점 이해.
- 미들웨어 실행 전략 예시
문제 해결
- 미들웨어에 리다이렉트가 있지만 앱이 리다이렉트되지 않는 것 같으면, 미들웨어에
exit;문을 추가했는지 확인하세요.
변경 로그
- v3.1: 미들웨어 지원 추가.
Learn/filtering
필터링
개요
Flight는 매핑된 메서드가 호출되기 전과 후에 필터링할 수 있도록 합니다.
이해하기
기억해야 할 미리 정의된 훅은 없습니다. 프레임워크의 기본 메서드와 매핑한 사용자 지정 메서드 모두를 필터링할 수 있습니다.
필터 함수는 다음과 같습니다:
/**
* @param array $params 필터링되는 메서드에 전달된 매개변수.
* @param string $output (v2 출력 버퍼링 전용) 필터링되는 메서드의 출력.
* @return bool 체인을 계속하려면 true/void를 반환하거나 반환하지 말고, 체인을 중단하려면 false를 반환합니다.
*/
function (array &$params, string &$output): bool {
// 필터 코드
}
전달된 변수를 사용하여 입력 매개변수와/또는 출력을 조작할 수 있습니다.
메서드 전에 필터를 실행하려면 다음과 같이 합니다:
Flight::before('start', function (array &$params, string &$output): bool {
// 무언가 수행
});
메서드 후에 필터를 실행하려면 다음과 같이 합니다:
Flight::after('start', function (array &$params, string &$output): bool {
// 무언가 수행
});
원하는 만큼의 필터를 어떤 메서드에도 추가할 수 있습니다. 필터는 선언된 순서대로 호출됩니다.
필터링 프로세스의 예는 다음과 같습니다:
// 사용자 지정 메서드 매핑
Flight::map('hello', function (string $name) {
return "Hello, $name!";
});
// before 필터 추가
Flight::before('hello', function (array &$params, string &$output): bool {
// 매개변수 조작
$params[0] = 'Fred';
return true;
});
// after 필터 추가
Flight::after('hello', function (array &$params, string &$output): bool {
// 출력 조작
$output .= " Have a nice day!";
return true;
});
// 사용자 지정 메서드 호출
echo Flight::hello('Bob');
이것은 다음과 같이 표시되어야 합니다:
Hello Fred! Have a nice day!
여러 필터를 정의한 경우, 필터 함수 중 하나에서 false를 반환하여 체인을 중단할 수 있습니다:
Flight::before('start', function (array &$params, string &$output): bool {
echo 'one';
return true;
});
Flight::before('start', function (array &$params, string &$output): bool {
echo 'two';
// 이것은 체인을 종료합니다
return false;
});
// 이것은 호출되지 않습니다
Flight::before('start', function (array &$params, string &$output): bool {
echo 'three';
return true;
});
참고:
map및register와 같은 핵심 메서드는 직접 호출되며 동적으로 호출되지 않기 때문에 필터링할 수 없습니다. 자세한 내용은 Extending Flight를 참조하세요.
관련 항목
문제 해결
- 체인을 중지하려면 필터 함수에서
false를 반환하는지 확인하세요. 아무것도 반환하지 않으면 체인이 계속됩니다.
변경 로그
- v2.0 - 초기 릴리스.
Learn/requests
요청
개요
Flight는 HTTP 요청을 단일 객체로 캡슐화하며, 이를 다음과 같이 접근할 수 있습니다:
$request = Flight::request();
이해
HTTP 요청은 HTTP 수명 주기를 이해하는 데 핵심적인 측면 중 하나입니다. 사용자가 웹 브라우저나 HTTP 클라이언트에서 작업을 수행하면, 헤더, 본문, URL 등을 프로젝트로 보냅니다. 이러한 헤더(브라우저 언어, 처리할 수 있는 압축 유형, 사용자 에이전트 등)를 캡처하고, Flight 애플리케이션으로 전송된 본문과 URL을 캡처할 수 있습니다. 이러한 요청은 앱이 다음에 무엇을 할지 이해하는 데 필수적입니다.
기본 사용법
PHP에는 $_GET, $_POST, $_REQUEST, $_SERVER, $_FILES, $_COOKIE와 같은 여러 슈퍼 글로벌이 있습니다. Flight는 이를 편리한 Collections로 추상화합니다. query, data, cookies, files 속성을 배열 또는 객체로 접근할 수 있습니다.
참고: 프로젝트에서 이러한 슈퍼 글로벌을 사용하는 것은 강력히 권장되지 않으며,
request()객체를 통해 참조해야 합니다.
참고:
$_ENV에 대한 추상화는 제공되지 않습니다.
$_GET
query 속성을 통해 $_GET 배열에 접근할 수 있습니다:
// GET /search?keyword=something
Flight::route('/search', function(){
$keyword = Flight::request()->query['keyword'];
// 또는
$keyword = Flight::request()->query->keyword;
echo "You are searching for: $keyword";
// $keyword로 데이터베이스 쿼리 또는 다른 작업 수행
});
$_POST
data 속성을 통해 $_POST 배열에 접근할 수 있습니다:
Flight::route('POST /submit', function(){
$name = Flight::request()->data['name'];
$email = Flight::request()->data['email'];
// 또는
$name = Flight::request()->data->name;
$email = Flight::request()->data->email;
echo "You submitted: $name, $email";
// $name과 $email로 데이터베이스 저장 또는 다른 작업 수행
});
$_COOKIE
cookies 속성을 통해 $_COOKIE 배열에 접근할 수 있습니다:
Flight::route('GET /login', function(){
$savedLogin = Flight::request()->cookies['myLoginCookie'];
// 또는
$savedLogin = Flight::request()->cookies->myLoginCookie;
// 실제로 저장되었는지 확인하고, 저장되었다면 자동 로그인
if($savedLogin) {
Flight::redirect('/dashboard');
return;
}
});
새 쿠키 값 설정에 대한 도움은 overclokk/cookie를 참조하세요.
$_SERVER
getVar() 메서드를 통해 $_SERVER 배열에 접근하는 단축 방법이 있습니다:
$host = Flight::request()->getVar('HTTP_HOST');
$_FILES
files 속성을 통해 업로드된 파일에 접근할 수 있습니다:
// $_FILES 속성에 대한 직접 접근. 권장 접근법은 아래 참조
$uploadedFile = Flight::request()->files['myFile'];
// 또는
$uploadedFile = Flight::request()->files->myFile;
더 많은 정보는 Uploaded File Handler를 참조하세요.
파일 업로드 처리
v3.12.0
프레임워크의 도우미 메서드를 사용하여 파일 업로드를 처리할 수 있습니다. 기본적으로 요청에서 파일 데이터를 가져와 새 위치로 이동하는 것입니다.
Flight::route('POST /upload', function(){
// <input type="file" name="myFile">와 같은 입력 필드가 있는 경우
$uploadedFileData = Flight::request()->getUploadedFiles();
$uploadedFile = $uploadedFileData['myFile'];
$uploadedFile->moveTo('/path/to/uploads/' . $uploadedFile->getClientFilename());
});
여러 파일이 업로드된 경우, 이를 반복할 수 있습니다:
Flight::route('POST /upload', function(){
// <input type="file" name="myFiles[]">와 같은 입력 필드가 있는 경우
$uploadedFiles = Flight::request()->getUploadedFiles()['myFiles'];
foreach ($uploadedFiles as $uploadedFile) {
$uploadedFile->moveTo('/path/to/uploads/' . $uploadedFile->getClientFilename());
}
});
보안 참고: 파일 업로드를 다룰 때는 항상 사용자 입력을 검증하고 정제하세요. 업로드할 허용 확장 유형을 검증하는 것뿐만 아니라, 파일의 "매직 바이트"를 검증하여 사용자가 주장하는 파일 유형인지 확인해야 합니다. 이에 대한 기사 및 라이브러리가 도움이 됩니다.
요청 본문
POST/PUT 요청을 다룰 때와 같은 원시 HTTP 요청 본문을 가져오려면 다음을 할 수 있습니다:
Flight::route('POST /users/xml', function(){
$xmlBody = Flight::request()->getBody();
// 전송된 XML로 작업 수행.
});
JSON 본문
콘텐츠 유형이 application/json이고 예시 데이터가 {"id": 123}인 요청을 받으면, data 속성에서 사용할 수 있습니다:
$id = Flight::request()->data->id;
요청 헤더
getHeader() 또는 getHeaders() 메서드를 사용하여 요청 헤더에 접근할 수 있습니다:
// Authorization 헤더가 필요한 경우
$host = Flight::request()->getHeader('Authorization');
// 또는
$host = Flight::request()->header('Authorization');
// 모든 헤더를 가져와야 하는 경우
$headers = Flight::request()->getHeaders();
// 또는
$headers = Flight::request()->headers();
요청 메서드
method 속성 또는 getMethod() 메서드를 사용하여 요청 메서드에 접근할 수 있습니다:
$method = Flight::request()->method; // 실제로는 getMethod()로 채워짐
$method = Flight::request()->getMethod();
참고: getMethod() 메서드는 먼저 $_SERVER['REQUEST_METHOD']에서 메서드를 가져온 다음, $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE']가 존재하면 이를 덮어쓰거나 $_REQUEST['_method']가 존재하면 이를 사용합니다.
요청 객체 속성
요청 객체는 다음 속성을 제공합니다:
- body - 원시 HTTP 요청 본문
- url - 요청된 URL
- base - URL의 상위 서브디렉토리
- method - 요청 메서드 (GET, POST, PUT, DELETE)
- referrer - 참조 URL
- ip - 클라이언트 IP 주소
- ajax - AJAX 요청인지 여부
- scheme - 서버 프로토콜 (http, https)
- user_agent - 브라우저 정보
- type - 콘텐츠 유형
- length - 콘텐츠 길이
- query - 쿼리 문자열 매개변수
- data - POST 데이터 또는 JSON 데이터
- cookies - 쿠키 데이터
- files - 업로드된 파일
- secure - 연결이 보안인지 여부
- accept - HTTP accept 매개변수
- proxy_ip - 클라이언트의 프록시 IP 주소.
$_SERVER배열에서HTTP_CLIENT_IP,HTTP_X_FORWARDED_FOR,HTTP_X_FORWARDED,HTTP_X_CLUSTER_CLIENT_IP,HTTP_FORWARDED_FOR,HTTP_FORWARDED를 순서대로 스캔합니다. - host - 요청 호스트 이름
- servername -
$_SERVER의 SERVER_NAME
도우미 메서드
URL의 일부를 조합하거나 특정 헤더를 다루기 위한 몇 가지 도우미 메서드가 있습니다.
전체 URL
getFullUrl() 메서드를 사용하여 전체 요청 URL에 접근할 수 있습니다:
$url = Flight::request()->getFullUrl();
// https://example.com/some/path?foo=bar
기본 URL
getBaseUrl() 메서드를 사용하여 기본 URL에 접근할 수 있습니다:
// http://example.com/path/to/something/cool?query=yes+thanks
$url = Flight::request()->getBaseUrl();
// https://example.com
// 주의: 후행 슬래시 없음.
쿼리 파싱
parseQuery() 메서드에 URL을 전달하여 쿼리 문자열을 연관 배열로 파싱할 수 있습니다:
$query = Flight::request()->parseQuery('https://example.com/some/path?foo=bar');
// ['foo' => 'bar']
콘텐츠 Accept 유형 협상
v3.17.2
클라이언트가 보낸 Accept 헤더를 기반으로 응답할 최적의 콘텐츠 유형을 결정하기 위해 negotiateContentType() 메서드를 사용할 수 있습니다.
// 예시 Accept 헤더: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
// 아래는 지원하는 유형을 정의합니다.
$availableTypes = ['application/json', 'application/xml'];
$typeToServe = Flight::request()->negotiateContentType($availableTypes);
if ($typeToServe === 'application/json') {
// JSON 응답 제공
} elseif ($typeToServe === 'application/xml') {
// XML 응답 제공
} else {
// 다른 기본값 사용 또는 오류 발생
}
참고:
Accept헤더에 사용 가능한 유형 중 하나가 없으면 메서드는null을 반환합니다.Accept헤더가 정의되지 않은 경우, 메서드는$availableTypes배열의 첫 번째 유형을 반환합니다.
관련 자료
- Routing - 라우트를 컨트롤러에 매핑하고 뷰를 렌더링하는 방법.
- Responses - HTTP 응답을 사용자 지정하는 방법.
- Why a Framework? - 요청이 전체 그림에 어떻게 맞는지.
- Collections - 데이터 컬렉션 작업.
- Uploaded File Handler - 파일 업로드 처리.
문제 해결
request()->ip와request()->proxy_ip는 웹 서버가 프록시, 로드 밸런서 등 뒤에 있는 경우 다를 수 있습니다.
변경 로그
- v3.17.2 - negotiateContentType() 추가
- v3.12.0 - 요청 객체를 통해 파일 업로드 처리 기능 추가.
- v1.0 - 초기 릴리스.
Learn/why_frameworks
프레임워크를 사용하는 이유?
일부 프로그래머는 프레임워크를 사용하는 것에 열렬히 반대합니다. 프레임워크가 부풀려지고 느리며 배우기 어렵다고 주장합니다. 그들은 프레임워크가 불필요하며 그들 없이 더 나은 코드를 작성할 수 있다고 말합니다. 프레임워크를 사용하는 단점에 대해 몇 가지 타당한 이유가 있습니다. 그러나 프레임워크를 사용하는 장점도 많이 있습니다.
프레임워크를 사용하는 이유
프레임워크를 사용해야 하는 몇 가지 이유는 다음과 같습니다:
- 빠른 개발: 프레임워크는 많은 기능을 제공합니다. 이는 웹 애플리케이션을 빠르게 구축할 수 있다는 것을 의미합니다. 프레임워크가 필요한 기능을 많이 제공하기 때문에 많은 코드를 작성할 필요가 없습니다.
- 일관성: 프레임워크는 일관된 방식으로 작업하는 방법을 제공합니다. 이는 코드가 작동하는 방식을 이해하기 쉽게 만들고 다른 개발자가 코드를 이해하기 쉽게 만듭니다. 팀 개발자와 함께 작업하는 경우 스크립트별로 작업하는 경우 스크립트 간의 일관성을 잃을 수 있습니다.
- 보안: 프레임워크는 보안 기능을 제공하여 일반적인 보안 위협으로부터 웹 애플리케이션을 보호하는 데 도움을 줍니다. 이는 프레임워크가 보안에 대해 크게 걱정할 필요가 없다는 것을 의미합니다. 프레임워크가 많은 부분을 처리하기 때문입니다.
- 커뮤니티: 프레임워크에는 프레임워크에 기여하는 개발자들의 큰 커뮤니티가 있습니다. 이는 다른 개발자들이 질문이나 문제가 있을 때 다른 개발자로부터 도움을 받을 수 있다는 것을 의미합니다. 또한 프레임워크 사용 방법을 배우는 데 도움이 되는 많은 리소스가 있다는 것을 의미합니다.
- 최선의 방법: 프레임워크는 최상의 방법으로 구축됩니다. 이는 프레임워크에서 배울 수 있고 자신의 코드에서 동일한 최상의 방법을 사용할 수 있다는 것을 의미합니다. 이는 당신을 더 나은 프로그래머로 만들 수 있습니다. 때로는 알지 못하는 것이 있을 수 있으며 그것이 마지막에 당신을 과중하게 할 수 있습니다.
- 확장성: 프레임워크는 확장할 수 있도록 설계되었습니다. 이는 프레임워크에 자신의 기능을 추가할 수 있다는 것을 의미합니다. 이를 통해 당신은 특정한 필요에 맞는 웹 애플리케이션을 구축할 수 있습니다.
Flight은 일종의 마이크로 프레임워크입니다. 이는 작고 가벼워서 많은 기능을 제공하지 않습니다. Laravel이나 Symfony와 같은 큰 프레임워크만큼의 기능을 제공하지는 않습니다. 그러나 웹 애플리케이션을 구축하는 데 필요한 많은 기능을 제공합니다. 또한 학습과 사용이 쉽습니다. 이는 빠르고 쉽게 웹 애플리케이션을 구축하는 좋은 선택입니다. 프레임워크가 처음이라면, Flight는 시작하기에 좋은 초보자 프레임워크입니다. 프레임워크를 사용하는 장점에 대해 배우며 복잡성이 너무 많지 않도록 도와줍니다. Flight 경험 후에는 Laravel이나 Symfony와 같이 더 복잡한 프레임워크로 전환하기가 쉬워질 것입니다만, 여전히 Flight로도 강력하고 견고한 애플리케이션을 만들 수 있습니다.
라우팅이란 무엇인가?
라우팅은 Flight 프레임워크의 핵심이지만 정확히 무엇일까요? 라우팅은 URL을 가져와 코드에서 특정 함수와 일치시키는 과정입니다. 요청된 URL에 따라 웹사이트가 다른 작업을 수행할 수 있습니다. 예를 들어, '/user/1234'를 방문하면 사용자 프로필을 보여줄 수 있지만 '/users'를 방문하면 모든 사용자 목록을 표시할지도 모릅니다. 모두 라우팅을 통해 이루어집니다.
이것은 다음과 같이 작동할 수 있습니다:
- 사용자가 브라우저를 열고
http://example.com/user/1234를 입력합니다. - 서버가 요청을 받아 URL을 확인하고 Flight 응용 프로그램 코드로 전달합니다.
- 예를 들어 Flight 코드에
Flight::route('/user/@id', [ 'UserController', 'viewUserProfile' ]);와 같은 것이 있다고 가정해 봅시다. Flight 응용 프로그램 코드는 URL을 확인하고 정의한 경로와 일치하는 것을 확인한 다음 해당 경로에 대해 정의한 코드를 실행합니다. - 그런 다음 Flight 라우터는 'UserController' 클래스의 'viewUserProfile($id)' 메서드를 호출하여 메서드 내의
$id인수로1234를 전달합니다. viewUserProfile()메서드의 코드가 실행되어 코드를 실행하고 사용자 프로필 페이지를 위한 HTML을 출력할 수 있으며, 이것이 RESTful API인 경우 사용자 정보와 함께 JSON 응답을 출력할 수도 있습니다.- Flight는 깔끔하게 감싸고 응답 헤더를 생성하여 사용자 브라우저로 보내줍니다.
- 사용자는 기쁨에 충만하고 따뜻한 포옹을 합니다!
그리고 왜 중요한가요?
적절한 중앙 집중형 라우터를 가지고 있다면 실제로 여러분의 삶이 크게 쉬워질 수 있습니다! 처음에는 약간 복잡해 보일 수도 있습니다. 다음은 몇 가지 이유입니다:
- 중앙 집중식 라우팅: 모든 라우트 정보를 한 곳에 유지할 수 있습니다. 어떤 라우트가 있고 그들이 무엇을 하는지 보는 것이 쉽습니다. 필요한 경우 라우트를 쉽게 변경할 수 있습니다.
- 라우트 매개변수: 라우트 매개변수를 사용하여 데이터를 라우트 메서드로 전달할 수 있습니다. 이것은 코드를 깨끗하고 정리된 상태로 유지하는 좋은 방법입니다.
- 라우트 그룹: 라우트를 그룹화할 수 있습니다. 코드를 정리하고 middleware를 라우트 그룹에 적용하는 데 유용합니다.
- 라우트 별칭: 라우트에 별칭을 지정하여 URL을 동적으로 생성할 수 있습니다. 예를 들어, 코드에서 '/user/1234'를 직접 코딩하는 대신 'user_view'와 같은 별칭을 참조하고 매개변수로 'id'를 전달할 수 있습니다. 나중에 '/user/1234'를 '/admin/user/1234'로 변경해야 하는 경우 모든 하드 코딩된 URL을 변경할 필요가 없습니다. 라우트에 연결된 URL만 변경하면 되므로 훌륭합니다.
- 라우트 미들웨어: 라우트에 미들웨어를 추가할 수 있습니다. 미들웨어는 특정 동작을 추가하는 데 매우 강력합니다. 예를 들어 특정 사용자가 라우트나 라우트 그룹에 액세스할 수 있는지 작업을 인증하는 데 사용됩니다.
아마도 'index.php'에 여러분의 웹사이트를 만드는 스크립트별 방법에 익숙하실 것입니다. 'index.php'라는 파일이 있고 URL을 확인하고 URL에 따라 특정 함수를 실행하는 많은 'if' 문이 있는 것일 것입니다. 이것은 라우팅 방식 중 하나일 수 있지만 조직화되지 않았으며 빠르게 복잡해질 수 있습니다. Flight의 라우팅 시스템은 라우팅을 처리하는 더 정리되고 강력한 방법입니다.
이렇게 하시겠습니까?
// /user/view_profile.php?id=1234
if ($_GET['id']) {
$id = $_GET['id'];
viewUserProfile($id);
}
// /user/edit_profile.php?id=1234
if ($_GET['id']) {
$id = $_GET['id'];
editUserProfile($id);
}
// etc...
아니면 이렇게 하시겠습니까?
// index.php
Flight::route('/user/@id', [ 'UserController', 'viewUserProfile' ]);
Flight::route('/user/@id/edit', [ 'UserController', 'editUserProfile' ]);
// 아마도 여러분의 app/controllers/UserController.php에서
class UserController {
public function viewUserProfile($id) {
// 무언가 수행
}
public function editUserProfile($id) {
// 무언가 수행
}
}
중앙 집중식 라우팅 시스템을 사용하는 이점을 보시기 바랍니다. 나중에 오랜 기간동안 관리하고 이해하기 쉽습니다!
요청과 응답
Flight는 요청과 응답을 처리하는 간단하고 쉬운 방법을 제공합니다. 이것이 웹 프레임워크의 핵심입니다. 사용자 브라우저에서 요청을 받아들이고, 처리한 다음 사용자에게 응답을 보냅니다. 사용자 프로필 표시, 사용자 로그인 허용 또는 사용자가 새 블로그 글을 게시할 수 있도록 하는 웹 애플리케이션을 구축할 수 있습니다.
요청
요청은 사용자 브라우저가 여러분의 웹사이트를 방문할 때 서버로 보내는 것입니다. 이 요청에는 사용자가 할 작업에 대한 정보가 포함되어 있습니다. 예를 들어, 사용자가 방문하려는 URL, 사용자가 서버로 보내려는 데이터 및 서버에서 사용자가 받기 원하는 데이터에 대한 정보가 포함될 수 있습니다. 이 요청은 읽기 전용임을 알려드립니다. 요청을 변경할 수는 없지만 읽을 수는 있습니다.
Flight은 요청에 관한 정보에 액세스할 수 있는 간단한 방법을 제공합니다. Flight::request() 메서드를 사용하여 요청에 관한 정보에 액세스할 수 있습니다. 이 메서드는 요청에 관한 정보를 포함한 Request 객체를 반환합니다. 이 객체를 사용하여 요청에 관한 정보에 액세스할 수 있으며, URL, 메서드 또는 사용자가 서버에게 보낸 데이터와 같은 정보를 액세스할 수 있습니다.
응답
응답은 사용자 브라우저가 여러분의 웹사이트를 방문할 때 여러분의 서버가 사용자 브라우저로 보내는 것입니다. 이 응답에는 서버가 하려는 작업에 대한 정보가 포함되어 있습니다. 예를 들어, 사용자에게 보내기 원하는 데이터, 사용자로부터 받기 원하는 데이터 또는 서버가 사용자의 컴퓨터에 저장하길 원하는 데이터에 대한 정보가 포함될 수 있습니다.
Flight는 사용자 브라우저로 응답을 보내는 간단한 방법을 제공합니다. Flight::response() 메서드를 사용하여 응답을 전송할 수 있습니다. 이 메서드는 Response 객체를 매개변수로 받아 사용자 브라우저로 응답을 전송합니다. 이 객체를 사용하여 사용자 브라우저로 응답을 보낼 수 있으며, HTML, JSON 또는 파일과 같은 응답을 보낼 수 있습니다. Flight는 응답의 일부를 자동으로 생성하여 작업을 쉽게 만들어줍니다. 그러나 최종적으로 사용자에게 반환할 것을 완전히 제어할 수 있습니다.
Learn/responses
응답
개요
Flight는 응답 헤더의 일부를 생성하는 데 도움을 주지만, 사용자에게 보낼 내용에 대한 대부분의 제어를 유지합니다. 대부분의 경우 response() 객체에 직접 접근하지만, Flight는 일부 응답 헤더를 설정하는 데 도움을 주는 헬퍼 메서드를 제공합니다.
이해하기
사용자가 요청 요청을 애플리케이션에 보낸 후, 적절한 응답을 생성해야 합니다. 그들은 선호하는 언어, 특정 유형의 압축을 처리할 수 있는지, 사용자 에이전트 등과 같은 정보를 보냈으며, 모든 것을 처리한 후 적절한 응답을 보내는 시간입니다. 이는 헤더 설정, HTML 또는 JSON 본문 출력, 또는 페이지로 리디렉션하는 등의 작업일 수 있습니다.
기본 사용법
응답 본문 보내기
Flight는 출력을 버퍼링하기 위해 ob_start()를 사용합니다. 이는 echo 또는 print를 사용하여 사용자에게 응답을 보낼 수 있으며, Flight가 이를 캡처하여 적절한 헤더와 함께 사용자에게 보낸다는 의미입니다.
// 이는 "Hello, World!"를 사용자의 브라우저로 보냅니다
Flight::route('/', function() {
echo "Hello, World!";
});
// HTTP/1.1 200 OK
// Content-Type: text/html
//
// Hello, World!
대안으로, write() 메서드를 호출하여 본문에 추가할 수도 있습니다.
// 이는 "Hello, World!"를 사용자의 브라우저로 보냅니다
Flight::route('/', function() {
// 때때로 필요할 때 작업을 수행하지만 장황합니다
Flight::response()->write("Hello, World!");
// 이 시점에서 설정한 본문을 검색하려면
// 이렇게 할 수 있습니다
$body = Flight::response()->getBody();
});
JSON
Flight는 JSON 및 JSONP 응답을 보내는 지원을 제공합니다. JSON 응답을 보내기 위해 JSON 인코딩할 데이터를 전달합니다:
Flight::route('/@companyId/users', function(int $companyId) {
// 예를 들어 데이터베이스에서 사용자를 가져오는 등의 작업
$users = Flight::db()->fetchAll("SELECT id, first_name, last_name FROM users WHERE company_id = ?", [ $companyId ]);
Flight::json($users);
});
// [{"id":1,"first_name":"Bob","last_name":"Jones"}, /* more users */ ]
참고: 기본적으로 Flight는 응답과 함께
Content-Type: application/json헤더를 보냅니다. JSON을 인코딩할 때JSON_THROW_ON_ERROR및JSON_UNESCAPED_SLASHES플래그도 사용합니다.
상태 코드와 함께 JSON
두 번째 인수로 상태 코드를 전달할 수도 있습니다:
Flight::json(['id' => 123], 201);
예쁘게 출력하는 JSON
마지막 위치에 인수를 전달하여 예쁘게 출력할 수도 있습니다:
Flight::json(['id' => 123], 200, true, 'utf-8', JSON_PRETTY_PRINT);
JSON 인수 순서 변경
Flight::json()은 매우 오래된 메서드이지만, Flight의 목표는 프로젝트의 이전 호환성을 유지하는 것입니다. 인수의 순서를 다시 설정하여 더 간단한 구문을 사용하려면, 다른 Flight 메서드처럼 JSON 메서드를 재매핑할 수 있습니다:
Flight::map('json', function($data, $code = 200, $options = 0) {
// 이제 json() 메서드를 사용할 때 `true, 'utf-8'`를 할 필요가 없습니다!
Flight::_json($data, $code, true, 'utf-8', $options);
}
// 이제 이렇게 사용할 수 있습니다
Flight::json(['id' => 123], 200, JSON_PRETTY_PRINT);
JSON과 실행 중지
v3.10.0
JSON 응답을 보내고 실행을 중지하려면 jsonHalt() 메서드를 사용할 수 있습니다. 이는 권한 부여를 확인하는 등의 경우에 유용하며, 사용자가 권한이 없으면 JSON 응답을 즉시 보내고 기존 본문 내용을 지우고 실행을 중지할 수 있습니다.
Flight::route('/users', function() {
$authorized = someAuthorizationCheck();
// 사용자가 권한이 있는지 확인
if($authorized === false) {
Flight::jsonHalt(['error' => 'Unauthorized'], 401);
// 여기서는 exit;가 필요 없습니다.
}
// 나머지 라우트로 계속 진행
});
v3.10.0 이전에는 다음과 같이 해야 했습니다:
Flight::route('/users', function() {
$authorized = someAuthorizationCheck();
// 사용자가 권한이 있는지 확인
if($authorized === false) {
Flight::halt(401, json_encode(['error' => 'Unauthorized']));
}
// 나머지 라우트로 계속 진행
});
응답 본문 지우기
응답 본문을 지우려면 clearBody 메서드를 사용할 수 있습니다:
Flight::route('/', function() {
if($someCondition) {
Flight::response()->write("Hello, World!");
} else {
Flight::response()->clearBody();
}
});
위 사용 사례는 흔하지 않지만, 미들웨어에서 사용된다면 더 흔할 수 있습니다.
응답 본문에 콜백 실행
addResponseBodyCallback 메서드를 사용하여 응답 본문에 콜백을 실행할 수 있습니다:
Flight::route('/users', function() {
$db = Flight::db();
$users = $db->fetchAll("SELECT * FROM users");
Flight::render('users_table', ['users' => $users]);
});
// 이는 모든 라우트의 응답을 gzip으로 압축합니다
Flight::response()->addResponseBodyCallback(function($body) {
return gzencode($body, 9);
});
여러 콜백을 추가할 수 있으며, 추가된 순서대로 실행됩니다. 이는 callable을 허용하므로 클래스 배열 [ $class, 'method' ], 클로저 $strReplace = function($body) { str_replace('hi', 'there', $body); };, 또는 HTML 코드를 압축하는 함수 이름 'minify' 등을 허용합니다.
참고: flight.v2.output_buffering 구성 옵션을 사용 중이라면 라우트 콜백이 작동하지 않습니다.
특정 라우트 콜백
이것을 특정 라우트에만 적용하려면 라우트 자체에 콜백을 추가할 수 있습니다:
Flight::route('/users', function() {
$db = Flight::db();
$users = $db->fetchAll("SELECT * FROM users");
Flight::render('users_table', ['users' => $users]);
// 이는 이 라우트의 응답만 gzip으로 압축합니다
Flight::response()->addResponseBodyCallback(function($body) {
return gzencode($body, 9);
});
});
미들웨어 옵션
미들웨어를 사용하여 모든 라우트에 콜백을 적용할 수도 있습니다:
// MinifyMiddleware.php
class MinifyMiddleware {
public function before() {
// response() 객체에 여기서 콜백을 적용합니다.
Flight::response()->addResponseBodyCallback(function($body) {
return $this->minify($body);
});
}
protected function minify(string $body): string {
// 본문을 압축하는 등의 작업
return $body;
}
}
// index.php
Flight::group('/users', function() {
Flight::route('', function() { /* ... */ });
Flight::route('/@id', function($id) { /* ... */ });
}, [ new MinifyMiddleware() ]);
상태 코드
status 메서드를 사용하여 응답의 상태 코드를 설정할 수 있습니다:
Flight::route('/@id', function($id) {
if($id == 123) {
Flight::response()->status(200);
echo "Hello, World!";
} else {
Flight::response()->status(403);
echo "Forbidden";
}
});
현재 상태 코드를 가져오려면 인수 없이 status 메서드를 사용할 수 있습니다:
Flight::response()->status(); // 200
응답 헤더 설정
header 메서드를 사용하여 응답의 콘텐츠 유형 등의 헤더를 설정할 수 있습니다:
// 이는 "Hello, World!"를 사용자의 브라우저에 일반 텍스트로 보냅니다
Flight::route('/', function() {
Flight::response()->header('Content-Type', 'text/plain');
// 또는
Flight::response()->setHeader('Content-Type', 'text/plain');
echo "Hello, World!";
});
리디렉션
redirect() 메서드를 사용하여 현재 요청을 리디렉션할 수 있으며, 새로운 URL을 전달합니다:
Flight::route('/login', function() {
$username = Flight::request()->data->username;
$password = Flight::request()->data->password;
$passwordConfirm = Flight::request()->data->password_confirm;
if($password !== $passwordConfirm) {
Flight::redirect('/new/location');
return; // 아래 기능이 실행되지 않도록 필요합니다
}
// 새 사용자 추가...
Flight::db()->runQuery("INSERT INTO users ....");
Flight::redirect('/admin/dashboard');
});
참고: 기본적으로 Flight는 HTTP 303 ("See Other") 상태 코드를 보냅니다. 선택적으로 사용자 지정 코드를 설정할 수 있습니다:
Flight::redirect('/new/location', 301); // 영구
라우트 실행 중지
halt 메서드를 호출하여 프레임워크를 중지하고 즉시 종료할 수 있습니다:
Flight::halt();
선택적으로 HTTP 상태 코드와 메시지를 지정할 수도 있습니다:
Flight::halt(200, 'Be right back...');
halt를 호출하면 그 시점까지의 응답 내용을 버리고 모든 실행을 중지합니다. 프레임워크를 중지하고 현재 응답을 출력하려면 stop 메서드를 사용합니다:
Flight::stop($httpStatusCode = null);
참고:
Flight::stop()은 응답을 출력하지만 스크립트 실행을 계속하므로 원하는 결과가 아닐 수 있습니다. 추가 실행을 방지하기 위해Flight::stop()호출 후exit또는return을 사용할 수 있지만, 일반적으로Flight::halt()를 사용하는 것이 권장됩니다.
이는 헤더 키와 값을 응답 객체에 저장합니다. 요청 수명 주기 끝에서 헤더를 빌드하고 응답을 보냅니다.
고급 사용법
즉시 헤더 보내기
헤더에 사용자 지정 작업을 수행해야 하며 작업 중인 코드 라인에서 헤더를 보내야 하는 경우가 있을 수 있습니다. 스트리밍 라우트를 설정 중이라면 이것이 필요합니다. 이는 response()->setRealHeader()를 통해 달성할 수 있습니다.
Flight::route('/', function() {
Flight::response()->setRealHeader('Content-Type: text/plain');
echo 'Streaming response...';
sleep(5);
echo 'Done!';
})->stream();
JSONP
JSONP 요청의 경우, 콜백 함수를 정의하는 데 사용하는 쿼리 매개변수 이름을 선택적으로 전달할 수 있습니다:
Flight::jsonp(['id' => 123], 'q');
따라서 ?q=my_func를 사용한 GET 요청을 하면 다음과 같은 출력을 받을 수 있습니다:
my_func({"id":123});
쿼리 매개변수 이름을 전달하지 않으면 기본적으로 jsonp가 됩니다.
참고: 2025년 이후에도 JSONP 요청을 사용 중이라면 채팅에 참여하여 이유를 알려주세요! 좋은 전투/공포 이야기를 듣는 것을 좋아합니다!
응답 데이터 지우기
clear() 메서드를 사용하여 응답 본문과 헤더를 지울 수 있습니다. 이는 응답에 할당된 모든 헤더를 지우고, 응답 본문을 지우며, 상태 코드를 200으로 설정합니다.
Flight::response()->clear();
응답 본문만 지우기
응답 본문만 지우려면 clearBody() 메서드를 사용할 수 있습니다:
// 이는 response() 객체에 설정된 헤더를 유지합니다.
Flight::response()->clearBody();
HTTP 캐싱
Flight는 HTTP 수준 캐싱에 대한 내장 지원을 제공합니다. 캐싱 조건이 충족되면 Flight는 HTTP 304 Not Modified 응답을 반환합니다. 클라이언트가 동일한 리소스를 다음에 요청할 때 로컬 캐시된 버전을 사용하도록 안내됩니다.
라우트 수준 캐싱
전체 응답을 캐싱하려면 cache() 메서드를 사용하고 캐싱 시간을 전달할 수 있습니다.
// 이는 응답을 5분 동안 캐싱합니다
Flight::route('/news', function () {
Flight::response()->cache(time() + 300);
echo 'This content will be cached.';
});
// 또는 strtotime() 메서드에 전달할 문자열을 사용할 수 있습니다
Flight::route('/news', function () {
Flight::response()->cache('+5 minutes');
echo 'This content will be cached.';
});
Last-Modified
lastModified 메서드를 사용하고 UNIX 타임스탬프를 전달하여 페이지가 마지막으로 수정된 날짜와 시간을 설정할 수 있습니다. 클라이언트는 마지막 수정 값이 변경될 때까지 캐시를 계속 사용합니다.
Flight::route('/news', function () {
Flight::lastModified(1234567890);
echo 'This content will be cached.';
});
ETag
ETag 캐싱은 Last-Modified와 유사하지만, 리소스에 원하는 ID를 지정할 수 있습니다:
Flight::route('/news', function () {
Flight::etag('my-unique-id');
echo 'This content will be cached.';
});
lastModified 또는 etag을 호출하면 캐시 값을 설정하고 확인합니다. 요청 간 캐시 값이 동일하면 Flight는 즉시 HTTP 304 응답을 보내고 처리를 중지합니다.
파일 다운로드
v3.12.0
사용자에게 파일을 스트리밍하는 헬퍼 메서드가 있습니다. download 메서드를 사용하고 경로를 전달할 수 있습니다.
Flight::route('/download', function () {
Flight::download('/path/to/file.txt');
// v3.17.1부터 다운로드에 사용자 지정 파일 이름을 지정할 수 있습니다
Flight::download('/path/to/file.txt', 'custom_name.txt');
});
관련 자료
- 라우팅 - 라우트를 컨트롤러에 매핑하고 뷰를 렌더링하는 방법.
- 요청 - 들어오는 요청을 처리하는 방법 이해.
- 미들웨어 - 인증, 로깅 등에 라우트와 함께 미들웨어 사용.
- 왜 프레임워크인가? - Flight와 같은 프레임워크를 사용하는 이점 이해.
- 확장 - Flight를 자체 기능으로 확장하는 방법.
문제 해결
- 리디렉션이 작동하지 않는 문제가 있다면 메서드에
return;을 추가했는지 확인하세요. stop()과halt()는 동일하지 않습니다.halt()는 실행을 즉시 중지하지만,stop()은 실행을 계속할 수 있습니다.
변경 로그
- v3.17.1 -
downloadFile()메서드에$fileName추가. - v3.12.0 - downloadFile 헬퍼 메서드 추가.
- v3.10.0 -
jsonHalt추가. - v1.0 - 초기 릴리스.
Learn/events
이벤트 관리자
v3.15.0 기준
개요
이벤트는 애플리케이션에서 사용자 지정 동작을 등록하고 트리거할 수 있게 합니다. Flight::onEvent()와 Flight::triggerEvent()의 추가로, 앱의 라이프사이클의 주요 순간에 후크하거나 알림 및 이메일과 같은 사용자 지정 이벤트를 정의하여 코드를 더 모듈화하고 확장 가능하게 만들 수 있습니다. 이러한 메서드는 Flight의 mappable methods 일부로, 필요에 맞게 동작을 재정의할 수 있습니다.
이해하기
이벤트는 애플리케이션의 서로 다른 부분을 분리하여 서로에게 너무 의존하지 않게 합니다. 이 분리—종종 디커플링이라고 불림—는 코드를 업데이트, 확장 또는 디버그하기 쉽게 만듭니다. 모든 것을 하나의 큰 덩어리로 작성하는 대신, 논리를 특정 작업(이벤트)에 응답하는 더 작고 독립적인 조각으로 분할할 수 있습니다.
블로그 앱을 구축한다고 상상해 보세요:
- 사용자가 댓글을 게시할 때, 다음을 원할 수 있습니다:
- 댓글을 데이터베이스에 저장.
- 블로그 소유자에게 이메일 보내기.
- 보안을 위해 작업 로그 기록.
이벤트 없이, 모든 것을 하나의 함수에 넣게 됩니다. 이벤트와 함께라면 분할할 수 있습니다: 한 부분은 댓글을 저장하고, 다른 부분은 'comment.posted'와 같은 이벤트를 트리거하며, 별도의 리스너가 이메일과 로깅을 처리합니다. 이는 코드를 더 깨끗하게 유지하고, 코어 논리를 건드리지 않고 기능(예: 알림)을 추가하거나 제거할 수 있게 합니다.
일반적인 사용 사례
대부분의 경우, 이벤트는 선택적이지만 시스템의 절대적인 코어 부분이 아닌 것에 적합합니다. 예를 들어 다음은 좋지만, 어떤 이유로 실패하더라도 애플리케이션이 여전히 작동해야 합니다:
- 로깅: 로그인이나 오류와 같은 작업을 기록하면서 메인 코드를 어지럽히지 않음.
- 알림: 무언가가 발생할 때 이메일이나 경고 보내기.
- 캐시 업데이트: 캐시 새로 고침 또는 변경 사항에 대해 다른 시스템 알림.
그러나 비밀번호를 잊었을 때 기능을 생각해 보세요. 이는 코어 기능의 일부여야 하며 이벤트가 아닙니다. 왜냐하면 그 이메일이 발송되지 않으면 사용자가 비밀번호를 재설정하지 못하고 애플리케이션을 사용할 수 없기 때문입니다.
기본 사용법
Flight의 이벤트 시스템은 두 가지 주요 메서드 Flight::onEvent()(이벤트 리스너 등록)와 Flight::triggerEvent()(이벤트 발화)를 중심으로 구축됩니다. 다음과 같이 사용할 수 있습니다:
이벤트 리스너 등록
이벤트에 대한 리스닝을 위해 Flight::onEvent()를 사용합니다. 이 메서드는 이벤트 발생 시 무엇이 일어나야 하는지 정의할 수 있게 합니다.
Flight::onEvent(string $event, callable $callback): void
$event: 이벤트 이름 (예:'user.login').$callback: 이벤트가 트리거될 때 실행할 함수.
Flight에 이벤트 발생 시 무엇을 할지 알려 "구독"합니다. 콜백은 이벤트 트리거에서 전달된 인수를 받을 수 있습니다.
Flight의 이벤트 시스템은 동기적입니다. 즉, 각 이벤트 리스너가 순차적으로 하나씩 실행됩니다. 이벤트를 트리거하면, 해당 이벤트에 등록된 모든 리스너가 완료될 때까지 코드가 계속되지 않습니다. 이는 비동기 이벤트 시스템(리스너가 병렬로 또는 나중에 실행될 수 있음)과 다르기 때문에 이해하는 것이 중요합니다.
간단한 예제
Flight::onEvent('user.login', function ($username) {
echo "Welcome back, $username!";
// you can send an email if the login is from a new location
});
여기서 'user.login' 이벤트가 트리거되면, 사용자 이름을 이용해 인사하고 필요 시 이메일을 보내는 로직을 포함할 수 있습니다.
참고: 콜백은 함수, 익명 함수 또는 클래스 메서드가 될 수 있습니다.
이벤트 트리거
이벤트가 발생하도록 하려면 Flight::triggerEvent()를 사용합니다. 이는 해당 이벤트에 등록된 모든 리스너를 실행하고 제공된 데이터를 전달합니다.
Flight::triggerEvent(string $event, ...$args): void
$event: 트리거할 이벤트 이름(등록된 이벤트와 일치해야 함)....$args: 리스너에 보낼 선택적 인수(임의 수의 인수).
간단한 예제
$username = 'alice';
Flight::triggerEvent('user.login', $username);
이는 'user.login' 이벤트를 트리거하고 'alice'를 이전에 정의한 리스너에 보내 출력: Welcome back, alice!.
- 등록된 리스너가 없으면 아무 일도 일어나지 않음—앱이 깨지지 않음.
- 여러 인수를 유연하게 전달하기 위해 스프레드 연산자(
...) 사용.
이벤트 중지
리스너가 false를 반환하면 해당 이벤트의 추가 리스너가 실행되지 않습니다. 이는 특정 조건에 따라 이벤트 체인을 중지할 수 있게 합니다. 리스너의 순서가 중요하다는 점을 기억하세요. 첫 번째로 false를 반환하는 리스너가 나머지를 중지합니다.
예제:
Flight::onEvent('user.login', function ($username) {
if (isBanned($username)) {
logoutUser($username);
return false; // Stops subsequent listeners
}
});
Flight::onEvent('user.login', function ($username) {
sendWelcomeEmail($username); // this is never sent
});
이벤트 메서드 재정의
Flight::onEvent()와 Flight::triggerEvent()는 확장 가능하므로, 동작 방식을 재정의할 수 있습니다. 이는 로깅 추가나 이벤트 디스패치 방식 변경과 같은 이벤트 시스템을 사용자 지정하려는 고급 사용자에게 유용합니다.
예제: onEvent 사용자 지정
Flight::map('onEvent', function (string $event, callable $callback) {
// Log every event registration
error_log("New event listener added for: $event");
// Call the default behavior (assuming an internal event system)
Flight::_onEvent($event, $callback);
});
이제 이벤트를 등록할 때마다 로그를 기록한 후 진행합니다.
왜 재정의하나?
- 디버깅이나 모니터링 추가.
- 특정 환경(예: 테스트 중)에서 이벤트 제한.
- 다른 이벤트 라이브러리와 통합.
이벤트를 어디에 배치할까
프로젝트에서 이벤트 개념에 익숙하지 않다면, 앱에서 이 모든 이벤트를 어디에 등록하나?라고 궁금할 수 있습니다. Flight의 단순함 덕분에 엄격한 규칙은 없으며, 프로젝트에 맞게 어디든 배치할 수 있습니다. 그러나 앱이 성장함에 따라 코드를 유지하기 위해 체계적으로 유지하는 것이 도움이 됩니다. Flight의 가벼운 특성에 맞춘 실용적인 옵션과 모범 사례는 다음과 같습니다:
옵션 1: 메인 index.php에
작은 앱이나 빠른 프로토타입의 경우, index.php 파일에 이벤트 등록과 라우트를 함께 배치할 수 있습니다. 모든 것을 한 곳에 유지하여 단순함을 우선할 때 적합합니다.
require 'vendor/autoload.php';
// Register events
Flight::onEvent('user.login', function ($username) {
error_log("$username logged in at " . date('Y-m-d H:i:s'));
});
// Define routes
Flight::route('/login', function () {
$username = 'bob';
Flight::triggerEvent('user.login', $username);
echo "Logged in!";
});
Flight::start();
- 장점: 단순, 추가 파일 없음, 작은 프로젝트에 좋음.
- 단점: 앱이 성장함에 따라 이벤트와 라우트가 많아지면 어지러워짐.
옵션 2: 별도의 events.php 파일
조금 더 큰 앱의 경우, 이벤트 등록을 app/config/events.php와 같은 전용 파일로 이동하는 것을 고려하세요. index.php에서 라우트 전에 이 파일을 포함합니다. 이는 Flight 프로젝트에서 app/config/routes.php에 라우트를 조직하는 방식을 모방합니다.
// app/config/events.php
Flight::onEvent('user.login', function ($username) {
error_log("$username logged in at " . date('Y-m-d H:i:s'));
});
Flight::onEvent('user.registered', function ($email, $name) {
echo "Email sent to $email: Welcome, $name!";
});
// index.php
require 'vendor/autoload.php';
require 'app/config/events.php';
Flight::route('/login', function () {
$username = 'bob';
Flight::triggerEvent('user.login', $username);
echo "Logged in!";
});
Flight::start();
- 장점:
index.php를 라우팅에 집중, 이벤트를 논리적으로 조직, 찾고 편집하기 쉬움. - 단점: 매우 작은 앱에는 과도한 구조처럼 느껴질 수 있음.
옵션 3: 트리거되는 곳 근처에
또 다른 접근은 컨트롤러나 라우트 정의 내부와 같이 트리거되는 곳 근처에 이벤트를 등록하는 것입니다. 이는 이벤트가 앱의 한 부분에 특정할 때 잘 작동합니다.
Flight::route('/signup', function () {
// Register event here
Flight::onEvent('user.registered', function ($email) {
echo "Welcome email sent to $email!";
});
$email = 'jane@example.com';
Flight::triggerEvent('user.registered', $email);
echo "Signed up!";
});
- 장점: 관련 코드를 함께 유지, 고립된 기능에 좋음.
- 단점: 이벤트 등록이 산재되어 모든 이벤트를 한 번에 보기 어려움; 주의하지 않으면 중복 등록 위험.
Flight를 위한 모범 사례
- 단순하게 시작: 작은 앱의 경우
index.php에 이벤트를 배치. Flight의 미니멀리즘에 맞음. - 스마트하게 성장: 앱이 확장됨(예: 5-10개 이상의 이벤트)에
app/config/events.php파일 사용. 라우트 조직처럼 자연스러운 단계로, 복잡한 프레임워크 없이 코드를 깔끔하게 유지. - 과도한 엔지니어링 피하기: 앱이 거대해지지 않는 한 전체 "이벤트 관리자" 클래스나 디렉토리를 만들지 마세요—Flight는 단순함으로 번성하므로 가볍게 유지.
팁: 목적별 그룹화
events.php에서 관련 이벤트(예: 모든 사용자 관련 이벤트)를 함께 그룹화하고 명확성을 위해 주석 사용:
// app/config/events.php
// User Events
Flight::onEvent('user.login', function ($username) {
error_log("$username logged in");
});
Flight::onEvent('user.registered', function ($email) {
echo "Welcome to $email!";
});
// Page Events
Flight::onEvent('page.updated', function ($pageId) {
Flight::cache()->delete("page_$pageId");
});
이 구조는 잘 확장되며 초보자 친화적입니다.
실제 세계 예제
이벤트가 어떻게 작동하고 왜 유용한지 보여주기 위해 실제 시나리오를 살펴보겠습니다.
예제 1: 사용자 로그인 로깅
// Step 1: Register a listener
Flight::onEvent('user.login', function ($username) {
$time = date('Y-m-d H:i:s');
error_log("$username logged in at $time");
});
// Step 2: Trigger it in your app
Flight::route('/login', function () {
$username = 'bob'; // Pretend this comes from a form
Flight::triggerEvent('user.login', $username);
echo "Hi, $username!";
});
왜 유용한가: 로그인 코드는 로깅에 대해 알 필요가 없음—그냥 이벤트를 트리거. 나중에 더 많은 리스너(예: 환영 이메일 보내기)를 라우트를 변경하지 않고 추가할 수 있음.
예제 2: 새 사용자 알림
// Listener for new registrations
Flight::onEvent('user.registered', function ($email, $name) {
// Simulate sending an email
echo "Email sent to $email: Welcome, $name!";
});
// Trigger it when someone signs up
Flight::route('/signup', function () {
$email = 'jane@example.com';
$name = 'Jane';
Flight::triggerEvent('user.registered', $email, $name);
echo "Thanks for signing up!";
});
왜 유용한가: 가입 논리는 사용자 생성에 집중하고, 이벤트는 알림 처리. 나중에 더 많은 리스너(예: 가입 로그) 추가 가능.
예제 3: 캐시 지우기
// Listener to clear a cache
Flight::onEvent('page.updated', function ($pageId) {
// if using the flightphp/cache plugin
Flight::cache()->delete("page_$pageId");
echo "Cache cleared for page $pageId.";
});
// Trigger when a page is edited
Flight::route('/edit-page/(@id)', function ($pageId) {
// Pretend we updated the page
Flight::triggerEvent('page.updated', $pageId);
echo "Page $pageId updated.";
});
왜 유용한가: 편집 코드는 캐싱에 신경 쓰지 않음—업데이트 신호만. 앱의 다른 부분이 필요에 따라 반응할 수 있음.
모범 사례
- 이벤트 명확히 이름 짓기:
'user.login'또는'page.updated'와 같이 구체적인 이름 사용으로 무엇을 하는지 명확. - 리스너 단순하게 유지: 느리거나 복잡한 작업을 리스너에 넣지 말고 앱을 빠르게 유지.
- 이벤트 테스트: 예상대로 리스너가 작동하는지 수동으로 트리거.
- 이벤트 현명하게 사용: 디커플링에 좋지만, 너무 많으면 코드 추적이 어려움—필요할 때 사용.
Flight PHP의 이벤트 시스템은 Flight::onEvent()와 Flight::triggerEvent()로 단순하면서도 강력한 유연한 애플리케이션 구축 방식을 제공합니다. 앱의 다른 부분이 이벤트 통해 서로 소통하게 함으로써 코드를 조직화, 재사용 가능하고 확장하기 쉽게 유지할 수 있습니다. 작업 로깅, 알림 보내기 또는 업데이트 관리 시, 이벤트는 논리를 엉키지 않게 도와줍니다. 게다가 이러한 메서드를 재정의할 수 있어 시스템을 필요에 맞게 조정할 자유가 있습니다. 단일 이벤트로 작게 시작해 앱 구조가 어떻게 변하는지 지켜보세요!
내장 이벤트
Flight PHP에는 프레임워크 라이프사이클에 후크할 수 있는 몇 가지 내장 이벤트가 있습니다. 이러한 이벤트는 요청/응답 사이클의 특정 지점에서 트리거되어 특정 작업 발생 시 사용자 지정 논리를 실행할 수 있게 합니다.
내장 이벤트 목록
- flight.request.received:
function(Request $request)요청이 수신, 파싱 및 처리될 때 트리거. - flight.error:
function(Throwable $exception)요청 라이프사이클 중 오류 발생 시 트리거. - flight.redirect:
function(string $url, int $status_code)리다이렉트가 시작될 때 트리거. - flight.cache.checked:
function(string $cache_key, bool $hit, float $executionTime)특정 키에 대한 캐시 확인 시 트리거, 캐시 히트 또는 미스 여부. - flight.middleware.before:
function(Route $route)비포 미들웨어 실행 후 트리거. - flight.middleware.after:
function(Route $route)애프터 미들웨어 실행 후 트리거. - flight.middleware.executed:
function(Route $route, $middleware, string $method, float $executionTime)미들웨어가 실행된 후 트리거. - flight.route.matched:
function(Route $route)라우트가 매치되었지만 아직 실행되지 않았을 때 트리거. - flight.route.executed:
function(Route $route, float $executionTime)라우트가 실행 및 처리된 후 트리거.$executionTime은 라우트 실행(컨트롤러 호출 등)에 걸린 시간. - flight.view.rendered:
function(string $template_file_path, float $executionTime)뷰가 렌더링된 후 트리거.$executionTime은 템플릿 렌더링에 걸린 시간. 참고:render메서드를 재정의하면 이 이벤트를 다시 트리거해야 함. - flight.response.sent:
function(Response $response, float $executionTime)응답이 클라이언트에 전송된 후 트리거.$executionTime은 응답 빌드에 걸린 시간.
관련 자료
- Extending Flight - Flight의 코어 기능을 확장하고 사용자 지정하는 방법.
- Cache - 페이지 업데이트 시 이벤트를 사용해 캐시 지우기 예제.
문제 해결
- 이벤트 리스너가 호출되지 않으면, 이벤트를 트리거하기 전에 등록했는지 확인. 등록 순서가 중요합니다.
변경 로그
- v3.15.0 - Flight에 이벤트 추가.
Learn/templates
HTML 뷰 및 템플릿
개요
Flight는 기본적으로 몇 가지 기본 HTML 템플릿 기능을 제공합니다. 템플릿은 애플리케이션 로직을 프레젠테이션 레이어에서 분리하는 매우 효과적인 방법입니다. 전용 엔진(Twig, Latte 등)은 AI 코딩 도구에 익숙하고 제한된 구문을 제공하므로 비즈니스 로직을 HTML에 넣을 가능성이 줄어듭니다.
이해
애플리케이션을 구축할 때 최종 사용자에게 전달하려는 HTML이 있을 것입니다. PHP 자체는 템플릿 언어이지만 데이터베이스 호출, API 호출 등의 비즈니스 로직을 HTML 파일에 포함시키기가 매우 쉬워 테스트와 분리가 매우 어려운 과정이 됩니다. 템플릿에 데이터를 전달하고 템플릿이 스스로 렌더링하도록 하면 코드를 분리하고 단위 테스트하기가 훨씬 쉬워집니다. 템플릿을 사용하면 우리에게 감사하게 될 것입니다!
기본 사용
Flight에서는 render를 매핑(또는 뷰 클래스 등록)하는 것만으로 기본 뷰 엔진을 교체할 수 있습니다. Twig, Latte, Smarty, Blade 등에 대한 자세한 내용은 아래로 스크롤하세요.
스켈레톤 기본값: 공식 flightphp/skeleton은
app/views/디렉토리에서 Twig만 사용합니다(*.twig). 컨트롤러는$this->app->render('welcome', $data)를 호출합니다(확장자 선택 사항). 이는 새 프로젝트를 위한 애플리케이션 선택 사항이며 Flight 핵심의 요구 사항은 아닙니다. Latte 및 다른 엔진도 완전히 지원됩니다.
Twig
스켈레톤 기본값
Twig는 Symfony 및 다른 많은 PHP 프로젝트에서 사용되는 유연하고 빠르며 안전한 템플릿 엔진입니다. AI 코딩 도구는 특히 Twig를 잘 알고 있는 경향이 있으며, 기본적으로 출력을 자동으로 이스케이프하여 XSS로부터 보호하는 데 도움이 됩니다.
설치
composer require twig/twig
(composer create-project flightphp/skeleton로 이미 포함되어 있습니다.)
기본 구성
기본 PHP 렌더러 대신 Twig를 사용하도록 render 메서드를 재정의하세요:
// 기본 PHP 렌더러 대신 Twig를 사용하도록 render 메서드를 재정의합니다
Flight::map('render', function(string $template, array $data): void {
$loader = new \Twig\Loader\FilesystemLoader(Flight::get('flight.views.path'));
$twig = new \Twig\Environment($loader, [
// Twig가 컴파일된 템플릿을 저장하는 위치
'cache' => __DIR__ . '/../cache/twig',
'auto_reload' => true,
]);
// "welcome" 또는 "welcome.twig" 허용
if (substr($template, -5) !== '.twig') {
$template .= '.twig';
}
echo $twig->render($template, $data);
});
스켈레톤에서 이 연결(wiring)은 app/config/services.php에 있습니다(공유 Twig 환경, 캐시 경로, base_url / CSP nonce 같은 전역 변수). 컨트롤러에서 Engine을 주입하고 $app->render()를 호출하여 코드가 AI 및 테스트 친화적으로 유지되도록 하는 것이 좋습니다.
Flight에서 Twig 사용하기
이제 Twig로 렌더링할 수 있으니 다음과 같이 할 수 있습니다:
{# app/views/home.twig #}
<html>
<head>
<title>{% if title %}{{ title }} - {% endif %}My App</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<h1>Hello, {{ name }}!</h1>
</body>
</html>
// routes.php
Flight::route('/@name', function ($name) {
Flight::render('home.twig', [
'title' => 'Home Page',
'name' => $name
]);
});
브라우저에서 /Bob을 방문하면 출력은 다음과 같습니다:
<html>
<head>
<title>Home Page - My App</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<h1>Hello, Bob!</h1>
</body>
</html>
추가 자료
Twig를 레이아웃과 함께 사용하는 더 완전한 예제는 이 문서의 awesome plugins 섹션에서 확인할 수 있습니다. Tracy 바에서 렌더링 시간 메트릭을 보려면 Tracy Extensions의 Twig 패널을 참조하세요.
Twig의 모든 기능에 대해 더 자세히 알아보려면 공식 문서를 읽어보세요.
Latte
훌륭한 대안
Latte는 PHP와 유사한 구문을 가진 완전한 기능을 갖춘 엔진입니다. 여전히 Flight 앱에 탁월한 선택이며, 스켈레톤은 단순히 하나의 공유 기본값을 위해 Twig로 표준화한 것입니다(AI 도구가 템플릿을 생성할 때 특히 유용합니다).
설치
composer require latte/latte
기본 구성
핵심 아이디어는 기본 PHP 렌더러 대신 Latte를 사용하도록 render 메서드를 재정의하는 것입니다.
// 기본 PHP 렌더러 대신 Latte를 사용하도록 render 메서드를 재정의합니다
Flight::map('render', function(string $template, array $data, ?string $block): void {
$latte = new Latte\Engine;
// Latte가 캐시를 저장하는 위치
$latte->setTempDirectory(__DIR__ . '/../cache/');
$finalPath = Flight::get('flight.views.path') . $template;
$latte->render($finalPath, $data, $block);
});
Flight에서 Latte 사용하기
이제 Latte로 렌더링할 수 있으니 다음과 같이 할 수 있습니다:
<!-- app/views/home.latte -->
<html>
<head>
<title>{$title ? $title . ' - '}My App</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<h1>Hello, {$name}!</h1>
</body>
</html>
// routes.php
Flight::route('/@name', function ($name) {
Flight::render('home.latte', [
'title' => 'Home Page',
'name' => $name
]);
});
브라우저에서 /Bob을 방문하면 출력은 다음과 같습니다:
<html>
<head>
<title>Home Page - My App</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<h1>Hello, Bob!</h1>
</body>
</html>
추가 자료
Latte를 레이아웃과 함께 사용하는 더 복잡한 예제는 이 문서의 awesome plugins 섹션에서 확인할 수 있습니다.
번역 및 언어 기능을 포함한 Latte의 모든 기능에 대해 더 자세히 알아보려면 공식 문서를 읽어보세요.
기본 제공 뷰 엔진
더 이상 사용되지 않음
참고: 이것은 여전히 기본 기능이며 기술적으로는 여전히 작동합니다.
뷰 템플릿을 표시하려면 render 메서드를 템플릿 파일 이름과 선택적 템플릿 데이터와 함께 호출하세요:
Flight::render('hello.php', ['name' => 'Bob']);
전달하는 템플릿 데이터는 템플릿에 자동으로 주입되며 로컬 변수처럼 참조할 수 있습니다. 템플릿 파일은 단순한 PHP 파일입니다. hello.php 템플릿 파일의 내용이 다음과 같다면:
Hello, <?= $name ?>!
출력은 다음과 같습니다:
Hello, Bob!
set 메서드를 사용하여 뷰 변수를 수동으로 설정할 수도 있습니다:
Flight::view()->set('name', 'Bob');
이제 name 변수는 모든 뷰에서 사용할 수 있습니다. 따라서 다음과 같이 간단히 할 수 있습니다:
Flight::render('hello');
render 메서드에서 템플릿 이름을 지정할 때 .php 확장자를 생략할 수 있습니다.
기본적으로 Flight는 템플릿 파일을 위해 views 디렉토리를 찾습니다. 다음 구성을 설정하여 템플릿의 대체 경로를 설정할 수 있습니다:
Flight::set('flight.views.path', '/path/to/views');
레이아웃
웹사이트에서는 콘텐츠가 변경되는 단일 레이아웃 템플릿 파일을 사용하는 것이 일반적입니다. 레이아웃에 사용할 콘텐츠를 렌더링하려면 render 메서드에 선택적 매개변수를 전달할 수 있습니다.
Flight::render('header', ['heading' => 'Hello'], 'headerContent');
Flight::render('body', ['body' => 'World'], 'bodyContent');
그러면 뷰에 headerContent 및 bodyContent라는 저장된 변수가 생깁니다. 그런 다음 다음과 같이 레이아웃을 렌더링할 수 있습니다:
Flight::render('layout', ['title' => 'Home Page']);
템플릿 파일이 다음과 같다면:
header.php:
<h1><?= $heading ?></h1>
body.php:
<div><?= $body ?></div>
layout.php:
<html>
<head>
<title><?= $title ?></title>
</head>
<body>
<?= $headerContent ?>
<?= $bodyContent ?>
</body>
</html>
출력은 다음과 같습니다:
<html>
<head>
<title>Home Page</title>
</head>
<body>
<h1>Hello</h1>
<div>World</div>
</body>
</html>
Smarty
뷰에 Smarty 템플릿 엔진을 사용하는 방법은 다음과 같습니다:
// Smarty 라이브러리 로드
require './Smarty/libs/Smarty.class.php';
// Smarty를 뷰 클래스로 등록
// 또한 로드 시 Smarty를 구성하는 콜백 함수를 전달합니다
Flight::register('view', Smarty::class, [], function (Smarty $smarty) {
$smarty->setTemplateDir('./templates/');
$smarty->setCompileDir('./templates_c/');
$smarty->setConfigDir('./config/');
$smarty->setCacheDir('./cache/');
});
// 템플릿 데이터 할당
Flight::view()->assign('name', 'Bob');
// 템플릿 표시
Flight::view()->display('hello.tpl');
완전성을 위해 Flight의 기본 render 메서드도 재정의해야 합니다:
Flight::map('render', function(string $template, array $data): void {
Flight::view()->assign($data);
Flight::view()->display($template);
});
Blade
뷰에 Blade 템플릿 엔진을 사용하는 방법은 다음과 같습니다:
먼저 Composer를 통해 BladeOne 라이브러리를 설치해야 합니다:
composer require eftec/bladeone
그런 다음 Flight에서 BladeOne을 뷰 클래스로 구성할 수 있습니다:
<?php
// BladeOne 라이브러리 로드
use eftec\bladeone\BladeOne;
// BladeOne을 뷰 클래스로 등록
// 또한 로드 시 BladeOne을 구성하는 콜백 함수를 전달합니다
Flight::register('view', BladeOne::class, [], function (BladeOne $blade) {
$views = __DIR__ . '/../views';
$cache = __DIR__ . '/../cache';
$blade->setPath($views);
$blade->setCompiledPath($cache);
});
// 템플릿 데이터 할당
Flight::view()->share('name', 'Bob');
// 템플릿 표시
echo Flight::view()->run('hello', []);
완전성을 위해 Flight의 기본 render 메서드도 재정의해야 합니다:
<?php
Flight::map('render', function(string $template, array $data): void {
echo Flight::view()->run($template, $data);
});
이 예제에서 hello.blade.php 템플릿 파일은 다음과 같을 수 있습니다:
<?php
Hello, {{ $name }}!
출력은 다음과 같습니다:
Hello, Bob!
함께 보기
- 설치 - 새 프로젝트를 위한 스켈레톤 레이아웃(
app/views/*.twig). - 확장 - 다른 템플릿 엔진을 사용하도록
render메서드를 재정의하는 방법. - 라우팅 - 라우트를 컨트롤러에 매핑하고 뷰를 렌더링하는 방법.
- 응답 - HTTP 응답을 사용자 지정하는 방법.
- 보안 - 자동 이스케이프 및 XSS.
- AI 및 개발자 경험 - 하나의 뷰 엔진 기본값이 코딩 에이전트에 도움이 되는 이유.
- 프레임워크가 필요한 이유? - 템플릿이 전체 그림에 어떻게 맞는지.
문제 해결
- 미들웨어에 리다이렉트가 있는데 앱이 리다이렉트되지 않는 것 같으면 미들웨어에
exit;문을 추가했는지 확인하세요. - Twig가 템플릿을 찾을 수 없으면
flight.views.path를 확인하고 해당 경로 아래에 예상 확장자(스켈레톤:app/views/)로 파일이 존재하는지 확인하세요.
변경 로그
- 문서 – Twig가 공식 스켈레톤 기본값으로 문서화됨; Latte는 여전히 일급 대안으로 남아 있습니다.
- v2.0 - 최초 릴리스.
Learn/simple_pdo
SimplePdo PDO 헬퍼 클래스
개요
Flight의 SimplePdo 클래스는 PDO를 사용하여 데이터베이스 작업을 수행하기 위한 현대적이고 기능이 풍부한 헬퍼입니다. 이는 PdoWrapper를 확장하며, insert(), update(), delete() 및 트랜잭션과 같은 일반적인 데이터베이스 작업을 위한 편리한 헬퍼 메서드를 추가합니다. 데이터베이스 작업을 단순화하고, 결과를 쉽게 접근할 수 있도록 Collections로 반환하며, 고급 사용 사례를 위해 쿼리 로깅과 애플리케이션 성능 모니터링(APM)을 지원합니다.
이해
SimplePdo 클래스는 PHP에서 데이터베이스 작업을 훨씬 쉽게 만들도록 설계되었습니다. 준비된 문장, 가져오기 모드 및 장황한 SQL 작업을 다루는 대신, 일반적인 작업을 위한 깨끗하고 간단한 메서드를 얻을 수 있습니다. 모든 행은 Collection으로 반환되므로 배열 표기법($row['name'])과 객체 표기법($row->name)을 모두 사용할 수 있습니다.
이 클래스는 PdoWrapper의 상위 집합으로, PdoWrapper의 모든 기능을 포함하며 코드가 더 깨끗하고 유지보수하기 쉽게 만드는 추가 헬퍼 메서드를 제공합니다. 현재 PdoWrapper를 사용 중이라면 SimplePdo로 업그레이드하는 것이 간단합니다. 왜냐하면 이는 PdoWrapper를 확장하기 때문입니다.
Flight에서 SimplePdo를 공유 서비스로 등록할 수 있으며, 앱의 어디서나 Flight::db()를 통해 사용할 수 있습니다.
기본 사용법
SimplePdo 등록
먼저 Flight에 SimplePdo 클래스를 등록합니다:
Flight::register('db', \flight\database\SimplePdo::class, [
'mysql:host=localhost;dbname=cool_db_name', 'user', 'pass', [
PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES \'utf8mb4\'',
PDO::ATTR_EMULATE_PREPARES => false,
PDO::ATTR_STRINGIFY_FETCHES => false,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
]
]);
노트
PDO::ATTR_DEFAULT_FETCH_MODE를 지정하지 않으면SimplePdo가 자동으로PDO::FETCH_ASSOC로 설정합니다.
이제 어디서나 Flight::db()를 사용하여 데이터베이스 연결을 얻을 수 있습니다.
쿼리 실행
runQuery()
function runQuery(string $sql, array $params = []): PDOStatement
INSERT, UPDATE 또는 결과를 수동으로 가져오고 싶을 때 사용합니다:
$db = Flight::db();
$statement = $db->runQuery("SELECT * FROM users WHERE status = ?", ['active']);
while ($row = $statement->fetch()) {
// $row는 배열입니다
}
쓰기 작업에도 사용할 수 있습니다:
$db->runQuery("INSERT INTO users (name) VALUES (?)", ['Alice']);
$db->runQuery("UPDATE users SET name = ? WHERE id = ?", ['Bob', 1]);
fetchField()
function fetchField(string $sql, array $params = []): mixed
데이터베이스에서 단일 값을 가져옵니다:
$count = Flight::db()->fetchField("SELECT COUNT(*) FROM users WHERE status = ?", ['active']);
fetchRow()
function fetchRow(string $sql, array $params = []): ?Collection
단일 행을 Collection(배열/객체 접근)으로 가져옵니다:
$user = Flight::db()->fetchRow("SELECT * FROM users WHERE id = ?", [123]);
echo $user['name'];
// 또는
echo $user->name;
팁
SimplePdo는fetchRow()쿼리에LIMIT 1이 이미 없으면 자동으로 추가하여 쿼리를 더 효율적으로 만듭니다.
fetchAll()
function fetchAll(string $sql, array $params = []): array<Collection>
모든 행을 Collection 배열로 가져옵니다:
$users = Flight::db()->fetchAll("SELECT * FROM users WHERE status = ?", ['active']);
foreach ($users as $user) {
echo $user['name'];
// 또는
echo $user->name;
}
fetchColumn()
function fetchColumn(string $sql, array $params = []): array
단일 열을 배열로 가져옵니다:
$ids = Flight::db()->fetchColumn("SELECT id FROM users WHERE active = ?", [1]);
// 반환: [1, 2, 3, 4, 5]
fetchPairs()
function fetchPairs(string $sql, array $params = []): array
결과를 키-값 쌍(첫 번째 열을 키로, 두 번째를 값으로)으로 가져옵니다:
$userNames = Flight::db()->fetchPairs("SELECT id, name FROM users");
// 반환: [1 => 'John', 2 => 'Jane', 3 => 'Bob']
IN() 플레이스홀더 사용
IN() 절에서 단일 ?를 사용하고 배열을 전달할 수 있습니다:
$ids = [1, 2, 3];
$users = Flight::db()->fetchAll("SELECT * FROM users WHERE id IN (?)", [$ids]);
헬퍼 메서드
SimplePdo의 PdoWrapper에 대한 주요 장점 중 하나는 일반적인 데이터베이스 작업을 위한 편리한 헬퍼 메서드의 추가입니다.
insert()
function insert(string $table, array $data): string
하나 이상의 행을 삽입하고 마지막 삽입 ID를 반환합니다.
단일 삽입:
$id = Flight::db()->insert('users', [
'name' => 'John',
'email' => 'john@example.com'
]);
대량 삽입:
$id = Flight::db()->insert('users', [
['name' => 'John', 'email' => 'john@example.com'],
['name' => 'Jane', 'email' => 'jane@example.com'],
]);
update()
function update(string $table, array $data, string $where, array $whereParams = []): int
행을 업데이트하고 영향을 받은 행 수를 반환합니다:
$affected = Flight::db()->update(
'users',
['name' => 'Jane', 'email' => 'jane@example.com'],
'id = ?',
[1]
);
노트
SQLite의
rowCount()는 데이터가 실제로 변경된 행 수를 반환합니다. 이미 동일한 값으로 행을 업데이트하면rowCount()가 0을 반환합니다. 이는PDO::MYSQL_ATTR_FOUND_ROWS를 사용할 때 MySQL의 동작과 다릅니다.
delete()
function delete(string $table, string $where, array $whereParams = []): int
행을 삭제하고 삭제된 행 수를 반환합니다:
$deleted = Flight::db()->delete('users', 'id = ?', [1]);
transaction()
function transaction(callable $callback): mixed
트랜잭션 내에서 콜백을 실행합니다. 트랜잭션은 성공 시 자동 커밋되거나 오류 시 롤백됩니다:
$result = Flight::db()->transaction(function($db) {
$db->insert('users', ['name' => 'John']);
$db->insert('logs', ['action' => 'user_created']);
return $db->lastInsertId();
});
콜백 내에서 예외가 발생하면 트랜잭션이 자동으로 롤백되고 예외가 다시 발생합니다.
고급 사용법
쿼리 로깅 & APM
쿼리 성능을 추적하려면 등록 시 APM 추적을 활성화합니다:
Flight::register('db', \flight\database\SimplePdo::class, [
'mysql:host=localhost;dbname=cool_db_name',
'user',
'pass',
[/* PDO 옵션 */],
[
'trackApmQueries' => true,
'maxQueryMetrics' => 1000
]
]);
쿼리를 실행한 후 수동으로 로깅할 수 있지만, 활성화되면 APM이 자동으로 로깅합니다:
Flight::db()->logQueries();
이것은 연결 및 쿼리 메트릭과 함께 이벤트(flight.db.queries)를 트리거하며, Flight의 이벤트 시스템을 사용하여 이를 수신할 수 있습니다.
전체 예제
Flight::route('/users', function () {
// 모든 사용자 가져오기
$users = Flight::db()->fetchAll('SELECT * FROM users');
// 모든 사용자 스트림
$statement = Flight::db()->runQuery('SELECT * FROM users');
while ($user = $statement->fetch()) {
echo $user['name'];
}
// 단일 사용자 가져오기
$user = Flight::db()->fetchRow('SELECT * FROM users WHERE id = ?', [123]);
// 단일 값 가져오기
$count = Flight::db()->fetchField('SELECT COUNT(*) FROM users');
// 단일 열 가져오기
$ids = Flight::db()->fetchColumn('SELECT id FROM users');
// 키-값 쌍 가져오기
$userNames = Flight::db()->fetchPairs('SELECT id, name FROM users');
// 특수 IN() 구문
$users = Flight::db()->fetchAll('SELECT * FROM users WHERE id IN (?)', [[1,2,3,4,5]]);
// 새 사용자 삽입
$id = Flight::db()->insert('users', [
'name' => 'Bob',
'email' => 'bob@example.com'
]);
// 대량 사용자 삽입
Flight::db()->insert('users', [
['name' => 'Bob', 'email' => 'bob@example.com'],
['name' => 'Jane', 'email' => 'jane@example.com']
]);
// 사용자 업데이트
$affected = Flight::db()->update('users', ['name' => 'Bob'], 'id = ?', [123]);
// 사용자 삭제
$deleted = Flight::db()->delete('users', 'id = ?', [123]);
// 트랜잭션 사용
$result = Flight::db()->transaction(function($db) {
$db->insert('users', ['name' => 'John', 'email' => 'john@example.com']);
$db->insert('audit_log', ['action' => 'user_created']);
return $db->lastInsertId();
});
});
PdoWrapper에서 마이그레이션
현재 PdoWrapper를 사용 중이라면 SimplePdo로 마이그레이션하는 것이 간단합니다:
-
등록 업데이트:
// 이전 Flight::register('db', \flight\database\PdoWrapper::class, [ /* ... */ ]); // 새 Flight::register('db', \flight\database\SimplePdo::class, [ /* ... */ ]); -
기존
PdoWrapper메서드가SimplePdo에서 모두 작동 - 변경 사항이 없습니다. 기존 코드가 계속 작동합니다. -
새 헬퍼 메서드 선택적으로 사용 - 코드를 단순화하기 위해
insert(),update(),delete(),transaction()을 사용하기 시작하세요.
관련 자료
- Collections - 쉬운 데이터 접근을 위한 Collection 클래스 사용법을 배웁니다.
- PdoWrapper - 레거시 PDO 헬퍼 클래스 (사용 중단됨).
문제 해결
- 데이터베이스 연결 오류가 발생하면 DSN, 사용자 이름, 비밀번호 및 옵션을 확인하세요.
- 모든 행은 Collection으로 반환됩니다—일반 배열이 필요하면
$collection->getData()를 사용하세요. IN (?)쿼리의 경우 배열을 전달했는지 확인하세요.- 장기 실행 프로세스에서 쿼리 로깅으로 인한 메모리 문제를 겪는다면
maxQueryMetrics옵션을 조정하세요.
변경 로그
- v3.18.0 - insert, update, delete 및 트랜잭션 헬퍼 메서드가 포함된 SimplePdo의 초기 릴리스.
Learn/collections
컬렉션
개요
Flight의 Collection 클래스는 데이터 집합을 관리하기 위한 유용한 유틸리티입니다. 배열 표기법과 객체 표기법을 모두 사용하여 데이터에 접근하고 조작할 수 있으므로 코드가 더 깔끔하고 유연해집니다.
이해
Collection은 기본적으로 배열을 감싸는 래퍼이지만, 몇 가지 추가 기능이 있습니다. 배열처럼 사용할 수 있고, 반복할 수 있으며, 항목 수를 셀 수 있고, 객체 속성처럼 항목에 접근할 수도 있습니다. 이는 앱에서 구조화된 데이터를 전달하거나 코드를 더 읽기 쉽게 만들고 싶을 때 특히 유용합니다.
컬렉션은 여러 PHP 인터페이스를 구현합니다:
ArrayAccess(배열 구문을 사용할 수 있음)Iterator(foreach로 반복할 수 있음)Countable(count()를 사용할 수 있음)JsonSerializable(쉽게 JSON으로 변환할 수 있음)
기본 사용법
컬렉션 생성
생성자에 배열을 전달하면 컬렉션을 만들 수 있습니다:
use flight\util\Collection;
$data = [
'name' => 'Flight',
'version' => 3,
'features' => ['routing', 'views', 'extending']
];
$collection = new Collection($data);
항목 접근
배열 표기법이나 객체 표기법을 사용하여 항목에 접근할 수 있습니다:
// 배열 표기법
echo $collection['name']; // 출력: FlightPHP
// 객체 표기법
echo $collection->version; // 출력: 3
존재하지 않는 키에 접근하려고 하면 오류 대신 null을 반환합니다.
항목 설정
두 표기법 모두를 사용하여 항목을 설정할 수 있습니다:
// 배열 표기법
$collection['author'] = 'Mike Cao';
// 객체 표기법
$collection->license = 'MIT';
항목 확인 및 제거
항목이 존재하는지 확인:
if (isset($collection['name'])) {
// 작업 수행
}
if (isset($collection->version)) {
// 작업 수행
}
항목 제거:
unset($collection['author']);
unset($collection->license);
컬렉션 반복
컬렉션은 반복 가능하므로 foreach 루프에서 사용할 수 있습니다:
foreach ($collection as $key => $value) {
echo "$key: $value\n";
}
항목 개수 세기
컬렉션의 항목 수를 셀 수 있습니다:
echo count($collection); // 출력: 4
모든 키 또는 데이터 가져오기
모든 키 가져오기:
$keys = $collection->keys(); // ['name', 'version', 'features', 'license']
모든 데이터를 배열로 가져오기:
$data = $collection->getData();
컬렉션 비우기
모든 항목 제거:
$collection->clear();
JSON 직렬화
컬렉션은 쉽게 JSON으로 변환할 수 있습니다:
echo json_encode($collection);
// 출력: {"name":"FlightPHP","version":3,"features":["routing","views","extending"],"license":"MIT"}
고급 사용법
필요한 경우 내부 데이터 배열을 완전히 교체할 수 있습니다:
$collection->setData(['foo' => 'bar']);
컬렉션은 컴포넌트 간에 구조화된 데이터를 전달하거나 배열 데이터에 더 객체 지향적인 인터페이스를 제공하려 할 때 특히 유용합니다.
참고 항목
- 요청 - HTTP 요청을 처리하는 방법과 컬렉션을 사용하여 요청 데이터를 관리하는 방법을 알아보세요.
- SimplePdo - 쿼리 결과 행을 컬렉션으로 반환하는 데이터베이스 헬퍼입니다.
문제 해결
- 존재하지 않는 키에 접근하려고 하면 오류 대신
null을 반환합니다. - 컬렉션은 재귀적이지 않다는 점을 기억하세요: 중첩 배열은 자동으로 컬렉션으로 변환되지 않습니다.
- 컬렉션을 재설정해야 한다면
$collection->clear()또는$collection->setData([])를 사용하세요.
변경 내역
- v3.0 - 타입 힌트 개선 및 PHP 8+ 지원.
- v1.0 - Collection 클래스 최초 릴리스.
Learn/flight_vs_fat_free
Flight vs Fat-Free
Fat-Free란 무엇인가?
Fat-Free(애칭으로 F3이라고도 함)은 강력하면서도 사용하기 쉬운 PHP 마이크로프레임워크로, 빠르게 동적이고 견고한 웹 애플리케이션을 구축할 수 있도록 도와줍니다.
Flight는 여러 면에서 Fat-Free와 비교되며, 기능과 단순성 면에서 가장 가까운 사촌뻘일 것입니다. Fat-Free에는 Flight에는 없는 많은 기능이 있지만, Flight에 있는 많은 기능도 갖추고 있습니다. Fat-Free는 시대에 뒤처지기 시작했으며 예전만큼 인기가 있지는 않습니다.
업데이트 빈도가 점점 줄어들고 있으며 커뮤니티도 예전처럼 활발하지 않습니다. 코드 자체는 단순하지만, 문법적 규율이 부족한 부분이 있어 읽고 이해하기 어려울 때가 있습니다. PHP 8.3에서 작동하지만 코드 자체는 여전히 PHP 5.3 시대에 살고 있는 것처럼 보입니다.
Flight와 비교한 장점
- Fat-Free는 GitHub에서 Flight보다 별(star)이 조금 더 많습니다.
- Fat-Free는 괜찮은 문서를 갖추고 있지만 일부 영역에서는 명확성이 부족합니다.
- Fat-Free는 YouTube 튜토리얼이나 온라인 기사처럼 프레임워크를 학습하는 데 사용할 수 있는 자료가 다소 있지만 빈약합니다.
- Fat-Free에는 때때로 유용한 플러그인이 내장되어 있습니다.
- Fat-Free에는 Mapper라고 하는 내장 ORM이 있어 데이터베이스와 상호작용할 수 있습니다. Flight에는 active-record가 있습니다.
- Fat-Free에는 세션, 캐싱, 지역화가 내장되어 있습니다. Flight는 타사 라이브러리를 사용해야 하지만 문서에서 다루고 있습니다.
- Fat-Free에는 프레임워크를 확장하는 데 사용할 수 있는 커뮤니티 제작 플러그인이 소수 있습니다. Flight는 문서와 예제 페이지에서 일부를 다루고 있습니다.
- Fat-Free는 Flight와 마찬가지로 의존성이 없습니다.
- Fat-Free는 Flight와 마찬가지로 개발자에게 애플리케이션에 대한 제어권을 부여하고 단순한 개발자 경험을 제공하는 데 중점을 둡니다.
- Fat-Free는 Flight처럼 이전 버전과의 호환성을 유지합니다(부분적으로는 업데이트가 점점 드물어지기 때문입니다).
- Fat-Free는 Flight와 마찬가지로 프레임워크 세계에 처음 발을 들이는 개발자를 위한 것입니다.
- Fat-Free에는 Flight의 템플릿 엔진보다 더 강력한 내장 템플릿 엔진이 있습니다. Flight는 이를 위해 Latte를 권장합니다.
- Fat-Free에는 Fat-Free 자체 내에서 CLI 앱을 구축하고 이를 마치
GET요청처럼 취급할 수 있는 독특한 CLI 유형 "route" 명령이 있습니다. Flight는 runway로 이를 구현합니다.
Flight와 비교한 단점
- Fat-Free에는 몇 가지 구현 테스트가 있고 자체 test 클래스도 있지만 매우 기본적입니다. 그러나 Flight처럼 100% 유닛 테스트가 이루어지지는 않습니다.
- 문서 사이트를 실제로 검색하려면 Google과 같은 검색 엔진을 사용해야 합니다.
- Flight 문서 사이트에는 다크 모드가 있습니다. (마이크 드롭)
- Fat-Free에는 유지 관리가 제대로 되지 않는 일부 모듈이 있습니다.
- Flight에는 데이터베이스 액세스를 위한 SimplePdo가 있어 Fat-Free의 내장
DB\SQL클래스보다 한결 간단합니다(그리고 더 이상 사용되지 않는 PdoWrapper보다 선호됩니다). - Flight에는 애플리케이션을 보호하는 데 사용할 수 있는 permissions 플러그인이 있습니다. Fat-Free는 타사 라이브러리를 사용해야 합니다.
- Flight에는 active-record라는 ORM이 있는데, Fat-Free의 Mapper보다 ORM에 더 가깝게 느껴집니다.
active-record의 추가 이점은 레코드 간의 관계를 정의하여 자동 조인을 할 수 있다는 점입니다. 반면 Fat-Free의 Mapper는 SQL 뷰를 직접 만들어야 합니다. - 놀랍게도 Fat-Free에는 루트 네임스페이스가 없습니다. Flight는 자체 코드와 충돌하지 않도록 끝까지 네임스페이스가 적용되어 있습니다.
여기서 가장 큰 문제는
Cache클래스입니다. - Fat-Free에는 미들웨어가 없습니다. 대신 컨트롤러에서 요청과 응답을 필터링하는 데 사용할 수 있는
beforeroute와afterroute훅이 있습니다. - Fat-Free는 라우트를 그룹화할 수 없습니다.
- Fat-Free에는 의존성 주입 컨테이너 핸들러가 있지만 사용 방법에 대한 문서는 매우 부족합니다.
- 기본적으로 모든 것이
HIVE라는 곳에 저장되기 때문에 디버깅이 약간 까다로울 수 있습니다.
Learn/extending
확장
개요
Flight는 확장 가능한 프레임워크로 설계되었습니다. 이 프레임워크는 기본 메서드와 구성 요소 세트를 제공하지만, 사용자의 자체 메서드를 매핑하거나, 자체 클래스 등록, 또는 기존 클래스와 메서드 재정의가 가능합니다.
이해
Flight의 기능을 확장하는 방법은 2가지가 있습니다:
- 메서드 매핑 - 애플리케이션의 어디서나 호출할 수 있는 간단한 사용자 정의 메서드를 생성하는 데 사용됩니다. 이는 코드의 어디서나 호출할 수 있는 유틸리티 함수에 일반적으로 사용됩니다.
- 클래스 등록 - Flight에 자체 클래스를 등록하는 데 사용됩니다. 이는 종속성이나 구성이 필요한 클래스에 일반적으로 사용됩니다.
기존 프레임워크 메서드를 재정의하여 프로젝트 요구사항에 더 잘 맞도록 기본 동작을 변경할 수도 있습니다.
DIC(Dependency Injection Container)를 찾고 계신다면, Dependency Injection Container 페이지로 이동하세요.
기본 사용법
프레임워크 메서드 재정의
Flight는 코드를 수정하지 않고도 자체 요구사항에 맞게 기본 기능을 재정의할 수 있도록 허용합니다. 재정의할 수 있는 모든 메서드는 아래에서 확인할 수 있습니다.
예를 들어, Flight가 URL을 라우트에 매칭할 수 없을 때 notFound 메서드를 호출하여 일반적인 HTTP 404 응답을 보냅니다. 이 동작을 map 메서드를 사용하여 재정의할 수 있습니다:
Flight::map('notFound', function() {
// 사용자 정의 404 페이지 표시
include 'errors/404.html';
});
Flight는 또한 프레임워크의 핵심 구성 요소를 교체할 수 있도록 허용합니다. 예를 들어 기본 Router 클래스를 자체 사용자 정의 클래스로 교체할 수 있습니다:
// 사용자 정의 Router 클래스 생성
class MyRouter extends \flight\net\Router {
// 여기서 메서드 재정의
// 예를 들어 GET 요청에 대한 단축으로
// pass route 기능을 제거
public function get($pattern, $callback, $alias = '') {
return parent::get($pattern, $callback, false, $alias);
}
}
// 사용자 정의 클래스 등록
Flight::register('router', MyRouter::class);
// Flight가 Router 인스턴스를 로드할 때 사용자 클래스 로드
$myRouter = Flight::router();
$myRouter->get('/hello', function() {
echo "Hello World!";
}, 'hello_alias');
그러나 map 및 register와 같은 프레임워크 메서드는 재정의할 수 없습니다. 이를 시도하면 오류가 발생합니다 (다시 아래에서 메서드 목록 확인).
매핑 가능한 프레임워크 메서드
아래는 프레임워크의 전체 메서드 세트입니다. 이는 일반 정적 메서드인 핵심 메서드와, 필터링되거나 재정의될 수 있는 매핑된 메서드인 확장 메서드로 구성됩니다.
핵심 메서드
이 메서드들은 프레임워크의 핵심이며 재정의할 수 없습니다.
Flight::map(string $name, callable $callback, bool $pass_route = false) // 사용자 정의 프레임워크 메서드 생성.
Flight::register(string $name, string $class, array $params = [], ?callable $callback = null) // 프레임워크 메서드에 클래스 등록.
Flight::unregister(string $name) // 프레임워크 메서드에 등록된 클래스 등록 해제.
Flight::before(string $name, callable $callback) // 프레임워크 메서드 전에 필터 추가.
Flight::after(string $name, callable $callback) // 프레임워크 메서드 후에 필터 추가.
Flight::path(string $path) // 클래스 자동 로딩을 위한 경로 추가.
Flight::get(string $key) // Flight::set()으로 설정된 변수 가져오기.
Flight::set(string $key, mixed $value) // Flight 엔진 내 변수 설정.
Flight::has(string $key) // 변수가 설정되었는지 확인.
Flight::clear(array|string $key = []) // 변수 지우기.
Flight::init() // 프레임워크를 기본 설정으로 초기화.
Flight::app() // 애플리케이션 객체 인스턴스 가져오기
Flight::request() // 요청 객체 인스턴스 가져오기
Flight::response() // 응답 객체 인스턴스 가져오기
Flight::router() // 라우터 객체 인스턴스 가져오기
Flight::view() // 뷰 객체 인스턴스 가져오기
확장 메서드
Flight::start() // 프레임워크 시작.
Flight::stop() // 프레임워크 중지 및 응답 전송.
Flight::halt(int $code = 200, string $message = '') // 선택적 상태 코드와 메시지로 프레임워크 중지.
Flight::route(string $pattern, callable $callback, bool $pass_route = false, string $alias = '') // URL 패턴을 콜백에 매핑.
Flight::post(string $pattern, callable $callback, bool $pass_route = false, string $alias = '') // POST 요청 URL 패턴을 콜백에 매핑.
Flight::put(string $pattern, callable $callback, bool $pass_route = false, string $alias = '') // PUT 요청 URL 패턴을 콜백에 매핑.
Flight::patch(string $pattern, callable $callback, bool $pass_route = false, string $alias = '') // PATCH 요청 URL 패턴을 콜백에 매핑.
Flight::delete(string $pattern, callable $callback, bool $pass_route = false, string $alias = '') // DELETE 요청 URL 패턴을 콜백에 매핑.
Flight::group(string $pattern, callable $callback) // URL 그룹화 생성, 패턴은 문자열이어야 함.
Flight::getUrl(string $name, array $params = []) // 라우트 별칭에 기반한 URL 생성.
Flight::redirect(string $url, int $code) // 다른 URL로 리디렉션.
Flight::download(string $filePath) // 파일 다운로드.
Flight::render(string $file, array $data, ?string $key = null) // 템플릿 파일 렌더링.
Flight::error(Throwable $error) // HTTP 500 응답 전송.
Flight::notFound() // HTTP 404 응답 전송.
Flight::etag(string $id, string $type = 'string') // ETag HTTP 캐싱 수행.
Flight::lastModified(int $time) // 마지막 수정 HTTP 캐싱 수행.
Flight::json(mixed $data, int $code = 200, bool $encode = true, string $charset = 'utf8', int $option) // JSON 응답 전송.
Flight::jsonp(mixed $data, string $param = 'jsonp', int $code = 200, bool $encode = true, string $charset = 'utf8', int $option) // JSONP 응답 전송.
Flight::jsonHalt(mixed $data, int $code = 200, bool $encode = true, string $charset = 'utf8', int $option) // JSON 응답 전송 및 프레임워크 중지.
Flight::onEvent(string $event, callable $callback) // 이벤트 리스너 등록.
Flight::triggerEvent(string $event, ...$args) // 이벤트 트리거.
map 및 register로 추가된 사용자 정의 메서드는 필터링될 수도 있습니다. 이러한 메서드를 필터링하는 방법에 대한 예시는 Filtering Methods 가이드를 참조하세요.
확장 가능한 프레임워크 클래스
기존 클래스를 확장하고 자체 클래스를 등록하여 기능을 재정의할 수 있는 여러 클래스가 있습니다. 이러한 클래스들은 다음과 같습니다:
Flight::app() // 애플리케이션 클래스 - flight\Engine 클래스 확장
Flight::request() // 요청 클래스 - flight\net\Request 클래스 확장
Flight::response() // 응답 클래스 - flight\net\Response 클래스 확장
Flight::router() // 라우터 클래스 - flight\net\Router 클래스 확장
Flight::view() // 뷰 클래스 - flight\template\View 클래스 확장
Flight::eventDispatcher() // 이벤트 디스패처 클래스 - flight\core\Dispatcher 클래스 확장
사용자 정의 메서드 매핑
간단한 사용자 정의 메서드를 매핑하려면 map 함수를 사용합니다:
// 메서드 매핑
Flight::map('hello', function (string $name) {
echo "hello $name!";
});
// 사용자 정의 메서드 호출
Flight::hello('Bob');
간단한 사용자 정의 메서드를 만드는 것은 가능하지만, PHP에서 표준 함수를 생성하는 것이 IDE의 자동 완성 기능이 있고 읽기 쉽기 때문에 권장됩니다. 위 코드의 등가물은 다음과 같습니다:
function hello(string $name) {
echo "hello $name!";
}
hello('Bob');
이것은 메서드에 변수를 전달하여 예상 값이 필요한 경우 더 많이 사용됩니다. 아래와 같이 register() 메서드를 사용하는 것은 구성 전달 후 사전 구성된 클래스를 호출하는 데 더 적합합니다.
사용자 정의 클래스 등록
자체 클래스를 등록하고 구성하려면 register 함수를 사용합니다. map()에 비해 이점은 이 함수를 호출할 때 동일한 클래스를 재사용할 수 있다는 것입니다 (예: Flight::db()로 동일한 인스턴스 공유에 유용).
// 클래스 등록
Flight::register('user', User::class);
// 클래스 인스턴스 가져오기
$user = Flight::user();
register 메서드는 클래스 생성자에 매개변수를 전달할 수도 있습니다. 따라서 사용자 정의 클래스를 로드할 때 미리 초기화된 상태로 제공됩니다. 생성자 매개변수는 추가 배열을 전달하여 정의할 수 있습니다. 데이터베이스 연결을 로드하는 예시입니다:
// 생성자 매개변수와 함께 클래스 등록
Flight::register('db', PDO::class, ['mysql:host=localhost;dbname=test', 'user', 'pass']);
// 클래스 인스턴스 가져오기
// 정의된 매개변수로 객체 생성
//
// new PDO('mysql:host=localhost;dbname=test','user','pass');
//
$db = Flight::db();
// 코드의 나중에 필요하다면 동일한 메서드를 다시 호출
class SomeController {
public function __construct() {
$this->db = Flight::db();
}
}
추가 콜백 매개변수를 전달하면 클래스 생성 직후 즉시 실행됩니다. 이는 새 객체에 대한 설정 절차를 수행할 수 있도록 합니다. 콜백 함수는 새 객체의 인스턴스를 하나의 매개변수로 받습니다.
// 콜백은 생성된 객체를 전달받음
Flight::register(
'db',
PDO::class,
['mysql:host=localhost;dbname=test', 'user', 'pass'],
function (PDO $db) {
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
}
);
기본적으로 클래스를 로드할 때마다 공유 인스턴스를 받습니다.
클래스의 새 인스턴스를 얻으려면 매개변수로 false를 전달하면 됩니다:
// 클래스의 공유 인스턴스
$shared = Flight::db();
// 클래스의 새 인스턴스
$new = Flight::db(false);
참고: 매핑된 메서드가 등록된 클래스보다 우선순위가 높습니다. 동일한 이름으로 둘 다 선언하면 매핑된 메서드만 호출됩니다.
예시
Flight를 핵심에 내장되지 않은 기능으로 확장하는 방법에 대한 예시입니다.
로깅
Flight에는 내장 로깅 시스템이 없지만, 로깅 라이브러리를 Flight와 함께 사용하는 것은 매우 쉽습니다. Monolog 라이브러리를 사용한 예시입니다:
// services.php
// Flight에 로거 등록
Flight::register('log', Monolog\Logger::class, [ 'name' ], function(Monolog\Logger $log) {
$log->pushHandler(new Monolog\Handler\StreamHandler('path/to/your.log', Monolog\Logger::WARNING));
});
이제 등록되었으므로 애플리케이션에서 사용할 수 있습니다:
// 컨트롤러나 라우트에서
Flight::log()->warning('This is a warning message');
이것은 지정한 로그 파일에 메시지를 로깅합니다. 오류 발생 시 로그를 남기고 싶다면 error 메서드를 사용할 수 있습니다:
// 컨트롤러나 라우트에서
Flight::map('error', function(Throwable $ex) {
Flight::log()->error($ex->getMessage());
// 사용자 정의 오류 페이지 표시
include 'errors/500.html';
});
또한 before 및 after 메서드를 사용하여 기본 APM(Application Performance Monitoring) 시스템을 생성할 수 있습니다:
// services.php 파일에서
Flight::before('start', function() {
Flight::set('start_time', microtime(true));
});
Flight::after('start', function() {
$end = microtime(true);
$start = Flight::get('start_time');
Flight::log()->info('Request '.Flight::request()->url.' took ' . round($end - $start, 4) . ' seconds');
// 요청 또는 응답 헤더를 로그에 추가할 수도 있음
// (많은 요청이 있으면 데이터가 많아지니 주의)
Flight::log()->info('Request Headers: ' . json_encode(Flight::request()->headers));
Flight::log()->info('Response Headers: ' . json_encode(Flight::response()->headers));
});
캐싱
Flight에는 내장 캐싱 시스템이 없지만, 캐싱 라이브러리를 Flight와 함께 사용하는 것은 매우 쉽습니다. PHP File Cache 라이브러리를 사용한 예시입니다:
// services.php
// Flight에 캐시 등록
Flight::register('cache', \flight\Cache::class, [ __DIR__ . '/../cache/' ], function(\flight\Cache $cache) {
$cache->setDevMode(ENVIRONMENT === 'development');
});
이제 등록되었으므로 애플리케이션에서 사용할 수 있습니다:
// 컨트롤러나 라우트에서
$data = Flight::cache()->get('my_cache_key');
if (empty($data)) {
// 데이터 처리 수행
$data = [ 'some' => 'data' ];
Flight::cache()->set('my_cache_key', $data, 3600); // 1시간 캐시
}
간단한 DIC 객체 인스턴스화
애플리케이션에서 DIC(Dependency Injection Container)를 사용 중이라면, Flight를 사용하여 객체를 인스턴스화할 수 있습니다. Dice 라이브러리를 사용한 예시입니다:
// services.php
// 새 컨테이너 생성
$container = new \Dice\Dice;
// 아래와 같이 자신에게 재할당하는 것을 잊지 마세요!
$container = $container->addRule('PDO', [
// shared는 동일 객체가 매번 반환됨을 의미
'shared' => true,
'constructParams' => ['mysql:host=localhost;dbname=test', 'user', 'pass' ]
]);
// 이제 모든 객체를 생성하는 매핑 가능한 메서드 생성 가능.
Flight::map('make', function($class, $params = []) use ($container) {
return $container->create($class, $params);
});
// 컨트롤러/미들웨어에 사용하도록 컨테이너 핸들러 등록
Flight::registerContainerHandler(function($class, $params) {
Flight::make($class, $params);
});
// 생성자에서 PDO 객체를 받는 샘플 클래스라고 가정
class EmailCron {
protected PDO $pdo;
public function __construct(PDO $pdo) {
$this->pdo = $pdo;
}
public function send() {
// 이메일 전송 코드
}
}
// 마지막으로 의존성 주입을 사용하여 객체 생성
$emailCron = Flight::make(EmailCron::class);
$emailCron->send();
멋지지 않나요?
관련 자료
- Dependency Injection Container - Flight와 DIC 사용 방법.
- File Cache - Flight와 캐싱 라이브러리 사용 예시.
문제 해결
- 매핑된 메서드가 등록된 클래스보다 우선순위가 높습니다. 동일한 이름으로 둘 다 선언하면 매핑된 메서드만 호출됩니다.
변경 로그
- v2.0 - 초기 릴리스.
Learn/json
JSON Wrapper
개요
Flight의 Json 클래스는 애플리케이션에서 JSON 데이터를 인코딩하고 디코딩하는 간단하고 일관된 방법을 제공합니다. PHP의 기본 JSON 함수를 더 나은 오류 처리와 유용한 기본값으로 감싸서 JSON 작업을 더 쉽고 안전하게 만듭니다.
이해하기
현대 PHP 앱에서 JSON 작업은 API 구축이나 AJAX 요청 처리 시 매우 일반적입니다. Json 클래스는 모든 JSON 인코딩과 디코딩을 중앙화하여 PHP의 내장 함수에서 발생하는 이상한 에지 케이스나 난해한 오류를 걱정할 필요가 없습니다.
주요 기능:
- 일관된 오류 처리 (실패 시 예외 발생)
- 인코딩/디코딩을 위한 기본 옵션 (예: 이스케이프되지 않은 슬래시)
- 예쁜 출력 및 유효성 검사 유틸리티 메서드
기본 사용법
데이터를 JSON으로 인코딩하기
PHP 데이터를 JSON 문자열로 변환하려면 Json::encode()를 사용하세요:
use flight\util\Json;
$data = [
'framework' => 'Flight',
'version' => 3,
'features' => ['routing', 'views', 'extending']
];
$json = Json::encode($data);
echo $json;
// 출력: {"framework":"Flight","version":3,"features":["routing","views","extending"]}
인코딩이 실패하면 유용한 오류 메시지가 포함된 예외를 받게 됩니다.
예쁜 출력
JSON을 사람이 읽기 쉽게 하려면 prettyPrint()를 사용하세요:
echo Json::prettyPrint($data);
/*
{
"framework": "Flight",
"version": 3,
"features": [
"routing",
"views",
"extending"
]
}
*/
JSON 문자열 디코딩
JSON 문자열을 PHP 데이터로 다시 변환하려면 Json::decode()를 사용하세요:
$json = '{"framework":"Flight","version":3}';
$data = Json::decode($json);
echo $data->framework; // 출력: Flight
객체 대신 연관 배열을 원하면 두 번째 인수로 true를 전달하세요:
$data = Json::decode($json, true);
echo $data['framework']; // 출력: Flight
디코딩이 실패하면 명확한 오류 메시지가 포함된 예외를 받게 됩니다.
JSON 유효성 검사
문자열이 유효한 JSON인지 확인하세요:
if (Json::isValid($json)) {
// 유효합니다!
} else {
// 유효하지 않은 JSON
}
마지막 오류 가져오기
네이티브 PHP 함수에서 발생한 마지막 JSON 오류 메시지를 확인하려면:
$error = Json::getLastError();
if ($error !== '') {
echo "마지막 JSON 오류: $error";
}
고급 사용법
더 많은 제어가 필요하다면 인코딩 및 디코딩 옵션을 사용자 지정할 수 있습니다 ( PHP의 json_encode 옵션 참조):
// HEX_TAG 옵션으로 인코딩
$json = Json::encode($data, JSON_HEX_TAG);
// 사용자 지정 깊이로 디코딩
$data = Json::decode($json, false, 1024);
관련 항목
- Collections - JSON으로 쉽게 변환할 수 있는 구조화된 데이터 작업.
- Configuration - Flight 앱 구성 방법.
- Extending - 사용자 지정 유틸리티 추가 또는 코어 클래스 재정의 방법.
문제 해결
- 인코딩 또는 디코딩이 실패하면 예외가 발생합니다—오류를 우아하게 처리하려면 호출을 try/catch로 감싸세요.
- 예상치 못한 결과가 나오면 데이터에서 순환 참조나 비-UTF8 문자를 확인하세요.
- 디코딩 전에
Json::isValid()를 사용하여 문자열이 유효한 JSON인지 확인하세요.
변경 로그
- v3.16.0 - JSON 래퍼 유틸리티 클래스 추가.
Learn/flight_vs_slim
Flight vs Slim
Slim이란 무엇인가?
Slim은 간단하면서도 강력한 웹 애플리케이션과 API를 빠르게 작성할 수 있도록 도와주는 PHP 마이크로 프레임워크입니다.
Flight의 v3 기능 중 일부는 실제로 Slim에서 많은 영감을 받았습니다. 라우트 그룹화와 특정 순서대로 미들웨어를 실행하는 것은 Slim에서 영감을 받은 두 가지 기능입니다. Slim v3는 단순함에 중점을 두고 출시되었지만, v4에 대해서는 엇갈린 평가가 있습니다.
Flight와 비교한 장점
- Slim은 더 많은 개발자 커뮤니티를 보유하고 있으며, 그 덕분에 바퀴를 다시 발명하지 않아도 되도록 도와주는 유용한 모듈이 많이 만들어지고 있습니다.
- Slim은 PHP 커뮤니티에서 흔히 사용되는 많은 인터페이스와 표준을 따르므로 상호 운용성이 높아집니다.
- Slim은 프레임워크를 배우는 데 사용할 수 있는 괜찮은 문서와 튜토리얼을 제공합니다. (물론 Laravel이나 Symfony에는 미치지 못합니다.)
- Slim에는 프레임워크를 배우는 데 활용할 수 있는 YouTube 튜토리얼과 온라인 기사 같은 다양한 자료가 있습니다.
- Slim은 PSR-7을 준수하므로 핵심 라우팅 기능을 처리하기 위해 원하는 구성 요소를 사용할 수 있습니다.
Flight와 비교한 단점
- 놀랍게도 Slim은 마이크로 프레임워크로서 생각만큼 빠르지 않습니다. 자세한 내용은 TechEmpower 벤치마크를 참조하세요.
- Flight는 가볍고 빠르며 사용하기 쉬운 웹 애플리케이션을 구축하려는 개발자를 대상으로 합니다.
- Flight는 의존성이 없지만, Slim은 설치해야 하는 몇 가지 의존성이 있습니다.
- Flight는 단순함과 사용 용이성에 중점을 둡니다.
- Flight의 핵심 기능 중 하나는 하위 호환성을 유지하기 위해 최선을 다한다는 것입니다. Slim v3에서 v4로의 업그레이드는 호환성을 깨뜨리는 변경이었습니다.
- Flight는 프레임워크 세계에 처음 발을 들이는 개발자를 위한 것입니다.
- Flight는 엔터프라이즈급 애플리케이션도 처리할 수 있지만, Slim만큼 많은 예제와 튜토리얼을 제공하지는 않습니다. 또한 개발자가 코드를 체계적이고 잘 구조화된 상태로 유지하기 위해 더 많은 규율이 필요합니다.
- Flight는 개발자에게 애플리케이션에 대한 더 많은 제어권을 제공하는 반면, Slim은 백그라운드에서 마법 같은 동작을 몰래 수행할 수 있습니다.
- Flight는 데이터베이스 접근을 위한 SimplePdo를 제공합니다 (더 이상 사용되지 않는 PdoWrapper보다 권장됩니다). Slim은 타사 라이브러리를 사용해야 합니다.
- Flight에는 애플리케이션을 보호하는 데 사용할 수 있는 권한 플러그인이 있습니다. Slim은 타사 라이브러리를 사용해야 합니다.
- Flight에는 데이터베이스와 상호 작용하는 데 사용할 수 있는 active-record라는 ORM이 있습니다. Slim은 타사 라이브러리를 사용해야 합니다.
- Flight에는 명령줄에서 애플리케이션을 실행하는 데 사용할 수 있는 runway라는 CLI 애플리케이션이 있습니다. Slim에는 없습니다.
Learn/autoloading
오토로딩
개요
오토로딩은 PHP에서 클래스를 로드할 디렉터리(하나 또는 여러 개)를 지정하는 개념입니다. require나 include를 사용해 클래스를 로드하는 것보다 훨씬 유용합니다. 또한 Composer 패키지를 사용하기 위한 필수 조건이기도 합니다.
오토로딩을 올바르게 설정하는 것은 AI 지원 개발에도 중요합니다. 에이전트는 네임스페이스가 가리키는 위치에 파일을 배치하기 때문입니다. 폴더 대소문자와 네임스페이스의 대소문자가 일치하지 않으면, 대소문자를 구분하지 않는 Mac 디스크에서는 "잘 작동"했더라도 Linux에서는 클래스를 찾을 수 없는 오류가 발생할 수 있습니다.
이해하기
기본적으로 모든 Flight 클래스는 Composer 덕분에 자동으로 오토로딩됩니다. 여러분의 애플리케이션 클래스의 경우 일반적으로 두 가지 방법이 있습니다.
- Composer PSR-4 (공식 스켈레톤이 사용하는 방식):
composer.json에서 네임스페이스 접두사를 디렉터리에 매핑한 다음composer dump-autoload를 실행합니다. Flight::path(): Flight의 로더에 디렉터리를 지정합니다. (간단한 앱이나 앱 코드에 Composer를 사용하지 않을 때 유용합니다.)
오토로더를 사용하면 코드가 훨씬 간결해집니다. 모든 파일 상단에 긴 include / require 목록을 작성하는 대신, 클래스를 처음 사용할 때 로드됩니다.
대소문자 구분 (두 번 읽어보세요)
네임스페이스는 디렉터리 구조 및 해당 디렉터리의 대소문자와 일치해야 합니다.
| 동작함 | Linux에서 깨짐 |
|---|---|
App\Controller\HomeController → app/Controller/HomeController.php |
App\Controller\… 인데 폴더가 app/controllers/ 인 경우 |
app\controllers\MyController → app/controllers/MyController.php |
App\과 소문자 controllers를 혼용한 경우 |
PHP 네임스페이스는 일부 상황에서 대소문자를 구분하지 않지만, Composer와 파일 시스템은 그렇지 않습니다. 공식 스켈레톤은 다음을 표준으로 사용합니다.
- Composer:
"App\\": "app/" - 폴더:
Controller,Middleware,Model,Utils(PascalCase) –controllers/middlewares가 아님
이전 문서와 커뮤니티 예제에서는 소문자 app\controllers를 사용하기도 했습니다. 폴더가 소문자라면 여전히 작동하지만, 새 스켈레톤 프로젝트는 App\ + PascalCase 폴더를 사용합니다. 프로젝트마다 하나의 규칙을 선택하고 일관되게 유지하여 사람과 AI 도구가 두 번째 레이아웃을 만들지 않도록 하세요.
스켈레톤 (새 프로젝트에 권장)
composer create-project flightphp/skeleton 실행 후에는 앱 코드가 Composer를 통해 오토로딩됩니다. App\ 클래스에 대해 Flight::path()가 필요하지 않습니다.
{
"autoload": {
"psr-4": {
"App\\": "app/"
}
}
}
// app/Controller/HomeController.php
namespace App\Controller;
use flight\Engine;
class HomeController
{
protected Engine $app;
public function __construct(Engine $app)
{
$this->app = $app;
}
public function index(): void
{
$this->app->render('welcome', ['message' => 'Hello!']);
}
}
// app/config/routes.php — Dice가 컨테이너를 통해 App\Controller\…을 해석합니다.
$router->get('/', [HomeController::class, 'index']);
전체 트리는 설치를 참조하고, AGENTS.md가 코딩 어시스턴트를 위해 이 레이아웃을 문서화하는 방법은 AI 및 개발자 경험을 참조하세요.
기본 사용법 (Flight::path())
다음과 같은 디렉터리 트리가 있다고 가정해 봅시다.
# 예제 경로
/home/user/project/my-flight-project/
├── app
│ ├── cache
│ ├── config
│ ├── controllers - 이 프로젝트의 컨트롤러를 포함합니다.
│ ├── translations
│ ├── UTILS - 이 애플리케이션만을 위한 클래스를 포함합니다. (나중에 예시를 위해 의도적으로 모두 대문자입니다.)
│ └── views
└── public
└── css
└── js
└── index.php
이것이 일반적인 앱 트리와 유사하다는 것을 눈치채셨을 것입니다. (문서 사이트 자체도 구조화된 레이아웃을 사용합니다.) 여기서 소문자 controllers는 유효한 선택입니다. 다만 스켈레톤의 현재 기본값은 아닙니다.
각 디렉터리를 다음과 같이 지정할 수 있습니다.
/**
* public/index.php
*/
// 오토로더에 경로 추가
Flight::path(__DIR__.'/../app/controllers/');
Flight::path(__DIR__.'/../app/utils/');
/**
* app/controllers/MyController.php
*/
// 네임스페이스 불필요
// 오토로딩되는 모든 클래스는 Pascal Case(각 단어의 첫 글자를 대문자로, 공백 없음)를 권장합니다.
class MyController {
public function index() {
// do something
}
}
Flight::path()와 네임스페이스
네임스페이스가 있다면 구현이 매우 쉬워집니다. Flight::path() 메서드를 사용하여 애플리케이션의 루트 디렉터리(문서 루트나 public/ 폴더가 아님)를 지정해야 합니다.
/**
* public/index.php
*/
// 오토로더에 경로 추가
Flight::path(__DIR__.'/../');
이제 컨트롤러는 다음과 같을 수 있습니다. 아래 예제를 보되, 중요한 정보가 담긴 주석에 주의하세요.
/**
* app/controllers/MyController.php
*/
// 네임스페이스 필수
// 네임스페이스는 디렉터리 구조와 동일해야 합니다.
// 네임스페이스는 디렉터리 구조와 대소문자가 일치해야 합니다.
// 네임스페이스와 디렉터리에는 밑줄을 사용할 수 없습니다. (Loader::setV2ClassLoading(false)를 설정한 경우는 제외)
namespace app\controllers;
// 오토로딩되는 모든 클래스는 Pascal Case(각 단어의 첫 글자를 대문자로, 공백 없음)를 권장합니다.
// 3.7.2부터는 Loader::setV2ClassLoading(false); 를 실행하여 클래스 이름에 Pascal_Snake_Case를 사용할 수 있습니다.
class MyController {
public function index() {
// do something
}
}
그리고 utils 디렉터리의 클래스를 오토로딩하려면 기본적으로 동일한 방식으로 하면 됩니다.
/**
* app/UTILS/ArrayHelperUtil.php
*/
// 네임스페이스는 디렉터리 구조 및 대소문자와 일치해야 합니다. (위 파일 트리에서처럼 UTILS 디렉터리는 모두 대문자임을 주의하세요.)
namespace app\UTILS;
class ArrayHelperUtil {
public function changeArrayCase(array $array) {
// do something
}
}
스켈레톤 스타일 네임스페이스 (동일한 규칙, 다른 대소문자)
/**
* app/Controller/MyController.php
*/
namespace App\Controller;
class MyController {
// ...
}
규칙은 변하지 않았습니다. 단지 스켈레톤이 선택한 폴더/네임스페이스 대소문자가 다를 뿐입니다. 폴더가 어떤 대소문자를 사용하든 namespace 줄은 동일하게 맞춰야 합니다.
클래스 이름의 밑줄
3.7.2부터는 Loader::setV2ClassLoading(false);를 실행하여 클래스 이름에 Pascal_Snake_Case를 사용할 수 있습니다.
이렇게 하면 클래스 이름에 밑줄을 사용할 수 있습니다.
권장되지는 않지만, 필요한 사람들을 위해 제공됩니다.
use flight\core\Loader;
/**
* public/index.php
*/
// 오토로더에 경로 추가
Flight::path(__DIR__.'/../app/controllers/');
Flight::path(__DIR__.'/../app/utils/');
Loader::setV2ClassLoading(false);
/**
* app/controllers/My_Controller.php
*/
// 네임스페이스 불필요
class My_Controller {
public function index() {
// do something
}
}
같이 보기
- 설치 - 스켈레톤 트리 및 새 프로젝트의
App\기본값. - 라우팅 - 컨트롤러에 경로를 매핑하고 뷰를 렌더링하는 방법.
- 의존성 주입 - 컨트롤러가
Engine및 서비스를 얻는 방법. - AI 및 개발자 경험 -
AGENTS.md를 통해 에이전트를 여러분의 레이아웃에 맞추는 방법. - 프레임워크를 사용하는 이유? - Flight 같은 프레임워크 사용의 이점 이해.
문제 해결
- 네임스페이스 클래스를 찾을 수 없는 이유를 모르겠다면,
Flight::path()사용 시 프로젝트 루트(또는 네임스페이스에 맞는 올바른 기준 경로)를 가리키는지 확인하세요. 네임스페이스에 반영하는 것을 잊은 중첩 폴더만 가리키지 마세요. - Composer PSR-4를 사용하는 경우
composer.json매핑을 변경한 후composer dump-autoload를 실행하세요. - Linux CI 또는 프로덕션 환경에서 폴더 대소문자 오류는 매우 흔한 "내 컴퓨터에서는 작동하는데" 실패 원인입니다.
클래스를 찾을 수 없음 (오토로딩이 작동하지 않음)
여기에는 몇 가지 이유가 있을 수 있습니다. 아래에 몇 가지 예시가 있습니다.
잘못된 파일 이름
가장 흔한 원인은 클래스 이름이 파일 이름과 일치하지 않는 경우입니다.
클래스 이름이 MyClass라면 파일 이름은 MyClass.php여야 합니다. 클래스 이름이 MyClass인데 파일 이름이 myclass.php라면 오토로더가 해당 클래스를 찾을 수 없습니다.
잘못된 네임스페이스 또는 폴더 대소문자
네임스페이스를 사용하는 경우, 네임스페이스는 디렉터리 구조와 대소문자까지 포함하여 일치해야 합니다.
// ...code...
// MyController가 app/Controller (스켈레톤)에 있고 App\Controller 네임스페이스를 사용한다면
// 아래는 작동하지 않습니다:
Flight::route('/hello', 'MyController->hello');
// 스켈레톤 스타일:
use App\Controller\MyController;
Flight::route('/hello', [ MyController::class, 'hello' ]);
// 이전 소문자 레이아웃 (폴더가 실제로 app/controllers인 경우에만):
use app\controllers\MyController;
Flight::route('/hello', [ MyController::class, 'hello' ]);
// 또는 완전히 자격을 갖춘 이름:
Flight::route('/hello', [ 'App\Controller\MyController', 'hello' ]);
path()가 정의되지 않음 (Composer를 사용하지 않는 앱 코드)
애플리케이션 클래스에 Composer 대신 Flight::path()를 사용한다면, 해당 클래스를 참조하는 라우트보다 먼저(보통 부트스트랩 시작 부분이나 public/index.php에서) 경로를 정의하세요.
// 오토로더에 경로 추가 (네임스페이스 앱의 경우 프로젝트 루트)
Flight::path(__DIR__.'/../');
공식 스켈레톤은 App\에 대해 주로 Composer PSR-4를 사용하므로, 컨트롤러와 모델에 Flight::path()가 필요하지 않은 경우가 대부분입니다.
변경 로그
- 문서 – 스켈레톤
App\+ PascalCase 폴더 및 사람과 AI 도구를 위한 대소문자 구분 주의 사항 문서화. - v3.7.2 -
Loader::setV2ClassLoading(false);를 실행하여 클래스 이름에 Pascal_Snake_Case를 사용할 수 있습니다. - v2.0 - 오토로딩 기능 추가.
Learn/uploaded_file
업로드된 파일 핸들러
개요
Flight의 UploadedFile 클래스는 애플리케이션에서 파일 업로드를 쉽게 그리고 안전하게 처리할 수 있게 합니다. 이는 PHP의 파일 업로드 프로세스 세부 사항을 래핑하여 파일 정보를 액세스하고 업로드된 파일을 이동하는 간단한 객체 지향 방식을 제공합니다.
이해
사용자가 폼을 통해 파일을 업로드하면 PHP는 파일에 대한 정보를 $_FILES 슈퍼글로벌에 저장합니다. Flight에서는 $_FILES와 직접 상호작용하는 경우가 거의 없습니다. 대신 Flight의 Request 객체( Flight::request()를 통해 액세스 가능)에 getUploadedFiles() 메서드가 있으며, 이는 UploadedFile 객체 배열을 반환하여 파일 처리를 훨씬 더 편리하고 견고하게 만듭니다.
UploadedFile 클래스는 다음을 수행하는 메서드를 제공합니다:
- 원본 파일 이름, MIME 유형, 크기 및 임시 위치 가져오기
- 업로드 오류 확인
- 업로드된 파일을 영구 위치로 이동
이 클래스는 파일 업로드의 일반적인 함정을 피하는 데 도움을 주며, 오류 처리나 파일을 안전하게 이동하는 등의 작업을 포함합니다.
기본 사용법
요청에서 업로드된 파일 액세스
업로드된 파일에 액세스하는 권장 방법은 요청 객체를 통하는 것입니다:
Flight::route('POST /upload', function() {
// <input type="file" name="myFile">라는 폼 필드에 대한 경우
$uploadedFiles = Flight::request()->getUploadedFiles();
$file = $uploadedFiles['myFile'];
// 이제 UploadedFile 메서드를 사용할 수 있습니다
if ($file->getError() === UPLOAD_ERR_OK) {
$file->moveTo('/path/to/uploads/' . $file->getClientFilename());
echo "File uploaded successfully!";
} else {
echo "Upload failed: " . $file->getError();
}
});
여러 파일 업로드 처리
폼이 여러 업로드를 위해 name="myFiles[]"를 사용하는 경우 UploadedFile 객체 배열을 받게 됩니다:
Flight::route('POST /upload', function() {
// <input type="file" name="myFiles[]">라는 폼 필드에 대한 경우
$uploadedFiles = Flight::request()->getUploadedFiles();
foreach ($uploadedFiles['myFiles'] as $file) {
if ($file->getError() === UPLOAD_ERR_OK) {
$file->moveTo('/path/to/uploads/' . $file->getClientFilename());
echo "Uploaded: " . $file->getClientFilename() . "<br>";
} else {
echo "Failed to upload: " . $file->getClientFilename() . "<br>";
}
}
});
UploadedFile 인스턴스 수동 생성
보통 UploadedFile을 수동으로 생성하지 않지만, 필요하다면 할 수 있습니다:
use flight\net\UploadedFile;
$file = new UploadedFile(
$_FILES['myfile']['name'],
$_FILES['myfile']['type'],
$_FILES['myfile']['size'],
$_FILES['myfile']['tmp_name'],
$_FILES['myfile']['error']
);
파일 정보 액세스
업로드된 파일에 대한 세부 정보를 쉽게 가져올 수 있습니다:
echo $file->getClientFilename(); // 사용자의 컴퓨터에서 원본 파일 이름
echo $file->getClientMediaType(); // MIME 유형 (예: image/png)
echo $file->getSize(); // 바이트 단위 파일 크기
echo $file->getTempName(); // 서버의 임시 파일 경로
echo $file->getError(); // 업로드 오류 코드 (0은 오류 없음)
업로드된 파일 이동
파일을 검증한 후 영구 위치로 이동하세요:
try {
$file->moveTo('/path/to/uploads/' . $file->getClientFilename());
echo "File uploaded successfully!";
} catch (Exception $e) {
echo "Upload failed: " . $e->getMessage();
}
moveTo() 메서드는 문제가 발생하면 예외를 발생시킵니다 (예: 업로드 오류나 권한 문제).
업로드 오류 처리
업로드 중 문제가 발생하면 읽기 쉬운 오류 메시지를 가져올 수 있습니다:
if ($file->getError() !== UPLOAD_ERR_OK) {
// 오류 코드를 사용하거나 moveTo()에서 예외를 잡을 수 있습니다
echo "There was an error uploading the file.";
}
관련 항목
- Requests - HTTP 요청에서 업로드된 파일에 액세스하는 방법과 더 많은 파일 업로드 예제를 알아보세요.
- Configuration - PHP에서 업로드 제한 및 디렉토리를 구성하는 방법.
- Extending - Flight의 핵심 클래스를 사용자 지정하거나 확장하는 방법.
문제 해결
- 파일을 이동하기 전에 항상
$file->getError()를 확인하세요. - 업로드 디렉토리가 웹 서버에 의해 쓰기 가능하도록 하세요.
moveTo()가 실패하면 예외 메시지를 확인하여 세부 정보를 확인하세요.- PHP의
upload_max_filesize및post_max_size설정이 파일 업로드를 제한할 수 있습니다. - 여러 파일 업로드의 경우 항상
UploadedFile객체 배열을 반복하세요.
변경 로그
- v3.12.0 - 파일 처리를 더 쉽게 하기 위해 요청 객체에
UploadedFile클래스 추가.
Guides/unit_testing
Flight PHP에서 PHPUnit을 사용한 단위 테스트
이 가이드는 PHPUnit을 사용하여 Flight PHP에서 단위 테스트를 수행하는 방법을 소개하며, 단위 테스트가 왜 중요한지와 실제로 어떻게 적용하는지 이해하려는 초보자를 대상으로 합니다. 단순한 계산 테스트가 아니라 이메일 전송이나 레코드 저장과 같은 애플리케이션의 동작을 테스트하는 데 초점을 맞춥니다. 간단한 라우트 핸들러에서 시작하여 의존성 주입(DI)과 타사 서비스 모킹을 포함한 더 복잡한 컨트롤러로 진행합니다.
왜 단위 테스트를 해야 하나요?
단위 테스트는 코드가 예상대로 작동하는지 확인하여 버그가 프로덕션에 도달하기 전에 잡아냅니다. 특히 Flight의 가벼운 라우팅과 유연성으로 인해 복잡한 상호작용이 발생할 수 있으므로 단위 테스트는 매우 유용합니다. 개인 개발자든 팀이든 단위 테스트는 안전망 역할을 하며 예상 동작을 문서화하고 나중에 코드를 다시 볼 때 회귀를 방지합니다. 또한 테스트는 설계를 개선합니다. 테스트하기 어려운 코드는 종종 지나치게 복잡하거나 강하게 결합된 클래스를 나타냅니다.
단순한 예제(예: x * y = z 테스트)와 달리 입력 검증, 데이터 저장, 이메일 전송과 같은 트리거 등 실제 세계의 동작에 초점을 맞출 것입니다. 목표는 테스트를 접근하기 쉽고 의미 있게 만드는 것입니다.
일반적인 지침 원칙
- 구현이 아닌 동작을 테스트하세요: 결과(예: "이메일 전송됨" 또는 "레코드 저장됨")에 초점을 맞추고 내부 세부 사항은 무시하세요. 이렇게 하면 리팩터링에도 테스트가 견고해집니다.
Flight::사용을 중단하세요: Flight의 정적 메서드는 매우 편리하지만 테스트를 어렵게 만듭니다.$app = Flight::app();의$app변수에 익숙해져야 합니다.$app에는Flight::가 가진 모든 메서드가 동일하게 있습니다. 컨트롤러에서$app->route()또는$this->app->json()을 계속 사용할 수 있습니다. 또한 실제 Flight 라우터를$router = $app->router()로 사용하고$router->get(),$router->post(),$router->group()등을 사용할 수 있습니다. 라우팅을 참조하세요.- 테스트를 빠르게 유지하세요: 빠른 테스트는 자주 실행하게 만듭니다. 단위 테스트에서 데이터베이스 호출과 같은 느린 작업을 피하세요. 느린 테스트가 있다면 그것은 통합 테스트를 작성하고 있다는 신호입니다. 통합 테스트는 실제 데이터베이스, 실제 HTTP 호출, 실제 이메일 전송 등을 포함할 때입니다. 통합 테스트도 필요하지만 느리고 알 수 없는 이유로 실패할 수 있는 플레이키한 특성이 있습니다.
- 설명적인 이름을 사용하세요: 테스트 이름은 테스트하려는 동작을 명확하게 설명해야 합니다. 이는 가독성과 유지보수성을 향상시킵니다.
- 전역 변수를 벼락처럼 피하세요:
$app->set()과$app->get()은 전역 상태처럼 작동하므로 모든 테스트에서 목이 필요하게 됩니다. 사용을 최소화하고 DI 또는 DI 컨테이너를 선호하세요 (의존성 주입 컨테이너).$app->map()메서드도 기술적으로 "전역"이며 DI를 위해 피해야 합니다. flightphp/session과 같은 세션 라이브러리를 사용하여 테스트에서 세션 객체를 목으로 만들 수 있습니다.$_SESSION을 코드에서 직접 호출하지 마세요. 이는 코드에 전역 변수를 주입하여 테스트를 어렵게 만듭니다. - 의존성 주입을 사용하세요: 컨트롤러에 의존성(예:
PDO, 메일러)을 주입하여 로직을 분리하고 모킹을 단순화하세요. 너무 많은 의존성을 가진 클래스가 있다면 SOLID 원칙에 따라 각각 단일 책임을 갖는 더 작은 클래스로 리팩터링하는 것을 고려하세요. - 타사 서비스를 모킹하세요: 외부 호출을 피하기 위해 데이터베이스, HTTP 클라이언트(cURL), 이메일 서비스를 모킹하세요. 테스트는 한두 계층 깊이 수행하되 핵심 로직은 실제로 실행되도록 두세요. 예를 들어 앱이 문자 메시지를 보낸다면, 테스트를 실행할 때마다 실제로 문자 메시지를 보내고 싶지 않을 것입니다. 비용이 누적되고 느려지기 때문입니다. 대신 문자 메시지 서비스를 목으로 만들어 코드가 문자 메시지 서비스를 올바른 매개변수로 호출했는지만 확인하세요.
- 높은 커버리지를 목표로 하되 완벽함은 추구하지 마세요: 100% 라인 커버리지는 좋지만 실제로 코드의 모든 것이 제대로 테스트되고 있다는 뜻은 아닙니다(PHPUnit에서 라인/브랜치/경로 커버리지 수집을 연구해 보세요). 중요한 동작(예: 사용자 등록, API 응답 및 실패 응답 캡처)을 우선시하세요.
- 라우트에 컨트롤러를 사용하세요: 라우트 정의에서 클로저 대신 컨트롤러를 사용하세요.
flight\Engine $app은 기본적으로 생성자를 통해 모든 컨트롤러에 주입됩니다. 테스트에서는$app = new Flight\Engine()을 사용하여 테스트 내에서 Flight를 인스턴스화하고 컨트롤러에 주입한 다음 메서드를 직접 호출하세요(예:$controller->register()). Flight 확장 및 라우팅을 참조하세요. - 모킹 스타일을 정하고 일관되게 유지하세요: PHPUnit은 여러 모킹 스타일(예: prophecy, 내장 목)을 지원하며, 코드 완성, 메서드 정의 변경 시 깨짐 감지 등의 장점이 있는 익명 클래스를 사용할 수도 있습니다. 테스트 전반에 걸쳐 일관성을 유지하세요. PHPUnit 목 객체를 참조하세요.
- 하위 클래스에서 테스트하려는 메서드/속성에는
protected가시성을 사용하세요: 이렇게 하면 공개로 만들지 않고도 테스트 하위 클래스에서 재정의할 수 있습니다. 익명 클래스 목에서 특히 유용합니다.
PHPUnit 설정
먼저 Composer를 사용하여 Flight PHP 프로젝트에 PHPUnit을 설치하여 쉽게 테스트할 수 있게 하세요. 자세한 내용은 PHPUnit 시작 가이드를 참조하세요.
-
프로젝트 디렉터리에서 다음을 실행하세요:
composer require --dev phpunit/phpunit이 명령은 최신 PHPUnit을 개발 종속성으로 설치합니다.
-
프로젝트 루트에 테스트 파일용
tests디렉터리를 만드세요. -
편의를 위해
composer.json에 테스트 스크립트를 추가하세요:// 기타 composer.json 내용 "scripts": { "test": "phpunit --configuration phpunit.xml" } -
루트에
phpunit.xml파일을 만드세요:<?xml version="1.0" encoding="UTF-8"?> <phpunit bootstrap="vendor/autoload.php"> <testsuites> <testsuite name="Flight Tests"> <directory>tests</directory> </testsuite> </testsuites> </phpunit>
이제 테스트가 준비되면 composer test를 실행하여 테스트를 실행할 수 있습니다.
간단한 라우트 핸들러 테스트
사용자의 이메일 입력을 검증하는 기본적인 라우트부터 시작하겠습니다. 유효한 이메일에는 성공 메시지를, 유효하지 않은 이메일에는 오류를 반환하는 동작을 테스트합니다. 이메일 검증에는 filter_var를 사용합니다.
// index.php
$app->route('POST /register', [ UserController::class, 'register' ]);
// UserController.php
class UserController {
protected $app;
public function __construct(flight\Engine $app) {
$this->app = $app;
}
public function register() {
$email = $this->app->request()->data->email;
$responseArray = [];
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
$responseArray = ['status' => 'error', 'message' => 'Invalid email'];
} else {
$responseArray = ['status' => 'success', 'message' => 'Valid email'];
}
$this->app->json($responseArray);
}
}
테스트를 위해 테스트 파일을 만드세요. 테스트 구조에 대한 자세한 내용은 단위 테스트와 SOLID 원칙을 참조하세요:
// tests/UserControllerTest.php
use PHPUnit\Framework\TestCase;
use Flight;
use flight\Engine;
class UserControllerTest extends TestCase {
public function testValidEmailReturnsSuccess() {
$app = new Engine();
$request = $app->request();
$request->data->email = 'test@example.com'; // POST 데이터 시뮬레이션
$UserController = new UserController($app);
$UserController->register($request->data->email);
$response = $app->response()->getBody();
$output = json_decode($response, true);
$this->assertEquals('success', $output['status']);
$this->assertEquals('Valid email', $output['message']);
}
public function testInvalidEmailReturnsError() {
$app = new Engine();
$request = $app->request();
$request->data->email = 'invalid-email'; // POST 데이터 시뮬레이션
$UserController = new UserController($app);
$UserController->register($request->data->email);
$response = $app->response()->getBody();
$output = json_decode($response, true);
$this->assertEquals('error', $output['status']);
$this->assertEquals('Invalid email', $output['message']);
}
}
핵심 사항:
$_POST,$_GET같은 전역 변수를 사용하지 않고 요청 클래스를 사용하여 POST 데이터를 시뮬레이션합니다. 전역 변수를 사용하면 다른 테스트가 깨질 수 있으므로 항상 값을 재설정해야 해서 테스트가 더 복잡해집니다.- 모든 컨트롤러는 기본적으로 DIC(의존성 주입 컨테이너)가 설정되지 않아도
flight\Engine인스턴스가 주입됩니다. 이렇게 하면 컨트롤러를 직접 훨씬 쉽게 테스트할 수 있습니다. Flight::를 전혀 사용하지 않으므로 코드를 테스트하기가 더 쉽습니다.- 테스트는 유효/무효 이메일에 대한 올바른 상태와 메시지라는 동작을 검증합니다.
composer test를 실행하여 라우트가 예상대로 작동하는지 확인하세요. Flight의 요청 및 응답에 대한 자세한 내용은 관련 문서를 참조하세요.
테스트 가능한 컨트롤러를 위한 의존성 주입 사용
더 복잡한 시나리오에서는 의존성 주입(DI)을 사용하여 컨트롤러를 테스트 가능하게 만드세요. Flight의 전역 메서드(예: Flight::set(), Flight::map(), Flight::register())는 전역 상태처럼 작동하여 모든 테스트에서 목이 필요하므로 피하세요. 대신 Flight의 DI 컨테이너인 DICE, PHP-DI 또는 수동 DI를 사용하세요.
원시 PDO 대신 flight\database\SimplePdo를 사용해 보겠습니다. 이 헬퍼는 목으로 만들고 단위 테스트하기 훨씬 쉬우며(더 이상 사용되지 않는 PdoWrapper보다 권장됩니다).
다음은 사용자를 데이터베이스에 저장하고 환영 이메일을 보내는 컨트롤러입니다:
use flight\database\SimplePdo;
class UserController {
protected $app;
protected $db;
protected $mailer;
public function __construct(Engine $app, SimplePdo $db, MailerInterface $mailer) {
$this->app = $app;
$this->db = $db;
$this->mailer = $mailer;
}
public function register() {
$email = $this->app->request()->data->email;
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
// 여기서 return을 추가하면 단위 테스트에서 실행을 중단시키는 데 도움이 됩니다.
return $this->app->jsonHalt(['status' => 'error', 'message' => 'Invalid email']);
}
$this->db->runQuery('INSERT INTO users (email) VALUES (?)', [$email]);
$this->mailer->sendWelcome($email);
return $this->app->json(['status' => 'success', 'message' => 'User registered']);
}
}
핵심 사항:
- 컨트롤러는
SimplePdo인스턴스와MailerInterface(가상의 타사 이메일 서비스)에 의존합니다. - 의존성은 생성자를 통해 주입되므로 전역 변수를 피합니다.
목(Mock)을 사용하여 컨트롤러 테스트
이제 UserController의 동작(이메일 검증, 데이터베이스 저장, 이메일 전송)을 테스트해 보겠습니다. 데이터베이스와 메일러를 목으로 만들어 컨트롤러를 격리합니다.
// tests/UserControllerDICTest.php
use flight\database\SimplePdo;
use PHPUnit\Framework\TestCase;
class UserControllerDICTest extends TestCase {
public function testValidEmailSavesAndSendsEmail() {
// 때로는 모킹 스타일을 혼합하는 것이 필요합니다.
// 여기서는 PHPUnit 내장 목을 PDOStatement에 사용합니다.
$statementMock = $this->createMock(PDOStatement::class);
$statementMock->method('execute')->willReturn(true);
// 익명 클래스를 사용하여 SimplePdo를 모킹합니다.
$mockDb = new class($statementMock) extends SimplePdo {
protected $statementMock;
public function __construct($statementMock) {
$this->statementMock = $statementMock;
}
// 이렇게 모킹하면 실제 데이터베이스 호출이 이루어지지 않습니다.
// PDOStatement 목을 추가로 설정하여 실패 등을 시뮬레이션할 수 있습니다.
public function runQuery(string $sql, array $params = []): PDOStatement {
return $this->statementMock;
}
};
$mockMailer = new class implements MailerInterface {
public $sentEmail = null;
public function sendWelcome($email): bool {
$this->sentEmail = $email;
return true;
}
};
$app = new Engine();
$app->request()->data->email = 'test@example.com';
$controller = new UserControllerDIC($app, $mockDb, $mockMailer);
$controller->register();
$response = $app->response()->getBody();
$result = json_decode($response, true);
$this->assertEquals('success', $result['status']);
$this->assertEquals('User registered', $result['message']);
$this->assertEquals('test@example.com', $mockMailer->sentEmail);
}
public function testInvalidEmailSkipsSaveAndEmail() {
$mockDb = new class() extends SimplePdo {
// 빈 생성자는 부모 생성자를 우회합니다.
public function __construct() {}
public function runQuery(string $sql, array $params = []): PDOStatement {
throw new Exception('Should not be called');
}
};
$mockMailer = new class implements MailerInterface {
public $sentEmail = null;
public function sendWelcome($email): bool {
throw new Exception('Should not be called');
}
};
$app = new Engine();
$app->request()->data->email = 'invalid-email';
// 종료를 피하기 위해 jsonHalt를 매핑해야 합니다.
$app->map('jsonHalt', function($data) use ($app) {
$app->json($data, 400);
});
$controller = new UserControllerDIC($app, $mockDb, $mockMailer);
$controller->register();
$response = $app->response()->getBody();
$result = json_decode($response, true);
$this->assertEquals('error', $result['status']);
$this->assertEquals('Invalid email', $result['message']);
}
}
핵심 사항:
- 실제 데이터베이스 또는 이메일 호출을 피하기 위해
SimplePdo와MailerInterface를 목으로 만듭니다. - 테스트는 동작을 검증합니다: 유효한 이메일은 데이터베이스 삽입과 이메일 전송을 트리거하고, 유효하지 않은 이메일은 둘 다 건너뜁니다.
- 타사 의존성(예:
SimplePdo,MailerInterface)만 목으로 만들고 컨트롤러의 로직은 실제로 실행되게 합니다.
너무 많이 모킹하기
코드를 너무 많이 모킹하지 않도록 주의하세요. UserController를 예로 들어 이것이 왜 나쁜 일인지 아래에서 설명하겠습니다. 해당 검증을 isEmailValid( filter_var 사용)라는 메서드로 바꾸고, 다른 새 추가 사항은 registerUser라는 별도의 메서드로 분리하겠습니다.
use flight\database\SimplePdo;
use flight\Engine;
// UserControllerDICV2.php
class UserControllerDICV2 {
protected $app;
protected $db;
protected $mailer;
public function __construct(Engine $app, SimplePdo $db, MailerInterface $mailer) {
$this->app = $app;
$this->db = $db;
$this->mailer = $mailer;
}
public function register() {
$email = $this->app->request()->data->email;
if (!$this->isEmailValid($email)) {
// 여기서 return을 추가하면 단위 테스트에서 실행을 중단시키는 데 도움이 됩니다.
return $this->app->jsonHalt(['status' => 'error', 'message' => 'Invalid email']);
}
$this->registerUser($email);
$this->app->json(['status' => 'success', 'message' => 'User registered']);
}
protected function isEmailValid($email) {
return filter_var($email, FILTER_VALIDATE_EMAIL) !== false;
}
protected function registerUser($email) {
$this->db->runQuery('INSERT INTO users (email) VALUES (?)', [$email]);
$this->mailer->sendWelcome($email);
}
}
이제 실제로 아무것도 테스트하지 않는 과도하게 모킹된 단위 테스트를 보겠습니다:
use PHPUnit\Framework\TestCase;
class UserControllerTest extends TestCase {
public function testValidEmailSavesAndSendsEmail() {
$app = new Engine();
$app->request()->data->email = 'test@example.com';
// "쉽다"는 이유로 추가 의존성 주입을 건너뜁니다.
$controller = new class($app) extends UserControllerDICV2 {
protected $app;
// 생성자의 의존성을 우회합니다.
public function __construct($app) {
$this->app = $app;
}
// 그냥 유효하다고 강제합니다.
protected function isEmailValid($email) {
return true; // 항상 true를 반환하여 실제 검증을 우회합니다.
}
// 실제 DB 및 메일러 호출을 우회합니다.
protected function registerUser($email) {
return false;
}
};
$controller->register();
$response = $app->response()->getBody();
$result = json_decode($response, true);
$this->assertEquals('success', $result['status']);
$this->assertEquals('User registered', $result['message']);
}
}
훌륭합니다. 단위 테스트가 있고 통과합니다! 하지만 잠깐, isEmailValid나 registerUser의 내부 동작을 실제로 변경하면 어떻게 될까요? 모든 기능을 모킹했기 때문에 테스트는 여전히 통과할 것입니다. 그 의미를 보여드리겠습니다.
// UserControllerDICV2.php
class UserControllerDICV2 {
// ... 기타 메서드 ...
protected function isEmailValid($email) {
// 로직 변경
$validEmail = filter_var($email, FILTER_VALIDATE_EMAIL) !== false;
// 이제 특정 도메인만 허용해야 합니다.
$validDomain = strpos($email, '@example.com') !== false;
return $validEmail && $validDomain;
}
}
위의 단위 테스트를 실행하면 여전히 통과합니다! 그러나 동작을 테스트하지 않았기 때문에(일부 코드가 실제로 실행되도록 두지 않았기 때문에) 프로덕션에서 발생할 수 있는 버그를 코딩했을 가능성이 있습니다. 테스트는 새 동작을 반영하도록 수정되어야 하며, 동작이 기대와 다를 때의 반대 경우도 수정되어야 합니다.
전체 예제
단위 테스트가 포함된 Flight PHP 프로젝트의 전체 예제는 GitHub에서 확인할 수 있습니다: n0nag0n/flight-unit-tests-guide. 더 깊이 이해하려면 단위 테스트와 SOLID 원칙을 참조하세요.
흔한 함정
- 과도한 모킹: 모든 의존성을 모킹하지 마세요. 실제 동작을 테스트하려면 일부 로직(예: 컨트롤러 검증)은 실행되도록 두세요. 단위 테스트와 SOLID 원칙을 참조하세요.
- 전역 상태: PHP 전역 변수(예:
$_SESSION,$_COOKIE)를 많이 사용하면 테스트가 취약해집니다.Flight::사용도 마찬가지입니다. 의존성을 명시적으로 전달하도록 리팩터링하세요. - 복잡한 설정: 테스트 설정이 번거롭다면 클래스에 너무 많은 의존성이나 책임이 있어 SOLID 원칙을 위반했을 수 있습니다.
단위 테스트로 확장하기
단위 테스트는 더 큰 프로젝트나 몇 달 후 코드를 다시 볼 때 특히 빛을 발합니다. 동작을 문서화하고 회귀를 잡아내므로 앱을 다시 배우는 수고를 덜어줍니다. 개인 개발자는 중요한 경로(예: 사용자 가입, 결제 처리)를 테스트하세요. 팀의 경우 테스트는 기여 전반에 걸쳐 일관된 동작을 보장합니다. 프레임워크와 테스트의 이점에 대한 자세한 내용은 프레임워크가 왜 필요한가요?를 참조하세요.
Flight PHP 문서 저장소에 자신만의 테스트 팁을 기여하세요!
Written by n0nag0n 2025
Guides/blog
Flight PHP로 간단한 블로그 만들기
이 가이드는 Flight PHP 프레임워크를 사용하여 기본 블로그를 만드는 과정을 안내합니다. 프로젝트를 설정하고, 라우트를 정의하고, JSON으로 게시물을 관리하고, Latte 템플릿 엔진으로 렌더링하는 방법을 살펴봅니다. 이 모든 과정을 통해 Flight의 단순함과 유연성을 확인할 수 있습니다. 마지막에는 홈페이지, 개별 게시물 페이지, 작성 양식을 갖춘 작동하는 블로그를 만들 수 있습니다.
사전 요구 사항
- PHP 7.4+: 시스템에 설치되어 있어야 합니다.
- Composer: 의존성 관리를 위해 필요합니다.
- 텍스트 편집기: VS Code나 PHPStorm 같은 편집기.
- PHP와 웹 개발에 대한 기본 지식.
1단계: 프로젝트 설정
먼저 새 프로젝트 디렉터리를 만들고 Composer를 통해 Flight를 설치합니다.
-
디렉터리 만들기:
mkdir flight-blog cd flight-blog -
Flight 설치:
composer require flightphp/core -
public 디렉터리 만들기: Flight는 단일 진입점(
index.php)을 사용합니다. 이를 위해public/폴더를 만듭니다:mkdir public -
기본
index.php: 간단한 "hello world" 라우트를 가진public/index.php를 만듭니다:<?php require '../vendor/autoload.php'; Flight::route('/', function () { echo 'Hello, Flight!'; }); Flight::start(); -
내장 서버 실행: PHP 개발 서버로 설정을 테스트합니다:
php -S localhost:8000 -t public/http://localhost:8000에 접속하면 "Hello, Flight!"가 표시됩니다.
2단계: 프로젝트 구조 정리
깔끔한 구성을 위해 프로젝트를 다음과 같이 구성합니다:
flight-blog/
├── app/
│ ├── config/
│ └── views/
├── data/
├── public/
│ └── index.php
├── vendor/
└── composer.json
app/config/: 설정 파일 (예: 이벤트, 라우트).app/views/: 페이지 렌더링을 위한 템플릿.data/: 블로그 게시물을 저장하는 JSON 파일.public/:index.php가 있는 웹 루트.
3단계: Latte 설치 및 구성
Latte는 Flight와 잘 통합되는 가벼운 템플릿 엔진입니다.
-
Latte 설치:
composer require latte/latte -
Flight에서 Latte 구성:
public/index.php를 업데이트하여 Latte를 뷰 엔진으로 등록합니다:<?php require '../vendor/autoload.php'; use Latte\Engine; Flight::register('view', Engine::class, [], function ($latte) { $latte->setTempDirectory(__DIR__ . '/../cache/'); $latte->setLoader(new \Latte\Loaders\FileLoader(__DIR__ . '/../app/views/')); }); Flight::route('/', function () { Flight::view()->render('home.latte', ['title' => 'My Blog']); }); Flight::start(); -
레이아웃 템플릿 만들기:
app/views/layout.latte:<!DOCTYPE html> <html> <head> <title>{$title}</title> </head> <body> <header> <h1>My Blog</h1> <nav> <a href="/">Home</a> | <a href="/create">Create a Post</a> </nav> </header> <main> {block content}{/block} </main> <footer> <p>© {date('Y')} Flight Blog</p> </footer> </body> </html> -
홈 템플릿 만들기:
app/views/home.latte에 다음을 작성합니다:{extends 'layout.latte'} {block content} <h2>{$title}</h2> <ul> {foreach $posts as $post} <li><a href="/post/{$post['slug']}">{$post['title']}</a></li> {/foreach} </ul> {/block}서버에서 나왔다면 다시 시작하고
http://localhost:8000에 접속하여 렌더링된 페이지를 확인합니다. -
데이터 파일 만들기:
간단히 하기 위해 JSON 파일을 데이터베이스처럼 사용합니다.
data/posts.json에 다음을 작성합니다:[ { "slug": "first-post", "title": "My First Post", "content": "This is my very first blog post with Flight PHP!" } ]
4단계: 라우트 정의
더 나은 구성을 위해 라우트를 설정 파일로 분리합니다.
-
routes.php만들기:app/config/routes.php에 다음을 작성합니다:<?php Flight::route('/', function () { Flight::view()->render('home.latte', ['title' => 'My Blog']); }); Flight::route('/post/@slug', function ($slug) { Flight::view()->render('post.latte', ['title' => 'Post: ' . $slug, 'slug' => $slug]); }); Flight::route('GET /create', function () { Flight::view()->render('create.latte', ['title' => 'Create a Post']); }); -
index.php업데이트: 라우트 파일을 포함합니다:<?php require '../vendor/autoload.php'; use Latte\Engine; Flight::register('view', Engine::class, [], function ($latte) { $latte->setTempDirectory(__DIR__ . '/../cache/'); $latte->setLoader(new \Latte\Loaders\FileLoader(__DIR__ . '/../app/views/')); }); require '../app/config/routes.php'; Flight::start();
5단계: 블로그 게시물 저장 및 불러오기
게시물을 로드하고 저장하는 메서드를 추가합니다.
-
게시물 메서드 추가:
index.php에서 게시물을 로드하는 메서드를 추가합니다:Flight::map('posts', function () { $file = __DIR__ . '/../data/posts.json'; return json_decode(file_get_contents($file), true); }); -
라우트 업데이트:
app/config/routes.php를 게시물을 사용하도록 수정합니다:<?php Flight::route('/', function () { $posts = Flight::posts(); Flight::view()->render('home.latte', [ 'title' => 'My Blog', 'posts' => $posts ]); }); Flight::route('/post/@slug', function ($slug) { $posts = Flight::posts(); $post = array_filter($posts, fn($p) => $p['slug'] === $slug); $post = reset($post) ?: null; if (!$post) { Flight::notFound(); return; } Flight::view()->render('post.latte', [ 'title' => $post['title'], 'post' => $post ]); }); Flight::route('GET /create', function () { Flight::view()->render('create.latte', ['title' => 'Create a Post']); });
6단계: 템플릿 만들기
게시물을 표시하도록 템플릿을 업데이트합니다.
-
게시물 페이지 (
app/views/post.latte):{extends 'layout.latte'} {block content} <h2>{$post['title']}</h2> <div class="post-content"> <p>{$post['content']}</p> </div> {/block}
7단계: 게시물 작성 기능 추가
양식 제출을 처리하여 새 게시물을 추가합니다.
-
양식 만들기 (
app/views/create.latte):{extends 'layout.latte'} {block content} <h2>{$title}</h2> <form method="POST" action="/create"> <div class="form-group"> <label for="title">Title:</label> <input type="text" name="title" id="title" required> </div> <div class="form-group"> <label for="content">Content:</label> <textarea name="content" id="content" required></textarea> </div> <button type="submit">Save Post</button> </form> {/block} -
POST 라우트 추가:
app/config/routes.php에서:Flight::route('POST /create', function () { $request = Flight::request(); $title = $request->data['title']; $content = $request->data['content']; $slug = strtolower(str_replace(' ', '-', $title)); $posts = Flight::posts(); $posts[] = ['slug' => $slug, 'title' => $title, 'content' => $content]; file_put_contents(__DIR__ . '/../../data/posts.json', json_encode($posts, JSON_PRETTY_PRINT)); Flight::redirect('/'); }); -
테스트:
http://localhost:8000/create에 접속합니다.- 새 게시물을 제출합니다 (예: "Second Post"와 내용).
- 홈페이지에서 해당 게시물이 목록에 표시되는지 확인합니다.
8단계: 오류 처리 개선
더 나은 404 환경을 위해 notFound 메서드를 재정의합니다.
index.php에서:
Flight::map('notFound', function () {
Flight::view()->render('404.latte', ['title' => 'Page Not Found']);
});
app/views/404.latte를 만듭니다:
{extends 'layout.latte'}
{block content}
<h2>404 - {$title}</h2>
<p>Sorry, that page doesn't exist!</p>
{/block}
다음 단계
- 스타일 추가: 템플릿에 CSS를 사용하여 더 나은 디자인을 적용합니다.
- 데이터베이스: SimplePdo를 사용하여
posts.json을 SQLite 같은 데이터베이스로 대체합니다. - 유효성 검사: 중복 슬러그나 빈 입력에 대한 검사를 추가합니다.
- 미들웨어: 게시물 작성에 인증을 구현합니다.
결론
Flight PHP로 간단한 블로그를 만들었습니다! 이 가이드는 라우팅, Latte를 사용한 템플릿 처리, 양식 제출 처리와 같은 핵심 기능을 가볍게 유지하면서 보여줍니다. 블로그를 더 발전시키려면 Flight의 문서에서 더 많은 고급 기능을 살펴보세요!
License
MIT 라이센스 (MIT)
=====================
Copyright © 2024 @mikecao, @n0nag0n
어떠한 제약 없이 이 소프트웨어 및 관련 문서를 소유하고 있는 모든 사람에게 무료로 사용 권한이 제공됩니다.
("Software"), 여기에는 사용, 복사, 수정, 병합, 발행, 배포, 하위 라이선스, 판매권 등이 포함됩니다.
소프트웨어의 사본을 획득한 사람은 상기 조건에 따라 소프트웨어를 다룰 수 있으며,
소프트웨어가 제공되는 사람들에게 소프트웨어를 사용할 권리가 부여됩니다.
다음 조건에 따라 조건부로 허용됩니다.
상기 저작권 고지 및 이 권한 통지는 소프트웨어의 모든 복사본 또는 상당한 부분에 포함되어야합니다.
소프트웨어는 "있는 그대로" 제공되며 어떠한 종류의 보증도 없이, 명시적이든 묵시적이든, 상품성, 특정 목적에의 적합성 및 비침해성을 포함하되 이에 한하지 않는 보증이 포함됩니다. 등의 사건에서 발생하는 모든 청구, 손해 또는 기타 책임에 대해 제작자 또는 저작권 소유자가 책임지지 않습니다. 계약, 불법 행위 또는 기타, 소프트웨어 또는 사용 또는 기타 거래에 대한 이러한 저작물로부터의 파생작품.
About
Flight PHP 프레임워크
Flight은 빠르고 간단하며 확장 가능한 PHP 프레임워크로, 빠르게 작업을 완료하고자 하는 개발자들을 위해 설계되었습니다. 클래식 웹 앱, 초고속 API 구축, 또는 AI 코딩 어시스턴트와의 협업까지, Flight의 경량성과 직관적인 설계는 완벽한 선택이 될 것입니다. Flight은 경량성을 지향하지만, 엔터프라이즈 아키텍처 요구사항도 충분히 처리할 수 있습니다.
왜 Flight을 선택해야 할까요?
- 초보자 친화적: Flight은 PHP를 처음 접하는 개발자에게 훌륭한 시작점입니다. 명확한 구조와 간단한 문법으로 복잡한 보일러플레이트 없이 웹 개발을 배울 수 있습니다.
- 전문가들에게 사랑받는: 숙련된 개발자들은 Flight의 유연성과 제어 능력에 매료됩니다. 작은 프로토타입에서부터 완전한 기능을 갖춘 앱까지, 프레임워크를 바꾸지 않고도 확장할 수 있습니다.
- 하위 호환성: 우리는 여러분의 시간을 소중히 여깁니다. Flight v3는 v2의 확장으로, 기존 API를 거의 그대로 유지합니다. 우리는 진화가 아닌 혁명을 믿습니다—메이저 버전이 나올 때마다 "모든 것을 깨는" 일은 없습니다.
- 의존성 없음: Flight의 핵심은 완전히 의존성 없이 구성되어 있습니다—폴리필, 외부 패키지, PSR 인터페이스조차 없습니다. 이는 더 적은 공격 벡터, 더 작은 풋프린트, 그리고 상위 의존성으로 인한 예기치 않은 호환성 문제를 의미합니다. 선택적 플러그인은 의존성을 포함할 수 있지만, 핵심은 항상 경량성과 보안을 유지합니다.
- AI 친화적: Flight의 작은 API 표면과 공식 스켈레톤(단일 레이아웃,
AGENTS.md, 생성자 주입)은 AI 코딩 도구가 패턴을 유지하기 쉽게 합니다. 모든 코드를 직접 입력하든, 에이전트와 협업하든 동일한 코드베이스를 사용합니다. Flight과 AI 사용에 대해 더 알아보기.
비디오 개요
빠른 시작
빠른 기본 설치의 경우, Composer로 설치하세요:
composer require flightphp/core
또는 여기에서 저장소의 zip 파일을 다운로드할 수 있습니다. 그런 다음 다음과 같은 기본 index.php 파일을 가지게 됩니다:
<?php
// composer로 설치한 경우
require 'vendor/autoload.php';
// 또는 zip 파일로 수동 설치한 경우
// require 'flight/Flight.php';
Flight::route('/', function() {
echo 'hello world!';
});
Flight::route('/json', function() {
Flight::json([
'hello' => 'world'
]);
});
Flight::start();
이게 전부입니다! 기본적인 Flight 애플리케이션이 완성되었습니다. 이제 php -S localhost:8000로 이 파일을 실행하고 브라우저에서 http://localhost:8000을 방문하여 결과를 확인할 수 있습니다.
이와 같은 짧은 Flight:: 예제는 학습과 마이크로 앱에 적합합니다. 인간과 AI 도구가 공유하는 전체 프로젝트 레이아웃의 경우, 아래 스켈레톤을 사용하세요.
스켈레톤/보일러플레이트 앱
새로운 Flight 프로젝트를 시작할 수 있도록 공식 스타터가 있습니다. 구조, 설정, Composer 스크립트, AI 친화적인 지침을 처음부터 설정합니다.
준비된 프로젝트는 flightphp/skeleton을 확인하거나, 영감을 얻으려면 예제 페이지를 방문하세요. AI 워크플로우 세부 사항이 필요하신가요? AI와 개발자 경험 탐색.
얻을 수 있는 것들 (고급 수준):
App\네임스페이스와 PascalCase 폴더 (app/Controller/,app/Middleware/,app/Model/, …)—폴더 대소문자는 네임스페이스와 일치해야 함 (자동 로딩 참조)- Dice +
Engine주입으로 컨트롤러가 테스트 가능하게 유지 (앱 코드에서Flight::대신$this->app선호) - Twig 뷰, SimplePdo + ActiveRecord 샘플, Runway migrate
- 어시스턴트와 보안 정책을 위한 루트
AGENTS.md(및 범위 지정된 복사본)와SECURITY.md
스켈레톤 앱 설치
간단합니다!
# 새 프로젝트 생성
composer create-project flightphp/skeleton my-project/
# 새 프로젝트 디렉토리로 이동
cd my-project/
# 바로 시작할 수 있도록 로컬 개발 서버 실행!
composer start
프로젝트 구조를 생성하고, config_sample.php → config.php (그리고 .env.example → .env가 있는 경우)로 복사하며, 준비가 완료됩니다. 선택적 샘플 데이터:
php runway migrate
# 그 다음 /posts와 /api/posts 방문
고성능
Flight는 가장 빠른 PHP 프레임워크 중 하나입니다. 경량화된 코어는 오버헤드를 줄이고 속도를 높여—전통적인 앱과 현대적인 AI 지원 워크플로우 모두에 적합합니다. TechEmpower에서 모든 벤치마크를 확인할 수 있습니다.
다른 인기 있는 PHP 프레임워크들과의 벤치마크를 아래에서 확인하세요.
| Framework | Plaintext Reqs/sec | JSON Reqs/sec |
|---|---|---|
| Flight | 190,421 | 182,491 |
| Yii | 145,749 | 131,434 |
| Fat-Free | 139,238 | 133,952 |
| Slim | 89,588 | 87,348 |
| Phalcon | 95,911 | 87,675 |
| Symfony | 65,053 | 63,237 |
| Lumen | 40,572 | 39,700 |
| Laravel | 26,657 | 26,901 |
| CodeIgniter | 20,628 | 19,901 |
Flight과 AI
코딩 LLM과 Flight이 어떻게 연동되는지 궁금하신가요? AGENTS.md, Runway ai:* 명령어, 스켈레톤 레이아웃이 어시스턴트들을 올바른 길로 유지하는 방법을 발견하세요.
안정성과 하위 호환성
우리는 여러분의 시간을 소중히 여깁니다. 우리는 몇 년마다 완전히 재설계되어 개발자들에게 깨진 코드와 비용이 많이 드는 마이그레이션을 남기는 프레임워크들을 모두 보아왔습니다. Flight은 다릅니다. Flight v3는 v2의 확장으로 설계되어, 여러분이 알고 사랑하는 API가 제거되지 않았습니다. 실제로 대부분의 v2 프로젝트는 v3에서 아무런 변경 없이 작동할 것입니다.
여러분이 프레임워크를 고치는 대신 앱 구축에 집중할 수 있도록 Flight을 안정적으로 유지하는 데 전념하고 있습니다. 스켈레톤은 새로운 프로젝트에 대해 독단적일 수 있습니다; 코어 API는 다른 모든 사람들에게 친숙하게 유지됩니다.
커뮤니티
Matrix 채팅에 있습니다
그리고 Discord
기여하기
Flight에 기여할 수 있는 두 가지 방법이 있습니다:
- 코어 저장소를 방문하여 코어 프레임워크에 기여하세요.
- 문서를 더 좋게 만들어주세요! 이 문서 웹사이트는 Github에 호스팅되어 있습니다. 오류를 발견하거나 개선하고 싶은 부분이 있으면 풀 리퀘스트를 제출해 주세요. 우리는 업데이트와 새로운 아이디어를 사랑합니다—특히 AI와 새로운 기술에 관한 것들을!
요구사항
Flight은 PHP 7.4 이상이 필요합니다.
참고: PHP 7.4가 지원되는 이유는 현재 작성 시점(2024)에서 PHP 7.4가 일부 LTS Linux 배포판의 기본 버전이기 때문입니다. PHP >8로의 이동을 강제하면 해당 사용자들에게 많은 문제를 야기할 것입니다. 프레임워크는 PHP >8도 지원합니다.
라이선스
Flight은 MIT 라이선스로 배포됩니다.
Awesome-plugins/php_cookie
쿠키
overclokk/cookie은 앱 내에서 쿠키를 관리하는 간단한 라이브러리입니다.
설치
컴포저를 사용하여 설치가 간단합니다.
composer require overclokk/cookie
사용
사용법은 Flight 클래스에 새로운 메소드를 등록하는 것만큼 간단합니다.
use Overclokk\Cookie\Cookie;
/*
* 부트스트랩 또는 public/index.php 파일에서 설정
*/
Flight::register('cookie', Cookie::class);
/**
* ExampleController.php
*/
class ExampleController {
public function login() {
// 쿠키 설정
// 새로운 인스턴스를 받으려면 false로 설정해야 합니다
// 자동완성을 위해 아래 주석을 사용하려면
/** @var \Overclokk\Cookie\Cookie $cookie */
$cookie = Flight::cookie(false);
$cookie->set(
'stay_logged_in', // 쿠키의 이름
'1', // 설정할 값
86400, // 쿠키가 유지될 시간(초)
'/', // 쿠키가 유효한 경로
'example.com', // 쿠키가 유효한 도메인
true, // 안전한 HTTPS 연결로만 쿠키를 전송
true // HTTP 프로토콜을 통해서만 쿠키에 접근 가능
);
// 선택적으로, 기본값을 유지하고 오랫동안 쿠키를 설정하려는 경우
$cookie->forever('stay_logged_in', '1');
}
public function home() {
// 쿠키가 있는지 확인
if (Flight::cookie()->has('stay_logged_in')) {
// 예를 들어 사용자를 대시보드 영역에 넣습니다.
Flight::redirect('/dashboard');
}
}
}Awesome-plugins/php_encryption
PHP 암호화
defuse/php-encryption은 데이터를 암호화하고 복호화하는 데 사용할 수 있는 라이브러리입니다. 시작하고 실행하는 것은 암호화하고 복호화하는 것이 상당히 간단합니다. 그들은 라이브러리 사용법과 암호화에 관한 중요한 보안 요소에 대해 설명하는 훌륭한 튜토리얼이 있습니다.
설치
composer로 간단히 설치할 수 있습니다.
composer require defuse/php-encryption
설정
그런 다음 암호화 키를 생성해야 합니다.
vendor/bin/generate-defuse-key
이 과정에서 안전하게 보관해야 할 키가 표시됩니다. 이 키를 파일 하단에 있는 배열 안에 app/config/config.php 파일에 저장할 수 있습니다. 완벽한 위치는 아니지만 적어도 어딘가에 있습니다.
사용법
이제 라이브러리와 암호화 키가 준비되었으므로 데이터를 암호화하고 복호화할 수 있습니다.
use Defuse\Crypto\Crypto;
use Defuse\Crypto\Key;
/*
* 부트스트랩 또는 public/index.php 파일에 설정
*/
// 암호화 메서드
Flight::map('encrypt', function($raw_data) {
$encryption_key = /* $config['encryption_key'] 또는 키가 위치한 파일에 대한 file_get_contents */;
return Crypto::encrypt($raw_data, Key::loadFromAsciiSafeString($encryption_key));
});
// 복호화 메서드
Flight::map('decrypt', function($encrypted_data) {
$encryption_key = /* $config['encryption_key'] 또는 키가 위치한 파일에 대한 file_get_contents */;
try {
$raw_data = Crypto::decrypt($encrypted_data, Key::loadFromAsciiSafeString($encryption_key));
} catch (Defuse\Crypto\Exception\WrongKeyOrModifiedCiphertextException $ex) {
// 공격! 잘못된 키가 로드되었거나 암호문이 생성된 후 변경되었습니다. 데이터베이스에서 손상된 상태이거나 악의적으로 수정되어 있는 경우입니다.
// ... 응용 프로그램에 적합한 방식으로 이 경우를 처리합니다 ...
}
return $raw_data;
});
Flight::route('/encrypt', function() {
$encrypted_data = Flight::encrypt('비밀 정보입니다');
echo $encrypted_data;
});
Flight::route('/decrypt', function() {
$encrypted_data = '...'; // 어딘가에서 암호화된 데이터 가져오기
$decrypted_data = Flight::decrypt($encrypted_data);
echo $decrypted_data;
});Awesome-plugins/php_file_cache
flightphp/cache
가볍고, 간단하며, 독립형 PHP 파일 내장 캐싱 클래스이며 Wruczek/PHP-File-Cache에서 포크되었습니다.
장점
- 가볍고, 독립형이며, 간단함
- 모든 코드가 하나의 파일에 있음 - 불필요한 드라이버 없음
- 보안성 - 생성된 모든 캐시 파일에는 PHP 헤더와 die가 포함되어 있어, 경로를 알고 서버가 제대로 구성되지 않은 경우에도 직접 접근이 불가능함
- 잘 문서화되어 있으며 테스트됨
- flock을 통해 동시성을 올바르게 처리함
- PHP 7.4+ 지원
- MIT 라이선스 하에 무료
이 문서 사이트는 이 라이브러리를 사용하여 각 페이지를 캐시합니다!
코드 보기는 여기를 클릭하세요.
설치
컴포저를 통해 설치:
composer require flightphp/cache
사용법
사용법은 매우 간단합니다. 캐시 디렉토리에 캐시 파일을 저장합니다.
use flight\Cache;
$app = Flight::app();
// 캐시가 저장될 디렉토리를 생성자에 전달합니다
$app->register('cache', Cache::class, [ __DIR__ . '/../cache/' ], function(Cache $cache) {
// 이 코드는 프로덕션 모드일 때만 캐시가 사용되도록 보장합니다
// ENVIRONMENT는 부트스트랩 파일이나 앱의 다른 곳에서 설정된 상수입니다
$cache->setDevMode(ENVIRONMENT === 'development');
});
캐시 값 가져오기
get() 메서드를 사용하여 캐시된 값을 가져옵니다. 캐시가 만료된 경우 새로 고침하는 편의 메서드를 원하는 경우 refreshIfExpired()를 사용할 수 있습니다.
// 캐시 인스턴스 가져오기
$cache = Flight::cache();
$data = $cache->refreshIfExpired('simple-cache-test', function () {
return date("H:i:s"); // 캐시할 데이터 반환
}, 10); // 10초
// 또는
$data = $cache->get('simple-cache-test');
if(empty($data)) {
$data = date("H:i:s");
$cache->set('simple-cache-test', $data, 10); // 10초
}
캐시 값 저장하기
set() 메서드를 사용하여 캐시에 값을 저장합니다.
Flight::cache()->set('simple-cache-test', 'my cached data', 10); // 10초
캐시 값 삭제하기
delete() 메서드를 사용하여 캐시의 값을 삭제합니다.
Flight::cache()->delete('simple-cache-test');
캐시 값 존재 여부 확인하기
exists() 메서드를 사용하여 캐시에 값이 존재하는지 확인합니다.
if(Flight::cache()->exists('simple-cache-test')) {
// 무언가 수행
}
캐시 지우기
flush() 메서드를 사용하여 전체 캐시를 지웁니다.
Flight::cache()->flush();
캐시에서 메타데이터 추출하기
캐시 항목에 대한 타임스탬프 및 기타 메타데이터를 추출하려면 올바른 매개변수로 true를 전달해야 합니다.
$data = $cache->refreshIfExpired("simple-cache-meta-test", function () {
echo "Refreshing data!" . PHP_EOL;
return date("H:i:s"); // 캐시할 데이터 반환
}, 10, true); // true = 메타데이터와 함께 반환
// 또는
$data = $cache->get("simple-cache-meta-test", true); // true = 메타데이터와 함께 반환
/*
메타데이터와 함께 검색된 캐시 항목 예시:
{
"time":1511667506, <-- 유닉스 타임스탬프 저장
"expire":10, <-- 초 단위 만료 시간
"data":"04:38:26", <-- 역직렬화된 데이터
"permanent":false
}
메타데이터를 사용하면 예를 들어 항목이 저장된 시점이나 만료되는 시점을 계산할 수 있습니다
"data" 키를 통해 데이터 자체에도 접근할 수 있습니다
*/
$expiresin = ($data["time"] + $data["expire"]) - time(); // 데이터가 만료되는 유닉스 타임스탬프를 가져와 현재 타임스탬프에서 빼기
$cacheddate = $data["data"]; // "data" 키를 통해 데이터 자체에 접근
echo "Latest cache save: $cacheddate, expires in $expiresin seconds";
소스 코드
코드 보기는 https://github.com/flightphp/cache를 방문하세요.
Awesome-plugins/permissions
FlightPHP/Permissions
이 모듈은 앱에 여러 역할이 있고 각 역할마다 약간 다른 기능이 있는 경우 프로젝트에서 사용할 수 있는 권한 모듈입니다. 이 모듈을 사용하면 각 역할에 대한 권한을 정의한 다음 현재 사용자가 특정 페이지에 액세스하거나 특정 작업을 수행할 수 있는 권한이 있는지 확인할 수 있습니다.
GitHub의 저장소는 여기를 클릭하세요.
설치
composer require flightphp/permissions를 실행하면 됩니다!
사용법
먼저 권한을 설정한 다음, 앱에 권한이 무엇을 의미하는지 알려야 합니다. 궁극적으로 $Permissions->has(), ->can(), is()를 사용하여 권한을 확인하게 됩니다. has()와 can()은 동일한 기능을 가지지만, 코드의 가독성을 높이기 위해 다르게 이름이 지정되었습니다.
기본 예제
애플리케이션에 사용자가 로그인했는지 확인하는 기능이 있다고 가정해 보겠습니다. 다음과 같이 권한 객체를 만들 수 있습니다:
// index.php
require 'vendor/autoload.php';
// 일부 코드
// 그런 다음 현재 사용자의 역할이 무엇인지 알려주는 코드가 있을 것입니다.
// 세션 변수에서 현재 역할을 가져오는 코드가 있을 것입니다.
// 로그인 후에 정의되며, 그렇지 않으면 'guest' 또는 'public' 역할을 갖게 됩니다.
$current_role = 'admin';
// 권한 설정
$permission = new \flight\Permission($current_role);
$permission->defineRule('loggedIn', function($current_role) {
return $current_role !== 'guest';
});
// 이 객체를 Flight의 어딘가에 저장하고 싶을 것입니다
Flight::set('permission', $permission);
그런 다음 컨트롤러의 어딘가에서 다음과 같은 코드를 작성할 수 있습니다.
<?php
// 일부 컨트롤러
class SomeController {
public function someAction() {
$permission = Flight::get('permission');
if ($permission->has('loggedIn')) {
// 무언가를 수행
} else {
// 다른 작업 수행
}
}
}
또한 애플리케이션에서 사용자가 무언가를 할 수 있는 권한이 있는지 추적하는 데 사용할 수 있습니다. 예를 들어, 사용자가 소프트웨어에 게시물을 올릴 수 있는 방법이 있다면, 특정 작업을 수행할 수 있는 권한이 있는지 확인할 수 있습니다.
$current_role = 'admin';
// 권한 설정
$permission = new \flight\Permission($current_role);
$permission->defineRule('post', function($current_role) {
if($current_role === 'admin') {
$permissions = ['create', 'read', 'update', 'delete'];
} else if($current_role === 'editor') {
$permissions = ['create', 'read', 'update'];
} else if($current_role === 'author') {
$permissions = ['create', 'read'];
} else if($current_role === 'contributor') {
$permissions = ['create'];
} else {
$permissions = [];
}
return $permissions;
});
Flight::set('permission', $permission);
그런 다음 컨트롤러의 어딘가에서...
class PostController {
public function create() {
$permission = Flight::get('permission');
if ($permission->can('post.create')) {
// 무언가를 수행
} else {
// 다른 작업 수행
}
}
}
의존성 주입
권한을 정의하는 클로저에 의존성을 주입할 수 있습니다. 이는 확인하려는 토글, ID 또는 기타 데이터 포인트가 있는 경우 유용합니다. Class->Method 유형 호출에서도 동일하게 작동하지만, 인수를 메서드에 정의합니다.
클로저
$Permission->defineRule('order', function(string $current_role, MyDependency $MyDependency = null) {
// ... 코드
});
// 컨트롤러 파일에서
public function createOrder() {
$MyDependency = Flight::myDependency();
$permission = Flight::get('permission');
if ($permission->can('order.create', $MyDependency)) {
// 무언가를 수행
} else {
// 다른 작업 수행
}
}
클래스
namespace MyApp;
class Permissions {
public function order(string $current_role, MyDependency $MyDependency = null) {
// ... 코드
}
}
클래스로 권한 설정하기 위한 단축키
클래스를 사용하여 권한을 정의할 수도 있습니다. 권한이 많고 코드를 깔끔하게 유지하려는 경우 유용합니다. 다음과 같이 할 수 있습니다:
<?php
// 부트스트랩 코드
$Permissions = new \flight\Permission($current_role);
$Permissions->defineRule('order', 'MyApp\Permissions->order');
// myapp/Permissions.php
namespace MyApp;
class Permissions {
public function order(string $current_role, int $user_id) {
// 미리 설정했다고 가정
/** @var \flight\database\SimplePdo $db */
$db = Flight::db();
$allowed_permissions = [ 'read' ]; // 모든 사용자가 주문을 볼 수 있음
if($current_role === 'manager') {
$allowed_permissions[] = 'create'; // 관리자는 주문을 생성할 수 있음
}
$some_special_toggle_from_db = $db->fetchField('SELECT some_special_toggle FROM settings WHERE id = ?', [ $user_id ]);
if($some_special_toggle_from_db) {
$allowed_permissions[] = 'update'; // 사용자가 특별한 토글이 있는 경우 주문을 업데이트할 수 있음
}
if($current_role === 'admin') {
$allowed_permissions[] = 'delete'; // 관리자는 주문을 삭제할 수 있음
}
return $allowed_permissions;
}
}
멋진 부분은 (캐시도 가능!!!) 단축키를 사용할 수 있다는 것입니다. 권한 클래스에 클래스 내의 모든 메서드를 권한으로 매핑하도록 지시하기만 하면 됩니다. order()라는 메서드와 company()라는 메서드가 있는 경우, 자동으로 매핑되므로 $Permissions->has('order.read') 또는 $Permissions->has('company.read')를 실행하면 작동합니다. 정의하기는 매우 까다로우므로 잘 따라오세요. 다음을 수행하기만 하면 됩니다:
그룹화하려는 권한 클래스를 만듭니다.
class MyPermissions {
public function order(string $current_role, int $order_id = 0): array {
// 권한을 결정하는 코드
return $permissions_array;
}
public function company(string $current_role, int $company_id): array {
// 권한을 결정하는 코드
return $permissions_array;
}
}
그런 다음 이 라이브러리를 사용하여 권한을 검색 가능하게 만듭니다.
$Permissions = new \flight\Permission($current_role);
$Permissions->defineRulesFromClassMethods(MyApp\Permissions::class);
Flight::set('permissions', $Permissions);
마지막으로, 코드베이스에서 권한을 호출하여 사용자가 주어진 권한을 수행할 수 있는지 확인합니다.
class SomeController {
public function createOrder() {
if(Flight::get('permissions')->can('order.create') === false) {
die('주문을 생성할 수 없습니다. 죄송합니다!');
}
}
}
캐싱
캐싱을 활성화하려면 간단한 wruczak/phpfilecache 라이브러리를 참조하세요. 활성화하는 예는 아래에 나와 있습니다.
// 이 $app은 코드의 일부이거나,
// null을 전달하면 생성자에서 Flight::app()에서 가져옵니다.
$app = Flight::app();
// 현재로서는 파일 캐시로 이를 수락합니다. 다른 것은 쉽게
// 미래에 추가할 수 있습니다.
$Cache = new Wruczek\PhpFileCache\PhpFileCache;
$Permissions = new \flight\Permission($current_role, $app, $Cache);
$Permissions->defineRulesFromClassMethods(MyApp\Permissions::class, 3600); // 캐싱할 시간(초)입니다. 캐싱을 사용하지 않으려면 이 값을 생략하세요.
이제 시작하세요!
Awesome-plugins/simple_job_queue
간단한 작업 큐
간단한 작업 큐는 비동기적으로 작업을 처리하는 데 사용할 수 있는 라이브러리입니다. beanstalkd, MySQL/MariaDB, SQLite 및 PostgreSQL과 함께 사용할 수 있습니다.
설치
composer require n0nag0n/simple-job-queue
사용법
이 기능이 작동하려면 작업을 큐에 추가하는 방법과 작업을 처리하는 방법(작업자)이 필요합니다. 아래는 큐에 작업을 추가하는 방법과 작업을 처리하는 방법의 예입니다.
Flight에 추가하기
Flight에 이를 추가하는 것은 간단하며 register() 메서드를 사용하여 수행됩니다. 아래는 이를 Flight에 추가하는 방법의 예입니다.
<?php
require 'vendor/autoload.php';
// beanstalkd를 사용하려면 ['mysql']을 ['beanstalkd']로 변경하십시오.
Flight::register('queue', n0nag0n\Job_Queue::class, ['mysql'], function($Job_Queue) {
// Flight::db();에서 이미 PDO 연결이 있는 경우
$Job_Queue->addQueueConnection(Flight::db());
// 또는 beanstalkd/Pheanstalk를 사용하는 경우
$pheanstalk = Pheanstalk\Pheanstalk::create('127.0.0.1');
$Job_Queue->addQueueConnection($pheanstalk);
});
새로운 작업 추가하기
작업을 추가할 때는 파이프라인(큐)을 지정해야 합니다. 이는 RabbitMQ의 채널 또는 beanstalkd의 튜브에 해당합니다.
<?php
Flight::queue()->selectPipeline('send_important_emails');
Flight::queue()->addJob(json_encode([ 'something' => 'that', 'ends' => 'up', 'a' => 'string' ]));
작업자 실행하기
작업자를 실행하는 방법에 대한 예제 파일입니다.
<?php
require 'vendor/autoload.php';
$Job_Queue = new n0nag0n\Job_Queue('mysql');
// PDO 연결
$PDO = new PDO('mysql:dbname=testdb;host=127.0.0.1', 'user', 'pass');
$Job_Queue->addQueueConnection($PDO);
// 또는 beanstalkd/Pheanstalk를 사용하는 경우
$pheanstalk = Pheanstalk\Pheanstalk::create('127.0.0.1');
$Job_Queue->addQueueConnection($pheanstalk);
$Job_Queue->watchPipeline('send_important_emails');
while(true) {
$job = $Job_Queue->getNextJobAndReserve();
// 당신이 편안하게 잘 수 있도록 조정하세요 (데이터베이스 큐에만 해당, beanstalkd는 이 조건문이 필요하지 않습니다)
if(empty($job)) {
usleep(500000);
continue;
}
echo "처리 중 {$job['id']}\n";
$payload = json_decode($job['payload'], true);
try {
$result = doSomethingThatDoesSomething($payload);
if($result === true) {
$Job_Queue->deleteJob($job);
} else {
// 이는 준비된 큐에서 제거하고 나중에拾게 하거나 "찔러넣기"할 수 있는 다른 큐에 넣습니다.
$Job_Queue->buryJob($job);
}
} catch(Exception $e) {
$Job_Queue->buryJob($job);
}
}
Supervisord로 긴 프로세스 처리하기
Supervisord는 작업자 프로세스가 지속적으로 실행되는 것을 보장하는 프로세스 제어 시스템입니다. 다음은 간단한 작업 큐 작업자와 함께 설정하는 방법에 대한 보다 완전한 가이드입니다:
Supervisord 설치하기
# Ubuntu/Debian에서
sudo apt-get install supervisor
# CentOS/RHEL에서
sudo yum install supervisor
# macOS에서 Homebrew를 사용하여
brew install supervisor
작업자 스크립트 만들기
우선, 작업자 코드를 전용 PHP 파일에 저장합니다:
<?php
require 'vendor/autoload.php';
$Job_Queue = new n0nag0n\Job_Queue('mysql');
// PDO 연결
$PDO = new PDO('mysql:dbname=your_database;host=127.0.0.1', 'username', 'password');
$Job_Queue->addQueueConnection($PDO);
// 감시할 파이프라인 설정
$Job_Queue->watchPipeline('send_important_emails');
// 작업자 시작 로그
echo date('Y-m-d H:i:s') . " - 작업자 시작됨\n";
while(true) {
$job = $Job_Queue->getNextJobAndReserve();
if(empty($job)) {
usleep(500000); // 0.5초 동안 일시 정지
continue;
}
echo date('Y-m-d H:i:s') . " - 작업 {$job['id']} 처리 중\n";
$payload = json_decode($job['payload'], true);
try {
$result = doSomethingThatDoesSomething($payload);
if($result === true) {
$Job_Queue->deleteJob($job);
echo date('Y-m-d H:i:s') . " - 작업 {$job['id']} 성공적으로 완료됨\n";
} else {
$Job_Queue->buryJob($job);
echo date('Y-m-d H:i:s') . " - 작업 {$job['id']} 실패, 묻힘\n";
}
} catch(Exception $e) {
$Job_Queue->buryJob($job);
echo date('Y-m-d H:i:s') . " - 작업 {$job['id']} 처리 중 예외: {$e->getMessage()}\n";
}
}
Supervisord 구성하기
작업자에 대한 구성 파일을 생성합니다:
[program:email_worker]
command=php /path/to/worker.php
directory=/path/to/project
autostart=true
autorestart=true
startretries=3
stderr_logfile=/var/log/simple_job_queue_err.log
stdout_logfile=/var/log/simple_job_queue.log
user=www-data
numprocs=2
process_name=%(program_name)s_%(process_num)02d
주요 구성 옵션:
command: 작업자를 실행할 명령directory: 작업자의 작업 디렉토리autostart: supervisord 시작 시 자동으로 시작autorestart: 프로세스가 종료되면 자동으로 다시 시작startretries: 실패 시 시작을 재시도할 횟수stderr_logfile/stdout_logfile: 로그 파일 위치user: 프로세스를 실행할 시스템 사용자numprocs: 실행할 작업자 인스턴스 수process_name: 여러 작업자 프로세스의 이름 형식
Supervisorctl로 작업자 관리하기
구성을 생성하거나 수정한 후:
# supervisor 구성 다시 로드
sudo supervisorctl reread
sudo supervisorctl update
# 특정 작업자 프로세스 제어
sudo supervisorctl start email_worker:*
sudo supervisorctl stop email_worker:*
sudo supervisorctl restart email_worker:*
sudo supervisorctl status email_worker:*
여러 파이프라인 실행하기
여러 개의 파이프라인을 위해 별도의 작업자 파일 및 구성을 생성합니다:
[program:email_worker]
command=php /path/to/email_worker.php
# ... 기타 설정 ...
[program:notification_worker]
command=php /path/to/notification_worker.php
# ... 기타 설정 ...
모니터링 및 로그
로그를 확인하여 작업자 활동을 모니터링합니다:
# 로그 보기
sudo tail -f /var/log/simple_job_queue.log
# 상태 확인
sudo supervisorctl status
이 설정은 작업 큐 시스템이 충돌, 서버 재부팅 또는 기타 문제 발생 후에도 계속 실행되도록 하여 프로덕션 환경에서 신뢰할 수 있는 시스템이 되도록 보장합니다.
Awesome-plugins/jwt
Firebase JWT - JSON Web Token 인증
JWT (JSON Web Tokens)는 애플리케이션과 클라이언트 간의 클레임을 표현하는 컴팩트하고 URL 안전한 방법입니다. 서버 측 세션 저장소가 필요 없는 stateless API 인증에 완벽합니다! 이 가이드는 Firebase JWT를 Flight와 통합하여 안전한 토큰 기반 인증을 구현하는 방법을 보여줍니다.
전체 문서와 세부 사항은 Github 저장소를 방문하세요.
JWT란 무엇인가?
JSON Web Token은 세 부분으로 구성된 문자열입니다:
- 헤더: 토큰에 대한 메타데이터 (알고리즘, 유형)
- 페이로드: 데이터 (사용자 ID, 역할, 만료 등)
- 서명: 진위성을 확인하기 위한 암호화 서명
예제 JWT: eyJ0eXAiOiJKV1QiLCJhbGc... (무의미해 보이지만 구조화된 데이터입니다!)
JWT를 사용하는 이유는?
- Stateless: 서버 측 세션 저장소가 필요 없음—마이크로서비스와 API에 완벽
- 확장 가능: 세션 affinity 요구 사항이 없어 로드 밸런서와 잘 작동
- 크로스 도메인: 다른 도메인과 서비스 간에 사용할 수 있음
- 모바일 친화적: 쿠키가 잘 작동하지 않는 모바일 앱에 훌륭
- 표준화: 산업 표준 접근 방식 (RFC 7519)
설치
Composer를 통해 설치하세요:
composer require firebase/php-jwt
기본 사용법
JWT 생성 및 검증의 간단한 예제입니다:
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
// 비밀 키 (이것을 안전하게 보관하세요!)
$secretKey = 'your-256-bit-secret-key-here-keep-it-safe';
// 토큰 생성
$payload = [
'user_id' => 123,
'username' => 'johndoe',
'role' => 'admin',
'iat' => time(), // 발급 시각
'exp' => time() + 3600 // 1시간 후 만료
];
$jwt = JWT::encode($payload, $secretKey, 'HS256');
echo "Token: " . $jwt;
// 토큰 검증 및 디코딩
try {
$decoded = JWT::decode($jwt, new Key($secretKey, 'HS256'));
echo "User ID: " . $decoded->user_id;
} catch (Exception $e) {
echo "Invalid token: " . $e->getMessage();
}
Flight를 위한 JWT 미들웨어 (권장 접근 방식)
Flight에서 JWT를 사용하는 가장 일반적이고 유용한 방법은 API 경로를 보호하는 미들웨어입니다. 완전하고 프로덕션 준비된 예제입니다:
단계 1: JWT 미들웨어 클래스 생성
// app/middleware/JwtMiddleware.php
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
use Firebase\JWT\ExpiredException;
use Firebase\JWT\SignatureInvalidException;
use flight\Engine;
class JwtMiddleware {
protected Engine $app;
protected string $secretKey;
public function __construct(Engine $app) {
$this->app = $app;
// 비밀 키를 app/config/config.php에 저장하세요, 하드코딩하지 마세요!
$this->secretKey = $app->get('config')['jwt_secret'];
}
public function before(array $params) {
$authHeader = $this->app->request()->getHeader('Authorization');
// Authorization 헤더 존재 여부 확인
if (empty($authHeader)) {
$this->app->jsonHalt(['error' => 'No authorization token provided'], 401);
}
// "Bearer <token>" 형식에서 토큰 추출
if (!preg_match('/Bearer\s+(.*)$/i', $authHeader, $matches)) {
$this->app->jsonHalt(['error' => 'Invalid authorization format. Use: Bearer <token>'], 401);
}
$jwt = $matches[1];
try {
// 토큰 디코딩 및 검증
$decoded = JWT::decode($jwt, new Key($this->secretKey, 'HS256'));
// 라우트 핸들러에서 사용하기 위해 사용자 데이터를 요청에 저장
$this->app->request()->data->user = $decoded;
} catch (ExpiredException $e) {
$this->app->jsonHalt(['error' => 'Token has expired'], 401);
} catch (SignatureInvalidException $e) {
$this->app->jsonHalt(['error' => 'Invalid token signature'], 401);
} catch (Exception $e) {
$this->app->jsonHalt(['error' => 'Invalid token: ' . $e->getMessage()], 401);
}
}
}
단계 2: 구성에 JWT 비밀 키 등록
// app/config/config.php
return [
'jwt_secret' => getenv('JWT_SECRET') ?: 'your-fallback-secret-for-development'
];
// app/config/bootstrap.php 또는 index.php
// 구성 파일을 앱에 노출하려면 이 줄을 추가하세요
$app->set('config', $config);
보안 주의: 비밀 키를 하드코딩하지 마세요! 프로덕션에서는 환경 변수를 사용하세요.
단계 3: 미들웨어로 경로 보호
// 단일 경로 보호
Flight::route('GET /api/user/profile', function() {
$user = Flight::request()->data->user; // 미들웨어에서 설정
Flight::json([
'user_id' => $user->user_id,
'username' => $user->username,
'role' => $user->role
]);
})->addMiddleware(JwtMiddleware::class);
// 전체 경로 그룹 보호 (더 일반적!)
Flight::group('/api', function() {
Flight::route('GET /users', function() { /* ... */ });
Flight::route('GET /posts', function() { /* ... */ });
Flight::route('POST /posts', function() { /* ... */ });
Flight::route('DELETE /posts/@id', function($id) { /* ... */ });
}, [ JwtMiddleware::class ]); // 이 그룹의 모든 경로가 보호됩니다!
미들웨어에 대한 자세한 내용은 미들웨어 문서를 참조하세요.
일반적인 사용 사례
1. 로그인 엔드포인트 (토큰 생성)
인증 성공 후 JWT를 생성하는 경로를 만드세요:
Flight::route('POST /api/login', function() {
$data = Flight::request()->data;
$username = $data->username ?? '';
$password = $data->password ?? '';
// 자격 증명 검증 (예제 - 자체 로직 사용!)
$user = validateUserCredentials($username, $password);
if (!$user) {
Flight::jsonHalt(['error' => 'Invalid credentials'], 401);
}
// JWT 생성
$secretKey = Flight::get('config')['jwt_secret'];
$payload = [
'user_id' => $user->id,
'username' => $user->username,
'role' => $user->role,
'iat' => time(),
'exp' => time() + (60 * 60) // 1시간 만료
];
$jwt = JWT::encode($payload, $secretKey, 'HS256');
Flight::json([
'success' => true,
'token' => $jwt,
'expires_in' => 3600
]);
});
function validateUserCredentials($username, $password) {
// 데이터베이스 조회 및 비밀번호 검증은 여기에
// 예제:
$db = Flight::db();
$user = $db->fetchRow("SELECT * FROM users WHERE username = ?", [$username]);
if ($user && password_verify($password, $user['password_hash'])) {
return (object) [
'id' => $user['id'],
'username' => $user['username'],
'role' => $user['role']
];
}
return null;
}
2. 토큰 갱신 흐름
장기 세션을 위한 리프레시 토큰 시스템을 구현하세요:
Flight::route('POST /api/login', function() {
// ... 자격 증명 검증 ...
$secretKey = Flight::get('config')['jwt_secret'];
$refreshSecret = Flight::get('config')['jwt_refresh_secret'];
// 단기 액세스 토큰 (15분)
$accessToken = JWT::encode([
'user_id' => $user->id,
'type' => 'access',
'iat' => time(),
'exp' => time() + (15 * 60)
], $secretKey, 'HS256');
// 장기 리프레시 토큰 (7일)
$refreshToken = JWT::encode([
'user_id' => $user->id,
'type' => 'refresh',
'iat' => time(),
'exp' => time() + (7 * 24 * 60 * 60)
], $refreshSecret, 'HS256');
Flight::json([
'access_token' => $accessToken,
'refresh_token' => $refreshToken,
'expires_in' => 900
]);
});
Flight::route('POST /api/refresh', function() {
$refreshToken = Flight::request()->data->refresh_token ?? '';
$refreshSecret = Flight::get('config')['jwt_refresh_secret'];
try {
$decoded = JWT::decode($refreshToken, new Key($refreshSecret, 'HS256'));
// 리프레시 토큰인지 확인
if ($decoded->type !== 'refresh') {
Flight::jsonHalt(['error' => 'Invalid token type'], 401);
}
// 새 액세스 토큰 생성
$secretKey = Flight::get('config')['jwt_secret'];
$accessToken = JWT::encode([
'user_id' => $decoded->user_id,
'type' => 'access',
'iat' => time(),
'exp' => time() + (15 * 60)
], $secretKey, 'HS256');
Flight::json([
'access_token' => $accessToken,
'expires_in' => 900
]);
} catch (Exception $e) {
Flight::jsonHalt(['error' => 'Invalid refresh token'], 401);
}
});
3. 역할 기반 액세스 제어
미들웨어를 확장하여 사용자 역할을 확인하세요:
class JwtRoleMiddleware {
protected Engine $app;
protected array $allowedRoles;
public function __construct(Engine $app, array $allowedRoles = []) {
$this->app = $app;
$this->allowedRoles = $allowedRoles;
}
public function before(array $params) {
// JwtMiddleware가 이미 실행되어 사용자 데이터를 설정했다고 가정
$user = $this->app->request()->data->user ?? null;
if (!$user) {
$this->app->jsonHalt(['error' => 'Authentication required'], 401);
}
// 필요한 역할이 있는지 확인
if (!empty($this->allowedRoles) && !in_array($user->role, $this->allowedRoles)) {
$this->app->jsonHalt(['error' => 'Insufficient permissions'], 403);
}
}
}
// 사용: 관리자 전용 경로
Flight::route('DELETE /api/users/@id', function($id) {
// 사용자 삭제 로직
})->addMiddleware([
JwtMiddleware::class,
new JwtRoleMiddleware(Flight::app(), ['admin'])
]);
4. 사용자별 속도 제한이 있는 공용 API
세션 없이 JWT를 사용하여 사용자 추적 및 속도 제한:
class RateLimitMiddleware {
public function before(array $params) {
$user = Flight::request()->data->user ?? null;
$userId = $user ? $user->user_id : Flight::request()->ip;
$cacheKey = "rate_limit:$userId";
// app/config/services.php에 캐시 서비스를 설정하세요
$requests = Flight::cache()->get($cacheKey, 0);
if ($requests >= 100) { // 시간당 100 요청
Flight::jsonHalt(['error' => 'Rate limit exceeded'], 429);
}
Flight::cache()->set($cacheKey, $requests + 1, 3600);
}
}
보안 모범 사례
1. 강력한 비밀 키 사용
// 안전한 비밀 키 생성 (한 번 실행하고 .env 파일에 저장)
$secretKey = base64_encode(random_bytes(32));
echo $secretKey; // .env 파일에 저장하세요!
2. 환경 변수에 비밀 저장
// 비밀을 버전 제어에 커밋하지 마세요!
// .env 파일과 vlucas/phpdotenv 같은 라이브러리 사용
// .env 파일:
// JWT_SECRET=your-base64-encoded-secret-here
// JWT_REFRESH_SECRET=another-base64-encoded-secret-here
// 앱 구성 파일을 사용하여 비밀 저장할 수도 있음
// 구성 파일이 버전 제어에 커밋되지 않도록 하세요
// return [
// 'jwt_secret' => 'your-base64-encoded-secret-here',
// 'jwt_refresh_secret' => 'another-base64-encoded-secret-here',
// ];
// 앱에서:
$secretKey = getenv('JWT_SECRET');
3. 적절한 만료 시간 설정
// 좋은 관행: 단기 액세스 토큰
'exp' => time() + (15 * 60) // 15분
// 리프레시 토큰: 더 긴 만료
'exp' => time() + (7 * 24 * 60 * 60) // 7일
4. 프로덕션에서 HTTPS 사용
JWT는 항상 HTTPS를 통해 전송되어야 합니다. 프로덕션에서 평문 HTTP로 토큰을 보내지 마세요!
5. 토큰 클레임 검증
중요한 클레임을 항상 검증하세요:
$decoded = JWT::decode($jwt, new Key($secretKey, 'HS256'));
// 만료 확인은 라이브러리에서 자동 처리
// 하지만 사용자 지정 검증 추가 가능:
if ($decoded->iat > time()) {
throw new Exception('Token used before it was issued');
}
if (isset($decoded->nbf) && $decoded->nbf > time()) {
throw new Exception('Token not yet valid');
}
6. 로그아웃을 위한 토큰 블랙리스트 고려
추가 보안을 위해 무효화된 토큰의 블랙리스트를 유지하세요:
Flight::route('POST /api/logout', function() {
$authHeader = Flight::request()->getHeader('Authorization');
preg_match('/Bearer\s+(.*)$/i', $authHeader, $matches);
$jwt = $matches[1];
// 토큰 만료 추출
$decoded = Flight::request()->data->user;
$ttl = $decoded->exp - time();
// 만료까지 캐시/Redis에 저장
Flight::cache()->set("blacklist:$jwt", true, $ttl);
Flight::json(['message' => 'Successfully logged out']);
});
// JwtMiddleware에 추가:
public function before(array $params) {
// ... JWT 추출 ...
// 블랙리스트 확인
if (Flight::cache()->get("blacklist:$jwt")) {
$this->app->jsonHalt(['error' => 'Token has been revoked'], 401);
}
// ... 토큰 검증 ...
}
알고리즘 및 키 유형
Firebase JWT는 여러 알고리즘을 지원합니다:
대칭 알고리즘 (HMAC)
- HS256 (대부분의 앱에 권장): 단일 비밀 키 사용
- HS384, HS512: 더 강력한 변형
$jwt = JWT::encode($payload, $secretKey, 'HS256');
$decoded = JWT::decode($jwt, new Key($secretKey, 'HS256'));
비대칭 알고리즘 (RSA/ECDSA)
- RS256, RS384, RS512: 공개/비공개 키 쌍 사용
- ES256, ES384, ES512: 타원 곡선 변형
// 키 생성: openssl genrsa -out private.key 2048
// openssl rsa -in private.key -pubout -out public.key
$privateKey = file_get_contents('/path/to/private.key');
$publicKey = file_get_contents('/path/to/public.key');
// 비공개 키로 인코딩
$jwt = JWT::encode($payload, $privateKey, 'RS256');
// 공개 키로 디코딩
$decoded = JWT::decode($jwt, new Key($publicKey, 'RS256'));
RSA를 사용할 때: 검증을 위해 공개 키를 배포해야 할 때 사용 (예: 마이크로서비스, 타사 통합). 단일 애플리케이션의 경우 HS256이 더 간단하고 충분합니다.
문제 해결
"Expired token" 오류
토큰의 exp 클레임이 과거입니다. 새 토큰을 발급하거나 토큰 갱신을 구현하세요.
"Signature verification failed"
- 인코딩에 사용한 비밀 키와 다른 키로 디코딩 중
- 토큰이 변조됨
- 서버 간 시계 왜곡 (leeway 버퍼 추가)
use Firebase\JWT\JWT;
JWT::$leeway = 60; // 60초 시계 왜곡 허용
$decoded = JWT::decode($jwt, new Key($secretKey, 'HS256'));
요청에서 토큰이 전송되지 않음
클라이언트가 Authorization 헤더를 보내는지 확인하세요:
// JavaScript 예제
fetch('/api/users', {
headers: {
'Authorization': 'Bearer ' + token
}
});
메서드
Firebase JWT 라이브러리는 다음 핵심 메서드를 제공합니다:
JWT::encode(array $payload, string $key, string $alg): 페이로드에서 JWT 생성JWT::decode(string $jwt, Key $key): JWT 디코딩 및 검증JWT::urlsafeB64Encode(string $input): Base64 URL 안전 인코딩JWT::urlsafeB64Decode(string $input): Base64 URL 안전 디코딩JWT::$leeway: 검증을 위한 시간 여유 설정 (초 단위) 정적 속성
이 라이브러리를 사용하는 이유는?
- 산업 표준: Firebase JWT는 PHP에서 가장 인기 있고 신뢰받는 JWT 라이브러리
- 활성 유지보수: Google/Firebase 팀에서 유지보수
- 보안 중심: 정기 업데이트와 보안 패치
- 간단한 API: 이해하고 구현하기 쉬움
- 잘 문서화됨: 광범위한 문서와 커뮤니티 지원
- 유연함: 여러 알고리즘과 구성 가능한 옵션 지원
관련 자료
- Firebase JWT Github 저장소
- JWT.io - JWT 디버그 및 디코딩
- RFC 7519 - 공식 JWT 사양
- Flight 미들웨어 문서
- Flight 세션 플러그인 - 전통적인 세션 기반 인증용
라이선스
Firebase JWT 라이브러리는 BSD 3-Clause License로 라이선스되었습니다. 세부 사항은 Github 저장소를 참조하세요.
Awesome-plugins/n0nag0n_wordpress
워드프레스 통합: n0nag0n/wordpress-integration-for-flight-framework
워드프레스 사이트에서 Flight PHP를 사용하고 싶으신가요? 이 플러그인은 이를 매우 쉽게 만들어줍니다! n0nag0n/wordpress-integration-for-flight-framework를 사용하면 WordPress 설치와 함께 완전한 Flight 앱을 실행할 수 있습니다—커스텀 API, 마이크로서비스, 또는 완전한 기능을 갖춘 앱을 빌드하는 데 완벽합니다.
이 플러그인이 하는 일?
- Flight PHP를 WordPress와 원활하게 통합
- URL 패턴에 따라 요청을 Flight 또는 WordPress로 라우팅
- 컨트롤러, 모델, 뷰(MVC)로 코드를 구성
- 추천되는 Flight 폴더 구조를 쉽게 설정
- WordPress의 데이터베이스 연결 또는 자체 연결 사용
- Flight와 WordPress 간 상호작용을 미세 조정
- 간단한 관리자 인터페이스에서 구성
설치
flight-integration폴더를/wp-content/plugins/디렉터리에 업로드합니다.- WordPress 관리자(플러그인 메뉴)에서 플러그인을 활성화합니다.
- 설정 > Flight Framework로 이동하여 플러그인을 구성합니다.
- Flight 설치의 벤더 경로를 설정합니다(또는 Composer를 사용하여 Flight를 설치).
- 앱 폴더 경로를 구성하고 폴더 구조를 생성합니다(플러그인이 이를 도와줍니다!).
- Flight 애플리케이션 빌드를 시작합니다!
사용 예시
기본 라우트 예시
앱의 app/config/routes.php 파일에서:
Flight::route('GET /api/hello', function() {
Flight::json(['message' => 'Hello World!']);
});
컨트롤러 예시
app/controllers/ApiController.php에 컨트롤러를 생성합니다:
namespace app\controllers;
use Flight;
class ApiController {
public function getUsers() {
// 워드프레스 함수를 Flight 내부에서 사용할 수 있습니다!
$users = get_users();
$result = [];
foreach($users as $user) {
$result[] = [
'id' => $user->ID,
'name' => $user->display_name,
'email' => $user->user_email
];
}
Flight::json($result);
}
}
그런 다음, routes.php에서:
Flight::route('GET /api/users', [app\controllers\ApiController::class, 'getUsers']);
FAQ
Q: 이 플러그인을 사용하려면 Flight를 알아야 하나요?
A: 네, 이는 WordPress 내에서 Flight를 사용하고 싶어하는 개발자를 위한 것입니다. Flight의 라우팅과 요청 처리에 대한 기본 지식이 권장됩니다.
Q: 이 플러그인이 WordPress 사이트 속도를 저하시킬까요?
A: 아니요! 플러그인은 Flight 라우트와 일치하는 요청만 처리합니다. 다른 모든 요청은 평소처럼 WordPress로 이동합니다.
Q: Flight 앱에서 WordPress 함수를 사용할 수 있나요?
A: 물론입니다! Flight 라우트와 컨트롤러 내에서 모든 WordPress 함수, 후크, 전역 변수에 완전히 접근할 수 있습니다.
Q: 커스텀 라우트를 어떻게 생성하나요?
A: 앱 폴더의 config/routes.php 파일에서 라우트를 정의합니다. 폴더 구조 생성기가 만든 샘플 파일을 참조하여 예시를 확인하세요.
변경 로그
1.0.0
초기 릴리스.
추가 정보는 GitHub repo를 확인하세요.
Awesome-plugins/ghost_session
Ghostff/Session
PHP 세션 관리자 (비동기식, 플래시, 세그먼트, 세션 암호화). PHP open_ssl을 사용하여 세션 데이터를 선택적으로 암호화/복호화합니다. File, MySQL, Redis, 및 Memcached를 지원합니다.
클릭 여기 코드를 보기 위해.
설치
Composer를 사용하여 설치하세요.
composer require ghostff/session
기본 구성
기본 설정을 사용하려면 아무것도 전달할 필요가 없습니다. 더 많은 설정에 대해서는 Github Readme를 참조하세요.
use Ghostff\Session\Session;
require 'vendor/autoload.php';
$app = Flight::app();
$app->register('session', Session::class);
// 한 가지 기억할 점은 각 페이지 로드 시 세션을 커밋해야 한다는 것입니다
// 또는 구성에서 auto_commit을 실행해야 합니다.
간단한 예제
사용 방법의 간단한 예제입니다.
Flight::route('POST /login', function() {
$session = Flight::session();
// 여기에 로그인 로직을 수행하세요
// 비밀번호를 검증하는 등
// 로그인 성공 시
$session->set('is_logged_in', true);
$session->set('user', $user);
// 세션에 작성할 때마다 의도적으로 커밋해야 합니다.
$session->commit();
});
// 이 검사는 제한된 페이지 로직에서 수행되거나 미들웨어로 래핑될 수 있습니다.
Flight::route('/some-restricted-page', function() {
$session = Flight::session();
if(!$session->get('is_logged_in')) {
Flight::redirect('/login');
}
// 여기에서 제한된 페이지 로직을 수행하세요
});
// 미들웨어 버전
Flight::route('/some-restricted-page', function() {
// 일반 페이지 로직
})->addMiddleware(function() {
$session = Flight::session();
if(!$session->get('is_logged_in')) {
Flight::redirect('/login');
}
});
더 복잡한 예제
사용 방법의 더 복잡한 예제입니다.
use Ghostff\Session\Session;
require 'vendor/autoload.php';
$app = Flight::app();
// 세션 구성 파일의 사용자 정의 경로를 첫 번째 인자로 설정하세요
// 또는 사용자 정의 배열을 제공하세요
$app->register('session', Session::class, [
[
// 세션 데이터를 데이터베이스에 저장하려는 경우 (예: "모든 장치에서 로그아웃" 기능과 같은 것)
Session::CONFIG_DRIVER => Ghostff\Session\Drivers\MySql::class,
Session::CONFIG_ENCRYPT_DATA => true,
Session::CONFIG_SALT_KEY => hash('sha256', 'my-super-S3CR3T-salt'), // 이것을 다른 것으로 변경하세요
Session::CONFIG_AUTO_COMMIT => true, // 이것이 필요하거나 커밋()하기가 어렵다면만 수행하세요.
// 추가적으로 Flight::after('start', function() { Flight::session()->commit(); });을 사용할 수 있습니다.
Session::CONFIG_MYSQL_DS => [
'driver' => 'mysql', # PDO dns용 데이터베이스 드라이버(예: mysql:host=...;dbname=...)
'host' => '127.0.0.1', # 데이터베이스 호스트
'db_name' => 'my_app_database', # 데이터베이스 이름
'db_table' => 'sessions', # 데이터베이스 테이블
'db_user' => 'root', # 데이터베이스 사용자 이름
'db_pass' => '', # 데이터베이스 비밀번호
'persistent_conn'=> false, # 스크립트가 데이터베이스와 통신할 때마다 새로운 연결을 설정하는 오버헤드를 피합니다. 단점은 스스로 찾아보세요
]
]
]);
도움! 내 세션 데이터가 유지되지 않아요!
세션 데이터를 설정했는데 요청 사이에 유지되지 않나요? 세션 데이터를 커밋하는 것을 잊었을 수 있습니다. 세션 데이터를 설정한 후 $session->commit()을 호출하세요.
Flight::route('POST /login', function() {
$session = Flight::session();
// 여기에 로그인 로직을 수행하세요
// 비밀번호를 검증하는 등
// 로그인 성공 시
$session->set('is_logged_in', true);
$session->set('user', $user);
// 세션에 작성할 때마다 의도적으로 커밋해야 합니다.
$session->commit();
});
이것을 해결하는 다른 방법은 세션 서비스를 설정할 때 구성에 auto_commit을 true로 설정하는 것입니다. 이렇게 하면 각 요청 후 세션 데이터를 자동으로 커밋합니다.
$app->register('session', Session::class, [ 'path/to/session_config.php', bin2hex(random_bytes(32)) ], function(Session $session) {
$session->updateConfiguration([
Session::CONFIG_AUTO_COMMIT => true,
]);
}
);
또한 Flight::after('start', function() { Flight::session()->commit(); });을 사용하여 각 요청 후 세션 데이터를 커밋할 수 있습니다.
문서
전체 문서를 위해 Github Readme를 방문하세요. 구성 옵션은 default_config.php 파일 자체에 잘 문서화되어 있습니다. 이 패키지를 직접 살펴보고 싶다면 코드는 이해하기 쉽습니다.
Awesome-plugins/mcp
FlightPHP MCP 서버
FlightPHP MCP 서버는 MCP 호환 AI 코딩 어시스턴트에 FlightPHP 문서 전체 — 라우팅, 미들웨어, 플러그인, 가이드 등 — 에 대한 즉시 구조화된 액세스를 제공합니다. AI가 API 세부 사항을 환각하거나 메서드 시그니처를 추측하는 대신, 필요에 따라 실제 문서를 가져옵니다. API 키가 필요 없으며, 호스팅 버전의 경우 설치가 필요 없습니다.
전체 소스 코드와 세부 사항을 위해 Github 저장소를 방문하세요.
빠른 시작
서버는 공개적으로 호스팅되어 있으며 사용 준비가 되었습니다:
https://mcp.flightphp.com/mcp
이 URL을 AI 코딩 확장에 추가하기만 하면 됩니다. 가입이나 자격 증명이 필요 없습니다. 아래 IDE 구성 섹션에서 가장 인기 있는 도구에 대한 복사-붙여넣기 구성 옵션을 확인하세요.
기능
연결되면 AI 어시스턴트는 다음을 할 수 있습니다:
- 모든 사용 가능한 문서 탐색 — 모든 핵심 주제, 가이드 및 플러그인 페이지를 나열
- 문서 페이지 가져오기 — 라우팅, 미들웨어, 요청, 보안 등에 대한 전체 콘텐츠 검색
- 플러그인 문서 조회 — ActiveRecord, Session, Tracy, Runway 및 모든 다른 공식 플러그인에 대한 전체 문서 가져오기
- 단계별 가이드 따르기 — 블로그, REST API 및 테스트된 애플리케이션 구축을 위한 완전한 워크스루 액세스
- 모든 것 검색 — 핵심 문서, 가이드 및 플러그인 전반에서 관련 페이지를 한 번에 찾기
주요 포인트
- 설정 제로 —
https://mcp.flightphp.com/mcp의 호스팅 서버는 설치나 API 키가 필요 없습니다. - 항상 최신 — 서버는 docs.flightphp.com에서 실시간으로 문서를 가져오므로 항상 최신 상태입니다.
- 어디서나 작동 — MCP Streamable HTTP 전송을 지원하는 모든 도구가 연결할 수 있습니다.
- 셀프 호스팅 가능 — PHP >= 8.1 및 Composer를 사용해 자신의 인스턴스를 실행할 수 있습니다.
IDE / AI 확장 구성
서버는 Streamable HTTP 전송을 사용합니다. 아래 확장을 선택하고 구성을 붙여넣으세요.
Claude Code (CLI)
프로젝트에 추가하려면 다음 명령어를 실행하세요:
claude mcp add --transport http flightphp-docs https://mcp.flightphp.com/mcp
또는 프로젝트의 .mcp.json에 수동으로 추가하세요:
{
"mcpServers": {
"flightphp-docs": {
"type": "http",
"url": "https://mcp.flightphp.com/mcp"
}
}
}
GitHub Copilot (VS Code)
워크스페이스의 .vscode/mcp.json에 추가하세요:
{
"servers": {
"flightphp-docs": {
"type": "http",
"url": "https://mcp.flightphp.com/mcp"
}
}
}
Kilo Code (VS Code)
VS Code settings.json에 추가하세요:
{
"kilocode.mcpServers": {
"flightphp-docs": {
"url": "https://mcp.flightphp.com/mcp",
"transport": "streamable-http"
}
}
}
Continue.dev (VS Code / JetBrains)
~/.continue/config.json에 추가하세요:
{
"mcpServers": [
{
"name": "flightphp-docs",
"transport": {
"type": "http",
"url": "https://mcp.flightphp.com/mcp"
}
}
]
}
사용 가능한 도구
MCP 서버는 AI 어시스턴트에 다음 도구를 노출합니다:
| 도구 | 설명 |
|---|---|
list_docs_pages |
모든 사용 가능한 핵심 문서 주제를 슬러그와 설명과 함께 나열 |
get_docs_page |
주제 슬러그(예: routing, middleware, security)로 핵심 문서 페이지 가져오기 |
list_guide_pages |
모든 사용 가능한 단계별 가이드 나열 |
get_guide_page |
슬러그(예: blog, unit-testing)로 전체 가이드 가져오기 |
list_plugin_pages |
모든 사용 가능한 플러그인 및 확장 페이지 나열 |
get_plugin_docs |
슬러그(예: active-record, session, jwt)로 전체 플러그인 문서 가져오기 |
search_docs |
모든 문서, 가이드 및 플러그인 전반에서 키워드나 주제 검색 |
fetch_url |
전체 docs.flightphp.com URL로 직접 페이지 가져오기 |
셀프 호스팅
자신의 인스턴스를 실행하는 것을 선호하나요? PHP >= 8.1 및 Composer가 필요합니다.
git clone https://github.com/flightphp/mcp.git
cd mcp
composer install
php server.php
서버는 기본적으로 http://0.0.0.0:8890/mcp에서 시작됩니다. IDE 구성을 로컬 주소로 업데이트하세요:
{
"mcpServers": {
"flightphp-docs": {
"type": "http",
"url": "http://localhost:8890/mcp"
}
}
}Awesome-plugins/async
Async
Async는 Flight 프레임워크를 위한 작은 패키지로, Swoole, AdapterMan, ReactPHP, Amp, RoadRunner, Workerman 등의 비동기 서버와 런타임에서 Flight 앱을 실행할 수 있게 합니다. 기본적으로 Swoole과 AdapterMan 어댑터를 포함합니다.
목표: PHP-FPM(또는 내장 서버)으로 개발하고 디버그한 후, 프로덕션에서 Swoole(또는 다른 비동기 드라이버)로 최소한의 변경으로 전환합니다.
요구 사항
- PHP 7.4 이상
- Flight 프레임워크 3.16.1 이상
- Swoole 확장
설치
Composer를 통해 설치하세요:
composer require flightphp/async
Swoole로 실행할 계획이라면 확장을 설치하세요:
# pecl 사용
pecl install swoole
# 또는 openswoole
pecl install openswoole
# 또는 패키지 관리자 사용 (Debian/Ubuntu 예시)
sudo apt-get install php-swoole
간단한 Swoole 예제
아래는 동일한 코드베이스를 사용하여 PHP-FPM(또는 내장 서버)와 Swoole을 모두 지원하는 최소 설정입니다.
프로젝트에서 필요한 파일:
- index.php
- swoole_server.php
- SwooleServerDriver.php
index.php
이 파일은 개발 시 앱을 PHP 모드로 강제 실행하는 간단한 스위치입니다.
// index.php
<?php
define('NOT_SWOOLE', true);
include 'swoole_server.php';
swoole_server.php
이 파일은 Flight 앱을 부트스트랩하고, NOT_SWOOLE이 정의되지 않으면 Swoole 드라이버를 시작합니다.
// swoole_server.php
<?php
require_once __DIR__ . '/vendor/autoload.php';
$app = Flight::app();
$app->route('/', function() use ($app) {
$app->json(['hello' => 'world']);
});
if (!defined('NOT_SWOOLE')) {
// Swoole 모드에서 실행될 때 SwooleServerDriver 클래스를 요구합니다.
require_once __DIR__ . '/SwooleServerDriver.php';
Swoole\Runtime::enableCoroutine();
$Swoole_Server = new SwooleServerDriver('127.0.0.1', 9501, $app);
$Swoole_Server->start();
} else {
$app->start();
}
SwooleServerDriver.php
AsyncBridge와 Swoole 어댑터를 사용하여 Swoole 요청을 Flight로 연결하는 간결한 드라이버입니다.
// SwooleServerDriver.php
<?php
use flight\adapter\SwooleAsyncRequest;
use flight\adapter\SwooleAsyncResponse;
use flight\AsyncBridge;
use flight\Engine;
use Swoole\HTTP\Server as SwooleServer;
use Swoole\HTTP\Request as SwooleRequest;
use Swoole\HTTP\Response as SwooleResponse;
class SwooleServerDriver {
protected $Swoole;
protected $app;
public function __construct(string $host, int $port, Engine $app) {
$this->Swoole = new SwooleServer($host, $port);
$this->app = $app;
$this->setDefault();
$this->bindWorkerEvents();
$this->bindHttpEvent();
}
protected function setDefault() {
$this->Swoole->set([
'daemonize' => false,
'dispatch_mode' => 1,
'max_request' => 8000,
'open_tcp_nodelay' => true,
'reload_async' => true,
'max_wait_time' => 60,
'enable_reuse_port' => true,
'enable_coroutine' => true,
'http_compression' => false,
'enable_static_handler' => true,
'document_root' => __DIR__,
'static_handler_locations' => ['/css', '/js', '/images', '/.well-known'],
'buffer_output_size' => 4 * 1024 * 1024,
'worker_num' => 4,
]);
$app = $this->app;
$app->map('stop', function (?int $code = null) use ($app) {
if ($code !== null) {
$app->response()->status($code);
}
});
}
protected function bindHttpEvent() {
$app = $this->app;
$AsyncBridge = new AsyncBridge($app);
$this->Swoole->on('Start', function(SwooleServer $server) {
echo "Swoole http server is started at http://127.0.0.1:9501\n";
});
$this->Swoole->on('Request', function (SwooleRequest $request, SwooleResponse $response) use ($AsyncBridge) {
$SwooleAsyncRequest = new SwooleAsyncRequest($request);
$SwooleAsyncResponse = new SwooleAsyncResponse($response);
$AsyncBridge->processRequest($SwooleAsyncRequest, $SwooleAsyncResponse);
$response->end();
gc_collect_cycles();
});
}
protected function bindWorkerEvents() {
$createPools = function() {
// 여기서 워커별 연결 풀을 생성합니다.
};
$closePools = function() {
// 여기서 풀을 닫거나 정리합니다.
};
$this->Swoole->on('WorkerStart', $createPools);
$this->Swoole->on('WorkerStop', $closePools);
$this->Swoole->on('WorkerError', $closePools);
}
public function start() {
$this->Swoole->start();
}
}
서버 실행
- 개발 (PHP 내장 서버 / PHP-FPM):
- php -S localhost:8000 (index가 public/에 있는 경우 -t public/ 추가)
- 프로덕션 (Swoole):
- php swoole_server.php
팁: 프로덕션에서는 TLS, 정적 파일, 로드 밸런싱을 처리하기 위해 Swoole 앞에 리버스 프록시(Nginx)를 사용하세요.
구성 노트
Swoole 드라이버는 여러 구성 옵션을 노출합니다:
- worker_num: 워커 프로세스 수
- max_request: 재시작 전 워커당 요청 수
- enable_coroutine: 동시성을 위한 코루틴 사용
- buffer_output_size: 출력 버퍼 크기
호스트 리소스와 트래픽 패턴에 맞게 조정하세요.
오류 처리
AsyncBridge는 Flight 오류를 적절한 HTTP 응답으로 변환합니다. 라우트 수준 오류 처리도 추가할 수 있습니다:
$app->route('/*', function() use ($app) {
try {
// 라우트 로직
} catch (Exception $e) {
$app->response()->status(500);
$app->json(['error' => $e->getMessage()]);
}
});
AdapterMan 및 기타 런타임
AdapterMan은 대안 런타임 어댑터로 지원됩니다. 이 패키지는 적응 가능하게 설계되었습니다 — 다른 어댑터를 추가하거나 사용하는 것은 일반적으로 동일한 패턴을 따릅니다: AsyncBridge와 런타임별 어댑터를 통해 서버 요청/응답을 Flight의 요청/응답으로 변환합니다.
Awesome-plugins/migrations
마이그레이션
프로젝트의 마이그레이션은 프로젝트에 관련된 모든 데이터베이스 변경 사항을 추적합니다.
byjg/php-migration 는 시작하는 데 매우 유용한 기본 라이브러리입니다.
설치
PHP 라이브러리
프로젝트에 PHP 라이브러리만 사용하고 싶다면:
composer require "byjg/migration"
명령 줄 인터페이스
명령 줄 인터페이스는 독립형이며 프로젝트와 함께 설치할 필요가 없습니다.
글로벌로 설치하고 심볼릭 링크를 생성할 수 있습니다.
composer require "byjg/migration-cli"
Migration CLI에 대한 더 많은 정보를 얻으려면 byjg/migration-cli를 방문해 주세요.
지원되는 데이터베이스
| 데이터베이스 | 드라이버 | 연결 문자열 |
|---|---|---|
| Sqlite | pdo_sqlite | sqlite:///path/to/file |
| MySql/MariaDb | pdo_mysql | mysql://username:password@hostname:port/database |
| Postgres | pdo_pgsql | pgsql://username:password@hostname:port/database |
| Sql Server | pdo_dblib, pdo_sysbase Linux | dblib://username:password@hostname:port/database |
| Sql Server | pdo_sqlsrv Windows | sqlsrv://username:password@hostname:port/database |
작동 방식은?
데이터베이스 마이그레이션은 데이터베이스 버전 관리를 위해 PURE SQL을 사용합니다.
작동하게 하려면 다음이 필요합니다:
- SQL 스크립트 생성
- 명령 줄 또는 API를 사용하여 관리
SQL 스크립트
스크립트는 세 가지 세트로 나뉩니다:
- BASE 스크립트는 새 데이터베이스를 생성하기 위한 모든 SQL 명령을 포함합니다;
- UP 스크립트는 데이터베이스 버전을 "업"하기 위한 모든 SQL 마이그레이션 명령을 포함합니다;
- DOWN 스크립트는 데이터베이스 버전을 "다운"하거나 되돌리기 위한 모든 SQL 마이그레이션 명령을 포함합니다;
스크립트 디렉토리는 다음과 같습니다:
<root dir>
|
+-- base.sql
|
+-- /migrations
|
+-- /up
|
+-- 00001.sql
+-- 00002.sql
+-- /down
|
+-- 00000.sql
+-- 00001.sql
- "base.sql"은 기본 스크립트입니다.
- "up" 폴더는 버전을 업그레이드하는 스크립트를 포함합니다.
예를 들어: 00002.sql은 데이터베이스를 버전 '1'에서 '2'로 이동하는 스크립트입니다. - "down" 폴더는 버전을 다운그레이드하는 스크립트를 포함합니다.
예를 들어: 00001.sql은 데이터베이스를 버전 '2'에서 '1'로 이동하는 스크립트입니다.
"down" 폴더는 선택 사항입니다.
다중 개발 환경
여러 개발자와 여러 브랜치에서 작업하는 경우 다음 숫자를 결정하기가 너무 어렵습니다.
그 경우에는 버전 번호 뒤에 "-dev" 접미사를 추가해야 합니다.
시나리오는 다음과 같습니다:
- 개발자 1은 브랜치를 생성하고 가장 최근 버전이 예를 들어 42입니다.
- 개발자 2는 동시에 브랜치를 생성하고 동일한 데이터베이스 버전 번호를 가지고 있습니다.
두 경우 모두 개발자는 43-dev.sql이라는 파일을 생성할 것입니다. 두 개발자는 문제 없이 업그레이드하고 다운그레이드를 수행할 것이며, 로컬 버전은 43이 될 것입니다.
하지만 개발자 1이 변경 사항을 병합하고 최종 버전 43.sql을 생성했습니다(git mv 43-dev.sql 43.sql). 개발자 2가 로컬 브랜치를 업데이트하면 그는 43.sql(개발자 1의 파일)과 43-dev.sql를 갖게 됩니다.
그가 업그레이드하거나 다운그레이드를 시도하면 마이그레이션 스크립트는 다운되고 두 개의 버전 43이 있다고 경고합니다. 그 경우 개발자 2는 자신의 파일을 44-dev.sql로 업데이트하고 병합할 때까지 작업을 계속해야 합니다.
PHP API 사용 및 프로젝트에 통합하기
기본 사용법은 다음과 같습니다:
- ConnectionManagement 객체와의 연결을 생성합니다. 더 많은 정보는 "byjg/anydataset" 구성 요소를 참조하세요.
- 이 연결과 SQL 스크립트가 위치한 폴더로 Migration 객체를 생성합니다.
- 마이그레이션 스크립트를 "reset", "up" 또는 "down"을 위해 적절한 명령을 사용합니다.
예제를 보세요:
<?php
// 연결 URI 생성
// 추가 정보: https://github.com/byjg/anydataset#connection-based-on-uri
$connectionUri = new \ByJG\Util\Uri('mysql://migrateuser:migratepwd@localhost/migratedatabase');
// 데이터베이스 또는 데이터베이스를 등록할 수 있습니다:
\ByJG\DbMigration\Migration::registerDatabase(\ByJG\DbMigration\Database\MySqlDatabase::class);
// Migration 인스턴스 생성
$migration = new \ByJG\DbMigration\Migration($connectionUri, '.');
// 실행 중 정보를 받기 위한 콜백 진행 함수를 추가합니다.
$migration->addCallbackProgress(function ($action, $currentVersion, $fileInfo) {
echo "$action, $currentVersion, ${fileInfo['description']}\n";
});
// "base.sql" 스크립트를 사용하여 데이터베이스를 복원합니다
// 그리고 데이터베이스 버전을 최신 버전으로 업그레이드하기 위해 모든 기존 스크립트를 실행합니다
$migration->reset();
// 현재 버전에서 시작하여 $version 번호까지 데이터베이스 버전을 업그레이드하거나 다운그레이드하기 위해 모든 기존 스크립트를 실행합니다;
// 버전 번호가 지정되지 않으면 마지막 데이터베이스 버전까지 마이그레이션
$migration->update($version = null);
Migration 객체는 데이터베이스 버전을 관리합니다.
프로젝트에서 버전 관리 생성
<?php
// 데이터베이스 또는 데이터베이스를 등록할 수 있습니다:
\ByJG\DbMigration\Migration::registerDatabase(\ByJG\DbMigration\Database\MySqlDatabase::class);
// Migration 인스턴스 생성
$migration = new \ByJG\DbMigration\Migration($connectionUri, '.');
// 이 명령은 데이터베이스에 버전 테이블을 생성합니다
$migration->createVersion();
현재 버전 가져오기
<?php
$migration->getCurrentVersion();
진행 상황 제어를 위한 콜백 추가
<?php
$migration->addCallbackProgress(function ($command, $version, $fileInfo) {
echo "명령 실행 중: $command, 버전 $version - ${fileInfo['description']}, ${fileInfo['exists']}, ${fileInfo['file']}, ${fileInfo['checksum']}\n";
});
Db 드라이버 인스턴스 가져오기
<?php
$migration->getDbDriver();
사용하려면 다음을 방문해 주세요: https://github.com/byjg/anydataset-db
부분 마이그레이션 방지 (MySQL에 대해 사용할 수 없음)
부분 마이그레이션은 마이그레이션 스크립트가 오류 또는 수동 중단으로 인해 프로세스 중간에 중단되는 경우입니다.
마이그레이션 테이블은 partial up 또는 partial down 상태이므로 다시 마이그레이션하기 전에 수동으로 수정해야 합니다.
이 상황을 피하기 위해 마이그레이션이 트랜잭션 컨텍스트 내에서 실행되도록 명시할 수 있습니다.
마이그레이션 스크립트가 실패하면 트랜잭션이 롤백되고 마이그레이션 테이블은 complete로 표시되며, 버전은 오류를 일으킨 스크립트 이전의 가장 최근 버전이 될 것입니다.
이 기능을 사용하려면 withTransactionEnabled 메서드를 호출하고 true를 매개변수로 전달해야 합니다:
<?php
$migration->withTransactionEnabled(true);
참고: 이 기능은 MySQL에서 사용할 수 없습니다. MySQL은 트랜잭션 내에서 DDL 명령을 지원하지 않기 때문입니다.
이 메서드를 MySQL에서 사용하면 Migration은 그를 무시합니다.
자세한 내용: https://dev.mysql.com/doc/refman/8.0/en/cannot-roll-back.html
Postgres에 대한 SQL 마이그레이션 작성 팁
트리거 및 SQL 함수 생성 시
-- DO
CREATE FUNCTION emp_stamp() RETURNS trigger AS $emp_stamp$
BEGIN
-- empname과 salary가 주어졌는지 확인
IF NEW.empname IS NULL THEN
RAISE EXCEPTION 'empname cannot be null'; -- 이 주석이 비어 있든 지 여부는 중요하지 않음
END IF; --
IF NEW.salary IS NULL THEN
RAISE EXCEPTION '% cannot have null salary', NEW.empname; --
END IF; --
-- 누가 우리를 위해 일하며 언제 대가를 지불해야 하는가?
IF NEW.salary < 0 THEN
RAISE EXCEPTION '% cannot have a negative salary', NEW.empname; --
END IF; --
-- 누가 언제 급여를 변경했는지 기억
NEW.last_date := current_timestamp; --
NEW.last_user := current_user; --
RETURN NEW; --
END; --
$emp_stamp$ LANGUAGE plpgsql;
-- DON'T
CREATE FUNCTION emp_stamp() RETURNS trigger AS $emp_stamp$
BEGIN
-- empname과 salary가 주어졌는지 확인
IF NEW.empname IS NULL THEN
RAISE EXCEPTION 'empname cannot be null';
END IF;
IF NEW.salary IS NULL THEN
RAISE EXCEPTION '% cannot have null salary', NEW.empname;
END IF;
-- 누가 우리를 위해 일하며 언제 대가를 지불해야 하는가?
IF NEW.salary < 0 THEN
RAISE EXCEPTION '% cannot have a negative salary', NEW.empname;
END IF;
-- 누가 언제 급여를 변경했는지 기억
NEW.last_date := current_timestamp;
NEW.last_user := current_user;
RETURN NEW;
END;
$emp_stamp$ LANGUAGE plpgsql;
PDO 데이터베이스 추상화 계층은 SQL 문 전체를 배치로 실행할 수 없기 때문에, byjg/migration이 마이그레이션 파일을 읽을 때 SQL 파일의 전체 내용을 세미콜론에서 나누고 명령을 하나씩 실행해야 합니다. 그러나 하나의 종류의 명령은 본체에 여러 개의 세미콜론을 가질 수 있습니다: 함수입니다.
함수를 올바르게 파싱할 수 있도록 하기 위해, byjg/migration 2.1.0에서는 마이그레이션 파일을 단순히 세미콜론 대신 세미콜론 + EOL 시퀀스에서 나누도록 변경되었습니다. 이렇게 하면 각 함수 정의의 내부 세미콜론 뒤에 빈 주석을 추가하면 byjg/migration이 올바르게 파싱할 수 있습니다.
불행히도 이러한 주석을 추가하는 것을 잊으면 라이브러리는 CREATE FUNCTION 문을 여러 부분으로 나누고 마이그레이션이 실패하게 됩니다.
콜론 문자(:) 피하기
-- DO
CREATE TABLE bookings (
booking_id UUID PRIMARY KEY,
booked_at TIMESTAMPTZ NOT NULL CHECK (CAST(booked_at AS DATE) <= check_in),
check_in DATE NOT NULL
);
-- DON'T
CREATE TABLE bookings (
booking_id UUID PRIMARY KEY,
booked_at TIMESTAMPTZ NOT NULL CHECK (booked_at::DATE <= check_in),
check_in DATE NOT NULL
);
PDO는 준비된 문에서 이름 있는 매개변수를 접두사로 붙이는 데 콜론 문자를 사용하므로, 다른 컨텍스트에서 이 사용이 문제를 발생시킬 수 있습니다.
예를 들어, PostgreSQL에서는 값을 타입 간에 캐스팅할 때 ::를 사용할 수 있습니다. 반면, PDO는 이를 잘못된 컨텍스트에서 잘못된 이름 있는 매개변수로 읽고 실행하려고 할 때 실패합니다.
이 inconsistency를 수정하는 유일한 방법은 콜론을 완전히 피하는 것입니다 (이 경우 PostgreSQL은 대안 구문인 CAST(value AS type)도 제공합니다).
SQL 편집기 사용
마지막으로, 수동 SQL 마이그레이션 작성은 힘들 수 있지만 SQL 문법을 이해할 수 있는 편집기를 사용하면 훨씬 쉬워집니다.
자동 완성, 현재 데이터베이스 스키마 탐색 및/또는 코드 자동 포맷팅을 제공하는 편집기를 사용하는 것이 좋습니다.
동일한 스키마 내에서 서로 다른 마이그레이션 처리하기
동일한 스키마 내에서 서로 다른 마이그레이션 스크립트와 버전을 생성해야 하는 경우 가능하지만 너무 위험하며 전혀 추천하지 않습니다.
이렇게 하려면 생성자에 매개변수를 전달하여 서로 다른 "마이그레이션 테이블"을 만들어야 합니다.
<?php
$migration = new \ByJG\DbMigration\Migration("db:/uri", "/path", true, "NEW_MIGRATION_TABLE_NAME");
보안상의 이유로 이 기능은 명령 줄에서 사용할 수 없지만, 환경 변수 MIGRATION_VERSION을 사용하여 이름을 저장할 수 있습니다.
이 기능을 사용하지 않는 것을 강력히 권장합니다. 권장 사항은 하나의 스키마에 대해 하나의 마이그레이션입니다.
단위 테스트 실행
기본 단위 테스트는 다음과 같이 실행할 수 있습니다:
vendor/bin/phpunit
데이터베이스 테스트 실행
통합 테스트를 실행하려면 데이터베이스가 시작되고 실행 중이어야 합니다.
우리는 테스트를 위해 데이터베이스를 시작하는 데 사용할 수 있는 기본 docker-compose.yml을 제공했습니다.
데이터베이스 실행하기
docker-compose up -d postgres mysql mssql
테스트 실행하기
vendor/bin/phpunit
vendor/bin/phpunit tests/SqliteDatabase*
vendor/bin/phpunit tests/MysqlDatabase*
vendor/bin/phpunit tests/PostgresDatabase*
vendor/bin/phpunit tests/SqlServerDblibDatabase*
vendor/bin/phpunit tests/SqlServerSqlsrvDatabase*
선택적으로 단위 테스트에서 사용하는 호스트와 비밀번호를 설정할 수 있습니다.
export MYSQL_TEST_HOST=localhost # 기본값은 localhost
export MYSQL_PASSWORD=newpassword # 비밀번호가 null이면 '.' 사용
export PSQL_TEST_HOST=localhost # 기본값은 localhost
export PSQL_PASSWORD=newpassword # 비밀번호가 null이면 '.' 사용
export MSSQL_TEST_HOST=localhost # 기본값은 localhost
export MSSQL_PASSWORD=Pa55word
export SQLITE_TEST_HOST=/tmp/test.db # 기본값은 /tmp/test.dbAwesome-plugins/comment_template
CommentTemplate
CommentTemplate은 자산 컴파일, 템플릿 상속, 변수 처리 기능을 갖춘 강력한 PHP 템플릿 엔진입니다. 내장된 CSS/JS 최소화 및 캐싱을 통해 템플릿을 간단하고 유연하게 관리할 수 있습니다.
기능
- 템플릿 상속: 레이아웃 사용 및 다른 템플릿 포함
- 자산 컴파일: 자동 CSS/JS 최소화 및 캐싱
- 변수 처리: 필터와 명령어를 사용한 템플릿 변수
- Base64 인코딩: 데이터 URI로 인라인 자산
- Flight 프레임워크 통합: Flight PHP 프레임워크와의 선택적 통합
설치
Composer를 사용하여 설치하세요.
composer require knifelemon/comment-template
기본 구성
시작하기 위한 기본 구성 옵션이 있습니다. 이에 대해 더 자세히 읽으려면 CommentTemplate Repo를 참조하세요.
방법 1: 콜백 함수 사용
<?php
require_once 'vendor/autoload.php';
use KnifeLemon\CommentTemplate\Engine;
$app = Flight::app();
$app->register('view', Engine::class, [], function (Engine $engine) use ($app) {
// 루트 디렉토리 (index.php가 있는 곳) - 웹 애플리케이션의 문서 루트
$engine->setPublicPath(__DIR__);
// 템플릿 파일 디렉토리 - 상대 경로와 절대 경로 모두 지원
$engine->setSkinPath('views'); // 공용 경로에 상대적
// 컴파일된 자산이 저장될 위치 - 상대 경로와 절대 경로 모두 지원
$engine->setAssetPath('assets'); // 공용 경로에 상대적
// 템플릿 파일 확장자
$engine->setFileExtension('.php');
});
$app->map('render', function(string $template, array $data) use ($app): void {
echo $app->view()->render($template, $data);
});
방법 2: 생성자 매개변수 사용
<?php
require_once 'vendor/autoload.php';
use KnifeLemon\CommentTemplate\Engine;
$app = Flight::app();
// __construct(string $publicPath = "", string $skinPath = "", string $assetPath = "", string $fileExtension = "")
$app->register('view', Engine::class, [
__DIR__, // publicPath - 루트 디렉토리 (index.php가 있는 곳)
'views', // skinPath - 템플릿 경로 (상대/절대 경로 지원)
'assets', // assetPath - 컴파일된 자산 경로 (상대/절대 경로 지원)
'.php' // fileExtension - 템플릿 파일 확장자
]);
$app->map('render', function(string $template, array $data) use ($app): void {
echo $app->view()->render($template, $data);
});
경로 구성
CommentTemplate은 상대 경로와 절대 경로 모두에 대한 지능적인 경로 처리를 제공합니다:
공용 경로
공용 경로는 웹 애플리케이션의 루트 디렉토리로, 일반적으로 index.php가 있는 위치입니다. 이는 웹 서버가 파일을 제공하는 문서 루트입니다.
// 예: index.php가 /var/www/html/myapp/index.php에 있는 경우
$template->setPublicPath('/var/www/html/myapp'); // 루트 디렉토리
// Windows 예: index.php가 C:\xampp\htdocs\myapp\index.php에 있는 경우
$template->setPublicPath('C:\\xampp\\htdocs\\myapp');
템플릿 경로 구성
템플릿 경로는 상대 경로와 절대 경로 모두를 지원합니다:
$template = new Engine();
$template->setPublicPath('/var/www/html/myapp'); // 루트 디렉토리 (index.php가 있는 곳)
// 상대 경로 - 공용 경로와 자동으로 결합
$template->setSkinPath('views'); // → /var/www/html/myapp/views/
$template->setSkinPath('templates/pages'); // → /var/www/html/myapp/templates/pages/
// 절대 경로 - 그대로 사용 (Unix/Linux)
$template->setSkinPath('/var/www/templates'); // → /var/www/templates/
$template->setSkinPath('/full/path/to/templates'); // → /full/path/to/templates/
// Windows 절대 경로
$template->setSkinPath('C:\\www\\templates'); // → C:\www\templates\
$template->setSkinPath('D:/projects/templates'); // → D:/projects/templates/
// UNC 경로 (Windows 네트워크 공유)
$template->setSkinPath('\\\\server\\share\\templates'); // → \\server\share\templates\
자산 경로 구성
자산 경로도 상대 경로와 절대 경로 모두를 지원합니다:
// 상대 경로 - 공용 경로와 자동으로 결합
$template->setAssetPath('assets'); // → /var/www/html/myapp/assets/
$template->setAssetPath('static/files'); // → /var/www/html/myapp/static/files/
// 절대 경로 - 그대로 사용 (Unix/Linux)
$template->setAssetPath('/var/www/cdn'); // → /var/www/cdn/
$template->setAssetPath('/full/path/to/assets'); // → /full/path/to/assets/
// Windows 절대 경로
$template->setAssetPath('C:\\www\\static'); // → C:\www\static\
$template->setAssetPath('D:/projects/assets'); // → D:/projects/assets/
// UNC 경로 (Windows 네트워크 공유)
$template->setAssetPath('\\\\server\\share\\assets'); // → \\server\share\assets\
스마트 경로 감지:
- 상대 경로: 선행 구분자 (
/,\) 또는 드라이브 문자 없음 - Unix 절대:
/로 시작 (예:/var/www/assets) - Windows 절대: 드라이브 문자로 시작 (예:
C:\www,D:/assets) - UNC 경로:
\\로 시작 (예:\\server\share)
작동 방식:
- 모든 경로는 유형(상대 vs 절대)에 따라 자동으로 해결됩니다
- 상대 경로는 공용 경로와 결합됩니다
@css와@js는{resolvedAssetPath}/css/또는{resolvedAssetPath}/js/에 최소화된 파일을 생성합니다@asset은 단일 파일을{resolvedAssetPath}/{relativePath}로 복사합니다@assetDir은 디렉토리를{resolvedAssetPath}/{relativePath}로 복사합니다- 스마트 캐싱: 소스 파일이 대상 파일보다 최신일 때만 파일이 복사됩니다
Tracy 디버거 통합
CommentTemplate은 개발 로깅 및 디버깅을 위해 Tracy Debugger와 통합됩니다.

설치
composer require tracy/tracy
사용법
<?php
use KnifeLemon\CommentTemplate\Engine;
use Tracy\Debugger;
// Tracy 활성화 (출력 전에 호출되어야 함)
Debugger::enable(Debugger::DEVELOPMENT);
Flight::set('flight.content_length', false);
// 템플릿 재정의
$app->register('view', Engine::class, [], function (Engine $builder) use ($app) {
$builder->setPublicPath($app->get('flight.views.topPath'));
$builder->setAssetPath($app->get('flight.views.assetPath'));
$builder->setSkinPath($app->get('flight.views.path'));
$builder->setFileExtension($app->get('flight.views.extension'));
});
$app->map('render', function(string $template, array $data) use ($app): void {
echo $app->view()->render($template, $data);
});
$app->start();
디버그 패널 기능
CommentTemplate은 Tracy의 디버그 바에 4개의 탭이 있는 사용자 정의 패널을 추가합니다:
- Overview: 구성, 성능 지표 및 카운트
- Assets: 압축률이 포함된 CSS/JS 컴파일 세부정보
- Variables: 적용된 필터가 있는 원본 및 변환된 값
- Timeline: 모든 템플릿 작업의 시간순 보기
로깅되는 항목
- 템플릿 렌더링 (시작/종료, 기간, 레이아웃, 가져오기)
- 자산 컴파일 (CSS/JS 파일, 크기, 압축률)
- 변수 처리 (원본/변환된 값, 필터)
- 자산 작업 (base64 인코딩, 파일 복사)
- 성능 지표 (기간, 메모리 사용량)
참고: Tracy가 설치되지 않았거나 비활성화된 경우 성능에 영향이 없습니다.
Flight PHP와 함께 사용하는 완전한 예제를 참조하세요.
템플릿 지시어
레이아웃 상속
공통 구조를 생성하기 위해 레이아웃을 사용하세요:
layout/global_layout.php:
<!DOCTYPE html>
<html>
<head>
<title>{$title}</title>
</head>
<body>
<!--@contents-->
</body>
</html>
view/page.php:
<!--@layout(layout/global_layout)-->
<h1>{$title}</h1>
<p>{$content}</p>
자산 관리
CSS 파일
<!--@css(/css/styles.css)--> <!-- 최소화 및 캐싱 -->
<!--@cssSingle(/css/critical.css)--> <!-- 단일 파일, 최소화되지 않음 -->
JavaScript 파일
CommentTemplate은 다양한 JavaScript 로딩 전략을 지원합니다:
<!--@js(/js/script.js)--> <!-- 최소화, 하단에 로드 -->
<!--@jsAsync(/js/analytics.js)--> <!-- 최소화, 하단에 async로 로드 -->
<!--@jsDefer(/js/utils.js)--> <!-- 최소화, 하단에 defer로 로드 -->
<!--@jsTop(/js/critical.js)--> <!-- 최소화, head에 로드 -->
<!--@jsTopAsync(/js/tracking.js)--> <!-- 최소화, head에 async로 로드 -->
<!--@jsTopDefer(/js/polyfill.js)--> <!-- 최소화, head에 defer로 로드 -->
<!--@jsSingle(/js/widget.js)--> <!-- 단일 파일, 최소화되지 않음 -->
<!--@jsSingleAsync(/js/ads.js)--> <!-- 단일 파일, 최소화되지 않음, async -->
<!--@jsSingleDefer(/js/social.js)--> <!-- 단일 파일, 최소화되지 않음, defer -->
CSS/JS 파일 내 자산 지시어
CommentTemplate은 컴파일 중 CSS 및 JavaScript 파일 내 자산 지시어를 처리합니다:
CSS 예시:
/* CSS 파일 내 */
@font-face {
font-family: 'CustomFont';
src: url('<!--@asset(fonts/custom.woff2)-->') format('woff2');
}
.background-image {
background: url('<!--@asset(images/bg.jpg)-->');
}
.inline-icon {
background: url('<!--@base64(icons/star.svg)-->');
}
JavaScript 예시:
/* JS 파일 내 */
const fontUrl = '<!--@asset(fonts/custom.woff2)-->';
const imageData = '<!--@base64(images/icon.png)-->';
Base64 인코딩
<!--@base64(images/logo.png)--> <!-- 데이터 URI로 인라인 -->
예시:
<!-- 더 빠른 로딩을 위해 작은 이미지를 데이터 URI로 인라인 -->
<img src="<!--@base64(images/logo.png)-->" alt="Logo">
<div style="background-image: url('<!--@base64(icons/star.svg)-->');">
배경으로 작은 아이콘
</div>
자산 복사
<!--@asset(images/photo.jpg)--> <!-- 단일 자산을 공용 디렉토리로 복사 -->
<!--@assetDir(assets)--> <!-- 전체 디렉토리를 공용 디렉토리로 복사 -->
예시:
<!-- 정적 자산 복사 및 참조 -->
<img src="<!--@asset(images/hero-banner.jpg)-->" alt="Hero Banner">
<a href="<!--@asset(documents/brochure.pdf)-->" download>브로슈어 다운로드</a>
<!-- 전체 디렉토리 복사 (폰트, 아이콘 등) -->
<!--@assetDir(assets/fonts)-->
<!--@assetDir(assets/icons)-->
템플릿 포함
<!--@import(components/header)--> <!-- 다른 템플릿 포함 -->
예시:
<!-- 재사용 가능한 컴포넌트 포함 -->
<!--@import(components/header)-->
<main>
<h1>웹사이트에 오신 것을 환영합니다</h1>
<!--@import(components/sidebar)-->
<div class="content">
<p>여기에 주요 콘텐츠...</p>
</div>
</main>
<!--@import(components/footer)-->
변수 처리
기본 변수
<h1>{$title}</h1>
<p>{$description}</p>
변수 필터
{$title|upper} <!-- 대문자로 변환 -->
{$content|lower} <!-- 소문자로 변환 -->
{$html|striptag} <!-- HTML 태그 제거 -->
{$text|escape} <!-- HTML 이스케이프 -->
{$multiline|nl2br} <!-- 개행을 <br>로 변환 -->
{$html|br2nl} <!-- <br> 태그를 개행으로 변환 -->
{$description|trim} <!-- 공백 제거 -->
{$subject|title} <!-- 제목 형식으로 변환 -->
변수 명령어
{$title|default=Default Title} <!-- 기본값 설정 -->
{$name|concat= (Admin)} <!-- 텍스트 연결 -->
변수 명령어
{$content|striptag|trim|escape} <!-- 여러 필터 체인 -->
주석
템플릿 주석은 출력에서 완전히 제거되며 최종 HTML에 나타나지 않습니다:
{* 이것은 한 줄 템플릿 주석입니다 *}
{*
이것은 여러 줄
템플릿 주석입니다
여러 줄에 걸칩니다
*}
<h1>{$title}</h1>
{* 디버그 주석: title 변수가 작동하는지 확인 *}
<p>{$content}</p>
참고: 템플릿 주석 {* ... *}은 HTML 주석 <!-- ... -->과 다릅니다. 템플릿 주석은 처리 중 제거되며 브라우저에 도달하지 않습니다.
예시 프로젝트 구조
project/
├── source/
│ ├── layouts/
│ │ └── default.php
│ ├── components/
│ │ ├── header.php
│ │ └── footer.php
│ ├── css/
│ │ ├── bootstrap.min.css
│ │ └── custom.css
│ ├── js/
│ │ ├── app.js
│ │ └── bootstrap.min.js
│ └── homepage.php
├── public/
│ └── assets/ # 생성된 자산
│ ├── css/
│ └── js/
└── vendor/Awesome-plugins/easy_query
EasyQuery
knifelemon/easy-query는 가볍고 유창한 SQL 쿼리 빌더로, 준비된 문장에 대한 SQL과 매개변수를 생성합니다. SimplePdo와 함께 작동합니다.
기능
- 🔗 유창한 API - 읽기 쉬운 쿼리 구성에 대한 메서드 체이닝
- 🛡️ SQL 인젝션 보호 - 준비된 문장과 함께 자동 매개변수 바인딩
- 🔧 Raw SQL 지원 -
raw()를 사용한 raw SQL 표현식 삽입 - 📝 다양한 쿼리 유형 - SELECT, INSERT, UPDATE, DELETE, COUNT
- 🔀 JOIN 지원 - 별칭과 함께 INNER, LEFT, RIGHT 조인
- 🎯 고급 조건 - LIKE, IN, NOT IN, BETWEEN, 비교 연산자
- 🌐 데이터베이스 독립적 - SQL + 매개변수 반환, 모든 DB 연결과 사용
- 🪶 가벼움 - 의존성 제로의 최소한의 공간
설치
composer require knifelemon/easy-query
빠른 시작
use KnifeLemon\EasyQuery\Builder;
$q = Builder::table('users')
->select(['id', 'name', 'email'])
->where(['status' => 'active'])
->orderBy('created_at DESC')
->limit(10)
->build();
// Flight의 SimplePdo와 함께 사용
$users = Flight::db()->fetchAll($q['sql'], $q['params']);
build() 이해
build() 메서드는 sql과 params가 포함된 배열을 반환합니다. 이 분리는 준비된 문장을 사용하여 데이터베이스를 안전하게 유지합니다.
$q = Builder::table('users')
->where(['email' => 'user@example.com'])
->build();
// 반환:
// [
// 'sql' => 'SELECT * FROM users WHERE email = ?',
// 'params' => ['user@example.com']
// ]
쿼리 유형
SELECT
// 모든 컬럼 선택
$q = Builder::table('users')->build();
// SELECT * FROM users
// 특정 컬럼 선택
$q = Builder::table('users')
->select(['id', 'name', 'email'])
->build();
// SELECT id, name, email FROM users
// 테이블 별칭과 함께
$q = Builder::table('users')
->alias('u')
->select(['u.id', 'u.name'])
->build();
// SELECT u.id, u.name FROM users AS u
INSERT
$q = Builder::table('users')
->insert([
'name' => 'John Doe',
'email' => 'john@example.com',
'status' => 'active'
])
->build();
// INSERT INTO users SET name = ?, email = ?, status = ?
Flight::db()->runQuery($q['sql'], $q['params']);
$userId = Flight::db()->lastInsertId();
UPDATE
$q = Builder::table('users')
->update(['status' => 'inactive', 'updated_at' => date('Y-m-d H:i:s')])
->where(['id' => 123])
->build();
// UPDATE users SET status = ?, updated_at = ? WHERE id = ?
Flight::db()->runQuery($q['sql'], $q['params']);
DELETE
$q = Builder::table('users')
->delete()
->where(['id' => 123])
->build();
// DELETE FROM users WHERE id = ?
Flight::db()->runQuery($q['sql'], $q['params']);
COUNT
$q = Builder::table('users')
->count()
->where(['status' => 'active'])
->build();
// SELECT COUNT(*) AS cnt FROM users WHERE status = ?
$count = Flight::db()->fetchField($q['sql'], $q['params']);
WHERE 조건
간단한 동등성
$q = Builder::table('users')
->where(['id' => 123, 'status' => 'active'])
->build();
// WHERE id = ? AND status = ?
비교 연산자
$q = Builder::table('users')
->where([
'age' => ['>=', 18],
'score' => ['<', 100],
'name' => ['!=', 'admin']
])
->build();
// WHERE age >= ? AND score < ? AND name != ?
LIKE
$q = Builder::table('users')
->where(['name' => ['LIKE', '%john%']])
->build();
// WHERE name LIKE ?
IN / NOT IN
// IN
$q = Builder::table('users')
->where(['id' => ['IN', [1, 2, 3, 4, 5]]])
->build();
// WHERE id IN (?, ?, ?, ?, ?)
// NOT IN
$q = Builder::table('users')
->where(['status' => ['NOT IN', ['banned', 'deleted']]])
->build();
// WHERE status NOT IN (?, ?)
BETWEEN
$q = Builder::table('products')
->where(['price' => ['BETWEEN', [100, 500]]])
->build();
// WHERE price BETWEEN ? AND ?
OR 조건
orWhere()를 사용하여 OR 그룹화된 조건을 추가합니다:
$q = Builder::table('users')
->where(['status' => 'active'])
->orWhere([
'role' => 'admin',
'permissions' => ['LIKE', '%manage%']
])
->build();
// WHERE status = ? AND (role = ? OR permissions LIKE ?)
JOIN
INNER JOIN
$q = Builder::table('users')
->alias('u')
->select(['u.id', 'u.name', 'p.title'])
->innerJoin('posts', 'u.id = p.user_id', 'p')
->build();
// SELECT u.id, u.name, p.title FROM users AS u INNER JOIN posts AS p ON u.id = p.user_id
LEFT JOIN
$q = Builder::table('users')
->alias('u')
->select(['u.name', 'o.total'])
->leftJoin('orders', 'u.id = o.user_id', 'o')
->build();
// ... LEFT JOIN orders AS o ON u.id = o.user_id
여러 JOIN
$q = Builder::table('orders')
->alias('o')
->select(['o.id', 'u.name AS customer', 'p.title AS product'])
->innerJoin('users', 'o.user_id = u.id', 'u')
->leftJoin('order_items', 'o.id = oi.order_id', 'oi')
->leftJoin('products', 'oi.product_id = p.id', 'p')
->where(['o.status' => 'completed'])
->build();
정렬, 그룹화 및 제한
ORDER BY
$q = Builder::table('users')
->orderBy('created_at DESC')
->build();
// ORDER BY created_at DESC
GROUP BY
$q = Builder::table('orders')
->select(['user_id', 'COUNT(*) as order_count'])
->groupBy('user_id')
->build();
// SELECT user_id, COUNT(*) as order_count FROM orders GROUP BY user_id
LIMIT 및 OFFSET
$q = Builder::table('users')
->limit(10)
->build();
// LIMIT 10
$q = Builder::table('users')
->limit(10, 20) // limit, offset
->build();
// LIMIT 10 OFFSET 20
Raw SQL 표현식
SQL 함수나 표현식이 바인딩된 매개변수로 처리되지 않아야 할 때 raw()를 사용합니다.
기본 Raw
$q = Builder::table('users')
->update([
'login_count' => Builder::raw('login_count + 1'),
'updated_at' => Builder::raw('NOW()')
])
->where(['id' => 123])
->build();
// SET login_count = login_count + 1, updated_at = NOW()
바인딩 매개변수와 함께 Raw
$q = Builder::table('orders')
->update([
'total' => Builder::raw('COALESCE(subtotal, ?) + ?', [0, 10])
])
->where(['id' => 1])
->build();
// SET total = COALESCE(subtotal, ?) + ?
// params: [0, 10, 1]
WHERE에서 Raw (서브쿼리)
$q = Builder::table('products')
->where([
'price' => ['>', Builder::raw('(SELECT AVG(price) FROM products)')]
])
->build();
// WHERE price > (SELECT AVG(price) FROM products)
사용자 입력에 대한 안전한 식별자
컬럼 이름이 사용자 입력에서 올 때 SQL 인젝션을 방지하기 위해 safeIdentifier()를 사용합니다:
$sortColumn = $_GET['sort']; // 예: 'created_at'
$safeColumn = Builder::safeIdentifier($sortColumn);
$q = Builder::table('users')
->orderBy($safeColumn . ' DESC')
->build();
// 사용자가 시도: "name; DROP TABLE users--"
// InvalidArgumentException 발생
사용자 제공 컬럼 이름에 대한 rawSafe
$userColumn = $_GET['aggregate_column'];
$q = Builder::table('orders')
->select([
Builder::rawSafe('SUM({col})', ['col' => $userColumn])->value . ' AS total'
])
->build();
// 컬럼 이름 검증, 유효하지 않으면 예외 발생
경고: 사용자 입력을
raw()에 직접 연결하지 마세요. 항상 바인딩된 매개변수 또는safeIdentifier()를 사용하세요.
쿼리 빌더 재사용
Clear 메서드
빌더를 재사용하기 위해 특정 부분을 지웁니다:
$query = Builder::table('users')
->select(['id', 'name'])
->where(['status' => 'active'])
->orderBy('created_at DESC');
// 첫 번째 쿼리
$q1 = $query->limit(10)->build();
// 지우고 재사용
$query->clearWhere()->clearLimit();
// 다른 조건으로 두 번째 쿼리
$q2 = $query
->where(['status' => 'pending'])
->limit(5)
->build();
사용 가능한 Clear 메서드
| 메서드 | 설명 |
|---|---|
clearWhere() |
WHERE 조건과 매개변수 지우기 |
clearSelect() |
SELECT 컬럼을 기본 '*'로 재설정 |
clearJoin() |
모든 JOIN 절 지우기 |
clearGroupBy() |
GROUP BY 절 지우기 |
clearOrderBy() |
ORDER BY 절 지우기 |
clearLimit() |
LIMIT 및 OFFSET 지우기 |
clearAll() |
빌더를 초기 상태로 재설정 |
페이지네이션 예제
$baseQuery = Builder::table('users')
->select(['id', 'name', 'email'])
->where(['status' => 'active'])
->orderBy('created_at DESC');
// 총 개수 가져오기
$countQuery = clone $baseQuery;
$countResult = $countQuery->clearSelect()->count()->build();
$total = Flight::db()->fetchField($countResult['sql'], $countResult['params']);
// 페이지네이션된 결과 가져오기
$page = 1;
$perPage = 20;
$listResult = $baseQuery->limit($perPage, ($page - 1) * $perPage)->build();
$users = Flight::db()->fetchAll($listResult['sql'], $listResult['params']);
동적 쿼리 빌딩
$query = Builder::table('products')->alias('p');
if (!empty($categoryId)) {
$query->where(['p.category_id' => $categoryId]);
}
if (!empty($minPrice)) {
$query->where(['p.price' => ['>=', $minPrice]]);
}
if (!empty($maxPrice)) {
$query->where(['p.price' => ['<=', $maxPrice]]);
}
if (!empty($searchTerm)) {
$query->where(['p.name' => ['LIKE', "%{$searchTerm}%"]]);
}
$result = $query->orderBy('p.created_at DESC')->limit(20)->build();
$products = Flight::db()->fetchAll($result['sql'], $result['params']);
전체 FlightPHP 예제
use KnifeLemon\EasyQuery\Builder;
// 페이지네이션과 함께 사용자 목록
Flight::route('GET /users', function() {
$page = (int) (Flight::request()->query['page'] ?? 1);
$perPage = 20;
$q = Builder::table('users')
->select(['id', 'name', 'email', 'created_at'])
->where(['status' => 'active'])
->orderBy('created_at DESC')
->limit($perPage, ($page - 1) * $perPage)
->build();
$users = Flight::db()->fetchAll($q['sql'], $q['params']);
Flight::json(['users' => $users, 'page' => $page]);
});
// 사용자 생성
Flight::route('POST /users', function() {
$data = Flight::request()->data;
$q = Builder::table('users')
->insert([
'name' => $data->name,
'email' => $data->email,
'created_at' => Builder::raw('NOW()')
])
->build();
Flight::db()->runQuery($q['sql'], $q['params']);
Flight::json(['id' => Flight::db()->lastInsertId()]);
});
// 사용자 업데이트
Flight::route('PUT /users/@id', function($id) {
$data = Flight::request()->data;
$q = Builder::table('users')
->update([
'name' => $data->name,
'email' => $data->email,
'updated_at' => Builder::raw('NOW()')
])
->where(['id' => $id])
->build();
Flight::db()->runQuery($q['sql'], $q['params']);
Flight::json(['success' => true]);
});
// 사용자 삭제
Flight::route('DELETE /users/@id', function($id) {
$q = Builder::table('users')
->delete()
->where(['id' => $id])
->build();
Flight::db()->runQuery($q['sql'], $q['params']);
Flight::json(['success' => true]);
});
API 참조
정적 메서드
| 메서드 | 설명 |
|---|---|
Builder::table(string $table) |
테이블에 대한 새로운 빌더 인스턴스 생성 |
Builder::raw(string $sql, array $bindings = []) |
raw SQL 표현식 생성 |
Builder::rawSafe(string $expr, array $identifiers, array $bindings = []) |
안전한 식별자 대체와 함께 raw 표현식 |
Builder::safeIdentifier(string $identifier) |
안전한 컬럼/테이블 이름 검증 및 반환 |
인스턴스 메서드
| 메서드 | 설명 |
|---|---|
alias(string $alias) |
테이블 별칭 설정 |
select(string\|array $columns) |
선택할 컬럼 설정 (기본: '*') |
where(array $conditions) |
WHERE 조건 추가 (AND) |
orWhere(array $conditions) |
OR WHERE 조건 추가 |
join(string $table, string $condition, string $alias, string $type) |
JOIN 절 추가 |
innerJoin(string $table, string $condition, string $alias) |
INNER JOIN 추가 |
leftJoin(string $table, string $condition, string $alias) |
LEFT JOIN 추가 |
groupBy(string $groupBy) |
GROUP BY 절 추가 |
orderBy(string $orderBy) |
ORDER BY 절 추가 |
limit(int $limit, int $offset = 0) |
LIMIT 및 OFFSET 추가 |
count(string $column = '*') |
쿼리를 COUNT로 설정 |
insert(array $data) |
쿼리를 INSERT로 설정 |
update(array $data) |
쿼리를 UPDATE로 설정 |
delete() |
쿼리를 DELETE로 설정 |
build() |
['sql' => ..., 'params' => ...] 빌드 및 반환 |
get() |
build()의 별칭 |
Tracy 디버거 통합
EasyQuery는 설치된 경우 Tracy 디버거와 자동으로 통합됩니다. 설정이 필요 없습니다!
composer require tracy/tracy
use Tracy\Debugger;
Debugger::enable();
// 모든 쿼리가 Tracy 패널에 자동으로 로깅됩니다
$q = Builder::table('users')->where(['status' => 'active'])->build();
Tracy 패널은 다음을 보여줍니다:
- 총 쿼리 및 유형별 분해
- 생성된 SQL (구문 강조)
- 매개변수 배열
- 쿼리 세부 정보 (테이블, where, joins 등)
전체 문서의 경우 GitHub 저장소를 방문하세요.
Awesome-plugins/twig
Twig
Twig는 PHP를 위한 유연하고 빠르며 안전한 템플릿 엔진입니다. Symfony와 많은 다른 프로젝트에서 사용되는 템플릿 언어로, AI 코딩 도구와 대부분의 PHP 개발자들이 이미 그 문법에 익숙합니다. Twig는 템플릿을 최적화된 PHP로 컴파일하고, 기본적으로 출력 자동 이스케이프를 지원하여(XSS 보호에 유용) 필터, 함수, 확장을 통해 쉽게 확장할 수 있습니다.
설치
컴포저로 설치합니다.
composer require twig/twig
기본 설정
시작하기 위한 몇 가지 기본 설정 옵션이 있습니다. 이에 대한 자세한 내용은 Twig 문서에서 확인할 수 있습니다.
require 'vendor/autoload.php';
$app = Flight::app();
$app->map('render', function(string $template, array $data): void {
$loader = new \Twig\Loader\FilesystemLoader(Flight::get('flight.views.path'));
$twig = new \Twig\Environment($loader, [
// Twig가 컴파일된 템플릿을 저장하는 위치
'cache' => __DIR__ . '/../cache/twig',
// 소스가 변경될 때 템플릿을 다시 컴파일(개발 환경에 유용)
'auto_reload' => true,
]);
echo $twig->render($template, $data);
});
Twig를 뷰 클래스로 등록
단일 Twig 환경을 재사용하려면(프로덕션에 권장), 이를 등록하고 render가 해당 환경을 가리키도록 합니다:
require 'vendor/autoload.php';
$app = Flight::app();
$app->register('view', \Twig\Environment::class, [
new \Twig\Loader\FilesystemLoader($app->get('flight.views.path')),
[
'cache' => __DIR__ . '/../cache/twig',
'auto_reload' => true,
],
]);
$app->map('render', function(string $template, array $data): void {
echo Flight::view()->render($template, $data);
});
간단한 레이아웃 예제
다음은 레이아웃 파일의 간단한 예제입니다. 이 파일은 다른 모든 뷰를 감싸는 데 사용됩니다.
{# app/views/layout.twig #}
<!doctype html>
<html lang="en">
<head>
<title>{% if title %}{{ title }} - {% endif %}My App</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<header>
<nav>
{# 여기에 네비게이션 요소를 추가하세요 #}
</nav>
</header>
<div id="content">
{# 여기가 핵심입니다 #}
{% block content %}{% endblock %}
</div>
<div id="footer">
© Copyright
</div>
</body>
</html>
이제 해당 콘텐츠 블록 안에 렌더링할 파일입니다:
{# app/views/home.twig #}
{# 이 파일이 "layout.twig" 파일 안에 있음을 Twig에 알립니다 #}
{% extends 'layout.twig' %}
{# 콘텐츠 블록 안에 렌더링될 내용입니다 #}
{% block content %}
<h1>Home Page</h1>
<p>Welcome to my app!</p>
{% endblock %}
그리고 함수나 컨트롤러에서 이를 렌더링하려면 다음과 같이 합니다:
// 간단한 라우트
Flight::route('/', function () {
Flight::render('home.twig', [
'title' => 'Home Page'
]);
});
// 컨트롤러를 사용하는 경우
Flight::route('/', [HomeController::class, 'index']);
// HomeController.php
class HomeController
{
public function index()
{
Flight::render('home.twig', [
'title' => 'Home Page'
]);
}
}
Twig를 최대한 활용하는 방법에 대한 자세한 내용은 Twig 문서를 참조하세요!
디버깅
Twig에는 템플릿 내에서 사용할 수 있는 dump() 함수를 추가하는 디버그 확장이 포함되어 있습니다. 개발 환경에서만 활성화하세요:
$app->register('view', \Twig\Environment::class, [
new \Twig\Loader\FilesystemLoader($app->get('flight.views.path')),
[
'cache' => __DIR__ . '/../cache/twig',
'debug' => true, // dump() 함수에 필요
'auto_reload' => true,
],
], function (\Twig\Environment $twig): void {
$twig->addExtension(new \Twig\Extension\DebugExtension());
});
그런 다음 템플릿에서:
{{ dump(user) }}
또한 Twig를 Tracy와 함께 사용하여 PHP 수준 디버깅을 수행할 수도 있습니다. 템플릿 수준 메트릭(렌더링 시간, 메모리, 실행된 템플릿/블록)을 위해 flightphp/tracy-extensions의 선택적 Twig 패널을 사용하세요: twig_profile로 Twig\Profiler\Profile을 TracyExtensionLoader에 전달하세요. 선택적 TwigTracyExtension은 Tracy가 켜져 있을 때 템플릿에서 {{ dump() }} / {{ bdump() }} / {{ dumpe() }}를 노출합니다.
보안 참고 사항
Twig는 기본적으로 출력을 자동 이스케이프하여 XSS 공격으로부터 보호합니다. 텍스트에는 {{ variable }}을 사용하세요. HTML 콘텐츠를 의도적으로 신뢰하는 경우(예: 서버 측에서 이미 처리한 정제된 마크다운)에만 |raw 필터를 사용하세요.
Awesome-plugins/session
FlightPHP 세션 - 경량 파일 기반 세션 핸들러
이것은 Flight PHP Framework의 경량 파일 기반 세션 핸들러 플러그인입니다. 비동기 세션 읽기, 선택적 암호화, 자동 커밋 기능, 개발을 위한 테스트 모드 등의 기능을 제공하는 간단하면서도 강력한 세션 관리 솔루션입니다. 세션 데이터는 파일에 저장되므로 데이터베이스를 필요하지 않은 애플리케이션에 이상적입니다.
데이터베이스를 사용하고 싶다면, 이와 유사한 기능이 있지만 데이터베이스 백엔드를 가진 ghostff/session 플러그인을 확인하세요.
전체 소스 코드와 세부 사항을 위해 Github 저장소를 방문하세요.
설치
플러그인을 Composer를 통해 설치하세요:
composer require flightphp/session
기본 사용법
Flight 애플리케이션에서 flightphp/session 플러그인을 사용하는 간단한 예제입니다:
require 'vendor/autoload.php';
use flight\Session; // 세션 서비스를 등록합니다
$app = Flight::app();
// 세션 서비스를 등록합니다
$app->register('session', Session::class);
// 세션 사용 예제 라우트
Flight::route('/login', function() {
$session = Flight::session();
$session->set('user_id', 123);
$session->set('username', 'johndoe');
$session->set('is_admin', false);
echo $session->get('username'); // johndoe를 출력합니다
echo $session->get('preferences', 'default_theme'); // default_theme를 출력합니다
if ($session->get('user_id')) {
Flight::json(['message' => '사용자가 로그인되었습니다!', 'user_id' => $session->get('user_id')]);
}
});
Flight::route('/logout', function() {
$session = Flight::session();
$session->clear(); // 모든 세션 데이터를 지웁니다
Flight::json(['message' => '로그아웃되었습니다']);
});
Flight::start();
주요 포인트
- 비동기: 기본적으로
read_and_close를 사용하여 세션 잠금 문제를 방지합니다. - 자동 커밋: 기본으로 활성화되어 종료 시 변경 사항이 자동으로 저장됩니다.
- 파일 저장: 세션은 기본적으로
/flight_sessions아래 시스템 임시 디렉터리에 저장됩니다.
구성
등록할 때 배열 옵션을 전달하여 세션 핸들러를 사용자 정의할 수 있습니다:
// 예, 이중 배열입니다 :)
$app->register('session', Session::class, [ [
'save_path' => '/custom/path/to/sessions', // 세션 파일을 저장할 디렉터리
'prefix' => 'myapp_', // 세션 파일의 접두사
'encryption_key' => 'a-secure-32-byte-key-here', // AES-256-CBC 암호화를 활성화 (32바이트 추천)
'auto_commit' => false, // 자동 커밋을 비활성화하여 수동 제어
'start_session' => true, // 세션을 자동으로 시작 (기본: true)
'test_mode' => false, // 개발을 위한 테스트 모드 활성화
'serialization' => 'json', // 직렬화 방법: 'json' (기본) 또는 'php' (레거시)
] ]);
구성 옵션
| 옵션 | 설명 | 기본 값 |
|---|---|---|
save_path |
세션 파일이 저장되는 디렉터리 | sys_get_temp_dir() . '/flight_sessions' |
prefix |
저장된 세션 파일의 접두사 | sess_ |
encryption_key |
AES-256-CBC 암호화 키 (선택적) | null (암호화 없음) |
auto_commit |
종료 시 세션 데이터 자동 저장 | true |
start_session |
세션을 자동으로 시작 | true |
test_mode |
PHP 세션을 영향을 주지 않는 테스트 모드 실행 | false |
test_session_id |
테스트 모드의 사용자 정의 세션 ID (선택적) | 설정되지 않으면 무작위 생성 |
serialization |
직렬화 방법: 'json' (기본, 안전) 또는 'php' (레거시, 객체 허용) | 'json' |
직렬화 모드
기본적으로 이 라이브러리는 JSON 직렬화를 사용하여 세션 데이터를 처리하며, 이는 안전하고 PHP 객체 주입 취약점을 방지합니다. 세션에 PHP 객체를 저장해야 하는 경우 (대부분의 앱에서는 권장되지 않음) 레거시 PHP 직렬화를 선택할 수 있습니다:
'serialization' => 'json'(기본):- 세션 데이터에 배열과 기본 형식만 허용합니다.
- 더 안전: PHP 객체 주입에 면역.
- 파일은
J(일반 JSON) 또는F(암호화된 JSON)로 접두사 붙임.
'serialization' => 'php':- PHP 객체 저장 허용 (주의해서 사용).
- 파일은
P(일반 PHP 직렬화) 또는E(암호화된 PHP 직렬화)로 접두사 붙임.
노트: JSON 직렬화를 사용하면 객체를 저장하려고 시도하면 예외가 발생합니다.
고급 사용법
수동 커밋
자동 커밋을 비활성화하면 변경 사항을 수동으로 커밋해야 합니다:
$app->register('session', Session::class, ['auto_commit' => false]);
Flight::route('/update', function() {
$session = Flight::session();
$session->set('key', 'value');
$session->commit(); // 변경 사항을 명시적으로 저장합니다
});
암호화된 세션 보안
민감한 데이터를 위해 암호화를 활성화하세요:
$app->register('session', Session::class, [
'encryption_key' => 'your-32-byte-secret-key-here'
]);
Flight::route('/secure', function() {
$session = Flight::session();
$session->set('credit_card', '4111-1111-1111-1111'); // 자동으로 암호화됩니다
echo $session->get('credit_card'); // 검색 시 복호화됩니다
});
세션 재생성
보안상 세션 ID를 재생성하세요 (예: 로그인 후):
Flight::route('/post-login', function() {
$session = Flight::session();
$session->regenerate(); // 새 ID, 데이터 유지
// 또는
$session->regenerate(true); // 새 ID, 기존 데이터 삭제
});
미들웨어 예제
세션 기반 인증으로 라우트를 보호하세요:
Flight::route('/admin', function() {
Flight::json(['message' => '관리자 패널에 오신 것을 환영합니다']);
})->addMiddleware(function() {
$session = Flight::session();
if (!$session->get('is_admin')) {
Flight::halt(403, '접근 거부');
}
});
이것은 미들웨어에서 사용하는 간단한 예제입니다. 더 자세한 예제는 middleware 문서를 참조하세요.
메서드
Session 클래스는 다음 메서드를 제공합니다:
set(string $key, $value): 세션에 값을 저장합니다.get(string $key, $default = null): 값을 검색하며, 키가 존재하지 않으면 기본 값을 사용합니다.delete(string $key): 세션에서 특정 키를 제거합니다.clear(): 모든 세션 데이터를 삭제하지만 동일한 파일 이름을 유지합니다.commit(): 현재 세션 데이터를 파일 시스템에 저장합니다.id(): 현재 세션 ID를 반환합니다.regenerate(bool $deleteOldFile = false): 세션 ID를 재생성하며 새 세션 파일을 생성합니다. 기존 데이터는 유지되며,$deleteOldFile이true이면 기존 파일을 삭제합니다.destroy(string $id): 지정된 ID의 세션을 파괴하고 세션 파일을 시스템에서 삭제합니다. 이는SessionHandlerInterface의 일부이며$id가 필요합니다. 일반적인 사용법은$session->destroy($session->id())입니다.getAll(): 현재 세션의 모든 데이터를 반환합니다.
get()과 id()를 제외한 모든 메서드는 체이닝을 위해 Session 인스턴스를 반환합니다.
이 플러그인을 사용하는 이유?
- 경량: 외부 종속성 없음 - 단지 파일만 사용합니다.
- 비동기: 기본적으로
read_and_close로 세션 잠금을 피합니다. - 보안: 민감한 데이터에 AES-256-CBC 암호화를 지원합니다.
- 유연성: 자동 커밋, 테스트 모드, 수동 제어 옵션.
- Flight-네이티브: Flight 프레임워크를 위해 특별히 제작되었습니다.
기술 세부 사항
- 저장 형식: 세션 파일은 구성된
save_path에sess_로 접두사 붙여 저장됩니다. 파일 내용 접두사:J: 일반 JSON (기본, 암호화 없음)F: 암호화된 JSON (기본, 암호화 있음)P: 일반 PHP 직렬화 (레거시, 암호화 없음)E: 암호화된 PHP 직렬화 (레거시, 암호화 있음)
- 암호화:
encryption_key가 제공되면 세션 작성 시마다 무작위 IV와 함께 AES-256-CBC를 사용합니다. JSON과 PHP 직렬화 모드 모두에서 작동합니다. - 직렬화: JSON이 기본이며 가장 안전한 방법입니다. PHP 직렬화는 레거시/고급 사용을 위해 사용 가능하지만 덜 안전합니다.
- 가비지 수집: 만료된 세션을 정리하기 위해 PHP의
SessionHandlerInterface::gc()를 구현합니다.
기여
기여를 환영합니다! 저장소를 포크하여 변경 사항을 만들고 풀 요청을 제출하세요. 버그 보고나 기능 제안은 Github 이슈 트래커를 통해 하세요.
라이선스
이 플러그인은 MIT 라이선스 under입니다. 자세한 내용은 Github 저장소를 참조하세요.
Awesome-plugins/runway
Runway
Runway은 Flight 애플리케이션을 관리하는 데 도움이 되는 CLI 애플리케이션입니다. 컨트롤러를 생성하고, 모든 라우트를 표시하고, AI 설정 도우미, 마이그레이션(스켈레톤 내) 등을 실행할 수 있습니다. 우수한 adhocore/php-cli 라이브러리를 기반으로 합니다.
코드를 보려면 여기를 클릭하세요.
스캐폴딩 명령은 공식 스켈레톤과 의도적으로 정렬되어 있어 AI 코딩 도구와 인간이 매번 동일한 경로, 네임스페이스 및 생성자 주입 스타일을 얻을 수 있습니다.
설치
composer로 설치합니다.
composer require flightphp/runway
스켈레톤은 이미 Runway에 의존하고 있습니다. 프로젝트 루트에서 php runway를 사용하세요.
기본 구성
Runway를 처음 실행하면 'runway' 키를 통해 app/config/config.php에서 runway 구성을 찾으려고 시도합니다.
<?php
// app/config/config.php
return [
'runway' => [
'app_root' => 'app/',
'public_root' => 'public/',
// 선택 사항; 스켈레톤은 공개 엔트리에 index_root도 사용합니다
'index_root' => 'public/index.php',
],
];
참고 - v1.2.0부터
.runway-config.json은app/config/config.php로 대체되었습니다. 이전 프로젝트를 업그레이드할 때php runway config:migrate로 마이그레이션하세요. 스켈레톤은 호환성을 위해 create-project 시 작은.runway-config.json을 계속 작성할 수 있습니다. 앞으로는config.php의runway키를 선호하세요.
프로젝트 루트 감지
Runway는 하위 디렉토리에서 실행하더라도 프로젝트의 루트를 감지할 수 있을 만큼 스마트합니다. composer.json, .git 또는 app/config/config.php와 같은 지표를 찾아 프로젝트 루트가 어디인지 결정합니다. 이는 프로젝트의 어디에서든 Runway 명령을 실행할 수 있음을 의미합니다!
사용법
Runway에는 Flight 애플리케이션을 관리하는 데 사용할 수 있는 여러 명령이 있습니다. Runway를 사용하는 두 가지 쉬운 방법이 있습니다.
- 스켈레톤 프로젝트를 사용하는 경우 프로젝트 루트에서
php runway [명령]을 실행할 수 있습니다. - composer를 통해 설치된 패키지로 Runway를 사용하는 경우 프로젝트 루트에서
vendor/bin/runway [명령]을 실행할 수 있습니다.
명령 목록
php runway 명령을 실행하여 사용 가능한 모든 명령 목록을 볼 수 있습니다.
php runway
설치에 실제로 나타나는 명령에만 의존하세요 (핵심 Runway 명령 대 스켈레톤의 migrate와 같은 프로젝트별 명령).
명령 도움말
모든 명령에 대해 --help 플래그를 전달하여 명령 사용 방법에 대한 자세한 정보를 얻을 수 있습니다.
php runway routes --help
php runway make:controller --help
몇 가지 예는 다음과 같습니다:
컨트롤러 생성
make:controller는 공식 스켈레톤 레이아웃과 일치하는 컨트롤러를 스캐폴딩합니다:
| 경로 | app/Controller/{Name}.php |
| 네임스페이스 | App\Controller |
| 스타일 | flight\Engine의 생성자 주입 (클래스 본문에 Flight:: 없음) |
php runway make:controller MyController
# → app/Controller/MyController.php
# namespace App\Controller;
기대할 수 있는 형태의 예시(간소화됨):
<?php
declare(strict_types=1);
namespace App\Controller;
use flight\Engine;
class MyController
{
protected Engine $app;
public function __construct(Engine $app)
{
$this->app = $app;
}
public function index(): void
{
// 예: $this->app->render('…', […]);
}
}
Dice가 컨트롤러를 빌드할 수 있도록 클래스 호출 가능으로 등록하세요:
// app/config/routes.php
use App\Controller\MyController;
$router->get('/mine', [MyController::class, 'index']);
이 레이아웃이 왜 중요한가요? 폴더 대소문자는 Linux에서 Composer PSR-4를 위해 네임스페이스와 일치해야 합니다 (Controller가 controllers가 아님)—자동 로딩 참조. 동일한 경로는 루트 및 범위가 지정된 AGENTS.md 파일이 AI 도구에 사용하도록 지시하는 것이므로 생성된 컨트롤러와 손으로 작성된 컨트롤러가 동일하게 유지됩니다.
이전 문서와 커뮤니티 프로젝트에서는 때때로
app/controllers/및app\controllers를 사용했습니다. 귀하의 트리가 여전히 소문자 폴더를 사용하는 경우 이는 유효합니다. 새 스켈레톤 프로젝트 및 현재make:controller출력은app/Controller/+App\Controller을 사용합니다.
Active Record 모델 생성
먼저 Active Record 플러그인을 설치했는지 확인하세요.
php runway make:record users
공식 스켈레톤에서 모델은 app/Model/ 아래에 App\Model 네임스페이스로 존재하며, DB 연결은 SimplePdo입니다 (주입하거나 ActiveRecord 생성자에 전달). 생성된 파일 이름/네임스페이스는 Runway의 현재 기본값과 runway 구성을 따릅니다—새 모델을 App\Model과 정렬하여 자동 로딩 및 AGENTS.md와 일치하도록 하세요.
스켈레톤 posts 데모와 일치하는 모델의 예:
<?php
declare(strict_types=1);
namespace App\Model;
use flight\ActiveRecord;
/**
* @property int $id
* @property string $title
* // …
*/
class Post extends ActiveRecord
{
protected array $relations = [];
public function __construct($databaseConnection)
{
parent::__construct($databaseConnection, 'posts');
}
}
이전 생성기가 여전히 app/records / app\records를 내보내는 경우 레거시 앱에서 해당 규칙을 유지하거나 파일을 app/Model/로 이동하고 네임스페이스를 폴더 대소문자와 일치하도록 업데이트할 수 있습니다.
마이그레이션 (스켈레톤)
공식 스켈레톤은 다음과 같은 프로젝트 명령을 제공합니다 (app/commands/에서 발견됨):
php runway migrate
마이그레이션은 migrations/ 아래의 SQL 파일입니다 (예: SQLite용 YYYYMMDDHHMMSS_description.sql, MySQL용 …_description.mysql.sql). 데이터베이스 드라이버 구성 / env에서 선택됩니다. 정확한 플래그 및 동작은 해당 프로젝트 명령으로 정의됩니다—앱에서 php runway migrate --help를 실행하세요.
AI 도우미
Runway는 AI 및 개발자 경험과 함께 사용되는 AI 지향 명령을 노출합니다:
php runway ai:init
php runway ai:generate-instructions
이러한 명령은 LLM 자격 증명을 저장하고 프로젝트 지침(주로 AGENTS.md)을 생성합니다. 스켈레톤에서 AGENTS.md(및 app/ 아래의 범위가 지정된 복사본)와 SECURITY.md를 에이전트의 진실 소스로 취급하세요.
모든 라우트 표시
이것은 Flight에 현재 등록된 모든 라우트를 표시합니다.
php runway routes
특정 라우트만 보려는 경우 플래그를 전달하여 라우트를 필터링할 수 있습니다.
# GET 라우트만 표시
php runway routes --get
# POST 라우트만 표시
php runway routes --post
# 등등.
Runway에 사용자 정의 명령 추가
Flight용 패키지를 만들거나 프로젝트에 사용자 정의 명령을 추가하려는 경우 프로젝트/패키지용으로 src/commands/, flight/commands/, app/commands/ 또는 commands/ 디렉토리를 만들어 수행할 수 있습니다. 추가 사용자 정의가 필요한 경우 아래 구성 섹션을 참조하세요.
스켈레톤에서 프로젝트 명령은 app/commands/에 App\Command 네임스페이스로 존재합니다. Runway는 경로로 이를 발견합니다. 해당 폴더를 프로젝트가 이미 하는 Composer 클래스맵/PSR-4와 동기화된 상태로 유지하세요.
명령을 만들려면 AbstractBaseCommand 클래스를 확장하고 최소한 __construct 메서드와 execute 메서드를 구현하세요.
<?php
declare(strict_types=1);
namespace App\Command;
use flight\commands\AbstractBaseCommand;
class ExampleCommand extends AbstractBaseCommand
{
/**
* 생성자
*
* @param array<string,mixed> $config app/config/config.php의 구성
*/
public function __construct(array $config)
{
parent::__construct('make:example', '문서를 위한 예제 생성', $config);
$this->argument('<funny-gif>', '재미있는 gif의 이름');
}
/**
* 함수 실행
*
* @return void
*/
public function execute()
{
$io = $this->app()->io();
$io->info('예제 생성 중...');
// 여기서 무언가를 수행
$io->ok('예제 생성 완료!');
}
}
Flight 애플리케이션에 사용자 정의 명령을 구축하는 방법에 대한 자세한 정보는 adhocore/php-cli 문서를 참조하세요!
구성 관리
v1.2.0부터 구성이 app/config/config.php로 이동했으므로 구성을 관리하는 몇 가지 도우미 명령이 있습니다.
스켈레톤 팁:
config.php를 리터럴 PHP 값으로 유지하세요. 비밀은.env에 속합니다.config.php내부에서$_ENV[...]표현식을 피하세요—config:set는 해당 파일을 정적 데이터로 다시 작성하며 파일에 비밀을 구울 수 있습니다. 구성 참조.
이전 구성 마이그레이션
이전 .runway-config.json 파일이 있는 경우 다음 명령으로 app/config/config.php로 쉽게 마이그레이션할 수 있습니다:
php runway config:migrate
구성 값 설정
config:set 명령을 사용하여 구성 값을 설정할 수 있습니다. 파일을 열지 않고 구성 값을 업데이트하려는 경우 유용합니다.
php runway config:set app_root "app/"
구성 값 가져오기
config:get 명령을 사용하여 구성 값을 가져올 수 있습니다.
php runway config:get app_root
모든 Runway 구성
Runway의 구성을 사용자 정의해야 하는 경우 app/config/config.php에서 이러한 값을 설정할 수 있습니다. 설정할 수 있는 몇 가지 추가 구성은 다음과 같습니다:
<?php
// app/config/config.php
return [
// ... 기타 구성 값 ...
'runway' => [
// 애플리케이션 디렉토리가 있는 위치입니다
'app_root' => 'app/',
// 루트 인덱스 파일이 있는 디렉토리입니다
'index_root' => 'public/',
// 다른 프로젝트 루트의 경로입니다
'root_paths' => [
'/home/user/different-project',
'/var/www/another-project'
],
// 기본 경로는 구성할 필요가 없을 가능성이 높지만 원하는 경우 여기에 있습니다
'base_paths' => [
'/includes/libs/vendor', // 공급업체 디렉토리나 다른 것에 대해 정말 고유한 경로가 있는 경우
],
// 최종 경로는 명령 파일을 검색할 프로젝트 내 위치입니다
'final_paths' => [
'src/diff-path/commands',
'app/module/admin/commands',
],
// 전체 경로를 추가하려면 바로 추가하세요 (프로젝트 루트를 기준으로 절대 또는 상대)
'paths' => [
'/home/user/different-project/src/diff-path/commands',
'/var/www/another-project/app/module/admin/commands',
'app/my-unique-commands'
]
]
];
구성 접근
구성 값을 효과적으로 접근해야 하는 경우 __construct 메서드 또는 app() 메서드를 통해 접근할 수 있습니다. app/config/services.php 파일이 있는 경우 해당 서비스도 명령에서 사용할 수 있다는 점도 중요합니다.
public function execute()
{
$io = $this->app()->io();
// 구성 접근
$app_root = $this->config['runway']['app_root'];
// 아마도 데이터베이스 연결과 같은 서비스 접근
$database = $this->config['database']
// ...
}
AI 도우미 래퍼
Runway에는 AI가 명령을 생성하는 데 더 쉽게 만드는 몇 가지 도우미 래퍼가 있습니다. Symfony Console과 유사한 방식으로 addOption과 addArgument를 사용할 수 있습니다. 이는 AI 도구를 사용하여 명령을 생성하는 경우 유용합니다.
public function __construct(array $config)
{
parent::__construct('make:example', '문서를 위한 예제 생성', $config);
// 모드 인수는 null 가능하며 완전히 선택 사항으로 기본 설정됩니다
$this->addOption('name', '예제의 이름', null);
}
참고 항목
- 설치 - 스켈레톤 트리 및 create-project 기본값
- 자동 로딩 -
App\및 폴더 대소문자 - 의존성 주입 - 생성된 컨트롤러를 위한 Dice + Engine 주입
- AI 및 개발자 경험 -
ai:init,ai:generate-instructions,AGENTS.md - Active Record -
make:record/ 스켈레톤App\Model과 함께 사용되는 모델 - SimplePdo - 스켈레톤 마이그레이션 및 모델에 사용되는 DB 연결
Awesome-plugins/tracy_extensions
Tracy Flight 패널 확장
이것은 Flight 작업을 조금 더 풍부하게 만들어주는 확장 세트입니다.
- Flight - 모든 Flight 변수를 분석합니다.
- Database - 페이지에서 실행된 모든 쿼리를 분석합니다(데이터베이스 연결을 올바르게 초기화한 경우)
- Request - 모든
$_SERVER변수와 모든 전역 페이로드($_GET,$_POST,$_FILES)를 검사합니다. - Session - 세션이 활성화된 경우 모든
$_SESSION변수를 분석합니다. - Twig (선택사항) - Twig 템플릿 렌더 시간, 메모리 및 어떤 템플릿/블록/매크로가 실행되었는지 분석합니다(
twig/twig및twig_profile설정 필요)
이것은 Twig를 기본으로 사용하는 공식 스켈레톤과 함께 사용하기 특히 편리합니다: 동일한 레이아웃 AI 도구도 Tracy 바에 명확하게 표시됩니다.
이것이 패널입니다

각 패널은 애플리케이션에 대한 매우 유용한 정보를 표시합니다!

코드를 보려면 여기를 클릭하세요.
설치
composer require flightphp/tracy-extensions --dev를 실행하면 시작할 수 있습니다!
Twig는 패키지의 필수 종속성이 아닙니다. Twig 패널을 원하는 경우에만 twig/twig를 설치하세요(스켈레톤은 이미 뷰에 대해 설치되어 있습니다).
구성
이것을 시작하기 위해 필요한 구성은 거의 없습니다. 이 확장을 사용하기 전에 Tracy 디버거를 초기화해야 합니다 https://tracy.nette.org/en/guide:
<?php
use Tracy\Debugger;
use flight\debug\tracy\TracyExtensionLoader;
// 부트스트랩 코드
require __DIR__ . '/vendor/autoload.php';
Debugger::enable();
// 환경을 지정해야 할 수 있습니다: Debugger::enable(Debugger::DEVELOPMENT)
// 앱에서 데이터베이스 연결을 사용하는 경우,
// 개발 전용으로 사용해야 하는 PDO 래퍼가 필요합니다(프로덕션에서는 사용하지 마세요!)
// 일반 PDO 연결과 동일한 매개변수를 가집니다
$pdo = new PdoQueryCapture('sqlite:test.db', 'user', 'pass');
// 또는 Flight 프레임워크에 연결하는 경우
Flight::register('db', PdoQueryCapture::class, ['sqlite:test.db', 'user', 'pass']);
// 이제 쿼리를 만들 때마다 시간, 쿼리 및 매개변수가 캡처됩니다
// 이것이 모든 것을 연결합니다
if(Debugger::$showBar === true) {
// 이것은 false여야 합니다. 그렇지 않으면 Tracy가 실제로 렌더링할 수 없습니다 :(
Flight::set('flight.content_length', false);
new TracyExtensionLoader(Flight::app());
}
// 더 많은 코드
Flight::start();
추가 구성
세션 데이터
ghostff/session과 같은 사용자 정의 세션 핸들러가 있는 경우, 세션 데이터의 배열을 Tracy에 전달할 수 있으며 Tracy가 자동으로 출력합니다. TracyExtensionLoader 생성자의 두 번째 매개변수에서 session_data 키로 전달합니다.
use Ghostff\Session\Session;
// 또는 flight\Session을 사용합니다;
require 'vendor/autoload.php';
$app = Flight::app();
$app->register('session', Session::class);
if(Debugger::$showBar === true) {
// 이것은 false여야 합니다. 그렇지 않으면 Tracy가 실제로 렌더링할 수 없습니다 :(
Flight::set('flight.content_length', false);
new TracyExtensionLoader(Flight::app(), [ 'session_data' => Flight::session()->getAll() ]);
}
// 라우트 및 기타 항목...
Flight::start();
Twig 패널 (선택사항)
앱이 Twig를 사용하는 경우(공식 스켈레톤 포함), Tracy 바에 템플릿 메트릭을 표시할 수 있습니다. Twig Profile을 생성하고, 환경에 ProfilerExtension을 연결한 다음, 해당 프로필을 twig_profile 키 아래의 로더에 전달합니다. 개발에서만 프로파일링을 연결합니다.
<?php
use flight\debug\tracy\TracyExtensionLoader;
use flight\debug\tracy\TwigTracyExtension;
use Tracy\Debugger;
use Twig\Environment;
use Twig\Extension\ProfilerExtension;
use Twig\Loader\FilesystemLoader;
use Twig\Profiler\Profile;
$loader = new FilesystemLoader(__DIR__ . '/views');
$twig = new Environment($loader, [
'debug' => true,
'cache' => false,
]);
// 선택사항: 템플릿에 Tracy 덤프 헬퍼 노출
// {{ dump(var) }}, {{ bdump(var) }}, {{ dumpe(var) }}
$twig->addExtension(new TwigTracyExtension());
$tracyConfig = [];
if (Debugger::$showBar === true) {
$profile = new Profile();
$twig->addExtension(new ProfilerExtension($profile));
$tracyConfig['twig_profile'] = $profile;
}
if (Debugger::$showBar === true) {
Flight::set('flight.content_length', false);
new TracyExtensionLoader(Flight::app(), $tracyConfig);
}
// Flight::render()를 Twig에 매핑 (예시)
Flight::map('render', function (string $template, array $data = []) use ($twig) {
if (substr($template, -5) !== '.twig') {
$template .= '.twig';
}
echo $twig->render($template, $data);
});
패널에 표시되는 내용
- 총 Twig 렌더 시간 및 메모리
- 템플릿 / 블록 / 매크로 호출 횟수
- 렌더링된 각 템플릿과 해당 시간 및 메모리
Twig 탭은 요청에 대해 렌더링된 템플릿이 없거나 twig_profile을 생략하거나 Twig가 설치되지 않은 경우 숨겨집니다. 다른 Flight 패널은 계속 작동합니다.
스켈레톤 스타일의 services.php에서 디버그가 켜져 있을 때 동일한 $profile / ProfilerExtension을 빌드하고, TracyExtensionLoader에 twig_profile을 전달하고, $app->render()에 대해 공유 Twig 환경을 계속 사용합니다.
Latte
이 섹션에는 PHP 8.1+가 필요합니다.
프로젝트에 Latte가 설치되어 있는 경우, Tracy는 Latte와의 기본 통합을 통해 템플릿을 분석합니다. Latte 인스턴스에 확장을 등록하기만 하면 됩니다(이는 위의 Twig 패널이 아닌 Latte의 자체 Tracy 브리지입니다).
require 'vendor/autoload.php';
$app = Flight::app();
$app->map('render', function($template, $data, $block = null) {
$latte = new Latte\Engine;
// 기타 구성...
// Tracy 디버그 바가 활성화된 경우에만 확장을 추가합니다
if(Debugger::$showBar === true) {
// 이것이 Latte 패널을 Tracy에 추가하는 곳입니다
$latte->addExtension(new Latte\Bridges\Tracy\TracyExtension);
}
$latte->render($template, $data, $block);
});
참고 항목
- Tracy - Flight용 기본 Tracy 설정
- Twig - 스켈레톤 및 Twig 패널에서 사용하는 템플릿 엔진
- Templates - Flight이
render를 Twig/Latte에 매핑하는 방법 - Installation - 스켈레톤에는 dev에 tracy-extensions가 포함되어 있습니다
Awesome-plugins/apm
FlightPHP APM 문서
FlightPHP APM에 오신 것을 환영합니다—앱의 개인 성능 코치입니다! 이 가이드는 FlightPHP를 사용한 애플리케이션 성능 모니터링(APM)의 설정, 사용 및 마스터링을 위한 로드맵입니다. 느린 요청을 찾거나 지연 시간 차트를 보고 싶으시다면, 저희가 도와드리겠습니다. 앱을 더 빠르게 만들고, 사용자를 더 행복하게 하며, 디버깅 세션을 더 쉽게 만들어 보세요!
Flight Docs 사이트의 대시보드 데모를 확인하세요.

APM이 중요한 이유
상상해 보세요: 앱이 바쁜 레스토랑입니다. 주문이 얼마나 걸리는지, 주방이 어디에서 지연되는지 추적할 방법이 없다면, 고객이 왜 불만을 품고 떠나는지 추측하게 됩니다. APM은 소셰프입니다—들어오는 요청부터 데이터베이스 쿼리까지 모든 단계를 감시하고, 속도를 늦추는 모든 것을 표시합니다. 느린 페이지는 사용자를 잃게 만듭니다(연구에 따르면 사이트 로딩이 3초 이상 걸리면 53%가 이탈한다고 합니다!), APM은 문제가 발생하기 전에 이를 잡아줍니다. 이는 선제적 안심입니다—"이게 왜 고장났지?" 순간이 줄어들고, "이게 얼마나 매끄럽게 실행되는지 보세요!"라는 승리가 늘어납니다.
설치
Composer로 시작하세요:
composer require flightphp/apm
필요한 것:
- PHP 7.4+: 현대 PHP를 지원하면서 LTS Linux 배포판과 호환됩니다.
- FlightPHP Core v3.15+: 우리가 개선하고 있는 경량 프레임워크.
지원되는 데이터베이스
FlightPHP APM은 현재 메트릭 저장을 위해 다음 데이터베이스를 지원합니다:
- SQLite3: 간단하고 파일 기반이며 로컬 개발이나 소규모 앱에 적합합니다. 대부분의 설정에서 기본 옵션입니다.
- MySQL/MariaDB: 견고하고 확장 가능한 저장소가 필요한 대규모 프로젝트나 프로덕션 환경에 이상적입니다.
구성 단계(아래 참조)에서 데이터베이스 유형을 선택할 수 있습니다. PHP 환경에 필요한 확장(예: pdo_sqlite 또는 pdo_mysql)이 설치되어 있는지 확인하세요.
시작하기
APM의 훌륭함을 위한 단계별 가이드입니다:
1. APM 등록
추적을 시작하려면 index.php 또는 services.php 파일에 다음을 추가하세요:
use flight\apm\logger\LoggerFactory;
use flight\database\SimplePdo;
use flight\Apm;
$ApmLogger = LoggerFactory::create(__DIR__ . '/../../.runway-config.json');
$Apm = new Apm($ApmLogger);
$Apm->bindEventsToFlightInstance($app);
// 데이터베이스 연결을 추가하는 경우
// SimplePdo(또는 개발 시 Tracy Extensions의 PdoQueryCapture)를 선호하세요.
// 옵션 배열(5번째 인수)을 통해 APM 쿼리 추적을 활성화하세요.
$pdo = new SimplePdo('mysql:host=localhost;dbname=example', 'user', 'pass', null, [
'trackApmQueries' => true, // APM을 위한 쿼리 캡처에 필요
]);
$Apm->addPdoConnection($pdo);
여기서 무슨 일이 일어나고 있나요?
LoggerFactory::create()는 설정(곧 자세히 설명)을 가져와 로거를 설정합니다—기본값은 SQLite입니다.Apm이 스타입니다—Flight의 이벤트(요청, 경로, 오류 등)를 수신하고 메트릭을 수집합니다.bindEventsToFlightInstance($app)는 모든 것을 Flight 앱에 연결합니다.
전문가 팁: 샘플링 앱이 바쁘다면 모든 요청을 로깅하면 과부하가 걸릴 수 있습니다. 샘플 비율(0.0에서 1.0)을 사용하세요:
$Apm = new Apm($ApmLogger, 0.1); // 요청의 10%를 로깅
이렇게 하면 성능을 빠르게 유지하면서도 견고한 데이터를 제공합니다.
2. 구성하기
.runway-config.json을 생성하려면 다음을 실행하세요:
php vendor/bin/runway apm:init
이게 무슨 역할을 하나요?
- 원시 메트릭이 어디에서 오는지(소스)와 처리된 데이터가 어디로 가는지(대상)를 묻는 마법사를 시작합니다.
- 기본값은 SQLite입니다—예를 들어 소스의 경우
sqlite:/tmp/apm_metrics.sqlite, 대상의 경우 다른 것. - 다음과 같은 설정이 생성됩니다:
{ "apm": { "source_type": "sqlite", "source_db_dsn": "sqlite:/tmp/apm_metrics.sqlite", "storage_type": "sqlite", "dest_db_dsn": "sqlite:/tmp/apm_metrics_processed.sqlite" } }
이 과정은 또한 이 설정에 대한 마이그레이션을 실행할지 묻습니다. 처음 설정하는 경우, 답은 예입니다.
왜 두 개의 위치가 필요한가요? 원시 메트릭은 빠르게 쌓입니다(필터링되지 않은 로그를 생각해 보세요). 워커는 대시보드를 위한 구조화된 대상으로 처리합니다. 깔끔하게 유지됩니다!
3. 워커로 메트릭 처리하기
워커는 원시 메트릭을 대시보드 준비 데이터로 변환합니다. 한 번 실행하세요:
php vendor/bin/runway apm:worker
무슨 일을 하나요?
- 소스(예:
apm_metrics.sqlite)에서 읽습니다. - 최대 100개의 메트릭(기본 배치 크기)을 대상으로 처리합니다.
- 완료되거나 남은 메트릭이 없으면 중지됩니다.
계속 실행하기 라이브 앱의 경우, 지속적인 처리가 필요합니다. 옵션은 다음과 같습니다:
-
데몬 모드:
php vendor/bin/runway apm:worker --daemon영원히 실행되며, 메트릭이 들어올 때마다 처리합니다. 개발이나 소규모 설정에 적합합니다.
-
Crontab: crontab에 다음을 추가하세요(
crontab -e):* * * * * php /path/to/project/vendor/bin/runway apm:worker매분 실행됩니다—프로덕션에 완벽합니다.
-
Tmux/Screen: 분리 가능한 세션을 시작하세요:
tmux new -s apm-worker php vendor/bin/runway apm:worker --daemon # Ctrl+B, 그 다음 D로 분리; `tmux attach -t apm-worker`로 재연결로그아웃해도 계속 실행됩니다.
-
사용자 정의 조정:
php vendor/bin/runway apm:worker --batch_size 50 --max_messages 1000 --timeout 300--batch_size 50: 한 번에 50개의 메트릭을 처리합니다.--max_messages 1000: 1000개의 메트릭 후 중지합니다.--timeout 300: 5분 후 종료합니다.
왜 신경 써야 하나요? 워커 없이는 대시보드가 비어 있습니다. 원시 로그와 실행 가능한 인사이트 사이의 다리입니다.
4. 대시보드 시작하기
앱의 상태를 확인하세요:
php vendor/bin/runway apm:dashboard
이게 무슨 역할을 하나요?
http://localhost:8001/apm/dashboard에서 PHP 서버를 시작합니다.- 요청 로그, 느린 경로, 오류율 등을 표시합니다.
사용자 정의하기:
php vendor/bin/runway apm:dashboard --host 0.0.0.0 --port 8080 --php-path=/usr/local/bin/php
--host 0.0.0.0: 모든 IP에서 접근 가능(원격 보기용으로 편리함).--port 8080: 8001이 사용 중인 경우 다른 포트를 사용합니다.--php-path: PHP가 PATH에 없는 경우 PHP를 가리킵니다.
브라우저에서 URL을 클릭하고 탐색하세요!
프로덕션 모드
프로덕션의 경우, 방화벽 및 기타 보안 조치가 있을 가능성이 높으므로 대시보드를 실행하기 위해 몇 가지 기술을 시도해야 할 수 있습니다. 몇 가지 옵션은 다음과 같습니다:
- 역방향 프록시 사용: Nginx 또는 Apache를 설정하여 요청을 대시보드로 전달합니다.
- SSH 터널: 서버에 SSH할 수 있는 경우,
ssh -L 8080:localhost:8001 youruser@yourserver를 사용하여 대시보드를 로컬 머신으로 터널링합니다. - VPN: 서버가 VPN 뒤에 있는 경우, VPN에 연결하고 대시보드에 직접 접근합니다.
- 방화벽 구성: IP 또는 서버의 네트워크에 대해 포트 8001을 엽니다. (또는 설정한 포트).
- Apache/Nginx 구성: 애플리케이션 앞에 웹 서버가 있는 경우, 도메인 또는 하위 도메인으로 구성할 수 있습니다. 이렇게 하는 경우, 문서 루트를
/path/to/your/project/vendor/flightphp/apm/dashboard로 설정합니다.
다른 대시보드를 원하시나요?
원하는 경우 직접 대시보드를 만들 수 있습니다! vendor/flightphp/apm/src/apm/presenter 디렉토리를 확인하여 자체 대시보드용 데이터를 표시하는 방법에 대한 아이디어를 얻으세요!
대시보드 기능
대시보드는 APM HQ입니다—여기서 볼 수 있는 것들입니다:
- 요청 로그: 타임스탬프, URL, 응답 코드 및 총 시간이 포함된 모든 요청. 미들웨어, 쿼리 및 오류에 대한 "세부 정보"를 클릭하세요.
- 가장 느린 요청: 시간을 많이 소모하는 상위 5개 요청(예: 2.5초의 "/api/heavy").
- 가장 느린 경로: 평균 시간 기준 상위 5개 경로—패턴을 찾는 데 좋습니다.
- 오류율: 실패하는 요청의 백분율(예: 2.3%의 500 오류).
- 지연 시간 백분위수: 95번째(p95) 및 99번째(p99) 응답 시간—최악의 시나리오를 파악하세요.
- 응답 코드 차트: 시간 경과에 따른 200, 404, 500 오류를 시각화합니다.
- 긴 쿼리/미들웨어: 상위 5개의 느린 데이터베이스 호출 및 미들웨어 계층.
- 캐시 히트/미스: 캐시가 얼마나 자주 문제를 해결하는지.
추가 기능:
- "지난 1시간", "지난 1일" 또는 "지난 1주"로 필터링.
- 야간 세션을 위한 다크 모드 전환.
예시:
/users에 대한 요청은 다음을 표시할 수 있습니다:
- 총 시간: 150ms
- 미들웨어:
AuthMiddleware->handle(50ms) - 쿼리:
SELECT * FROM users(80ms) - 캐시:
user_list히트 (5ms)
사용자 정의 이벤트 추가하기
API 호출이나 결제 프로세스와 같은 모든 것을 추적하세요:
use flight\apm\CustomEvent;
$app->eventDispatcher()->trigger('apm.custom', new CustomEvent('api_call', [
'endpoint' => 'https://api.example.com/users',
'response_time' => 0.25,
'status' => 200
]));
어디에 표시되나요? 대시보드의 요청 세부 정보에서 "사용자 정의 이벤트" 아래에 표시됩니다—예쁜 JSON 형식으로 확장 가능합니다.
사용 사례:
$start = microtime(true);
$apiResponse = file_get_contents('https://api.example.com/data');
$app->eventDispatcher()->trigger('apm.custom', new CustomEvent('external_api', [
'url' => 'https://api.example.com/data',
'time' => microtime(true) - $start,
'success' => $apiResponse !== false
]));
이제 해당 API가 앱을 지연시키는지 확인할 수 있습니다!
데이터베이스 모니터링
PDO 쿼리를 다음과 같이 추적하세요:
use flight\database\SimplePdo;
$pdo = new SimplePdo('sqlite:/path/to/db.sqlite', null, null, null, [
'trackApmQueries' => true, // APM을 위한 쿼리 캡처에 필요
]);
$Apm->addPdoConnection($pdo);
얻을 수 있는 것:
- 쿼리 텍스트(예:
SELECT * FROM users WHERE id = ?) - 실행 시간(예: 0.015s)
- 행 수(예: 42)
주의사항:
- 선택 사항: DB 추적이 필요하지 않은 경우 건너뛰세요.
- SimplePdo(선호됨):
trackApmQueries => true와 함께SimplePdo를 사용하세요. 더 이상 사용되지 않는PdoWrapper도 여전히 작동합니다(5번째 생성자 인수true). 원시 코어 PDO는 아직 연결되지 않았습니다—기다려 주세요! - 성능 경고: DB가 많은 사이트에서 모든 쿼리를 로깅하면 속도가 느려질 수 있습니다. 부하를 줄이기 위해 샘플링(
$Apm = new Apm($ApmLogger, 0.1))을 사용하세요.
예시 출력:
- 쿼리:
SELECT name FROM products WHERE price > 100 - 시간: 0.023s
- 행: 15
워커 옵션
워커를 원하는 대로 조정하세요:
--timeout 300: 5분 후 중지—테스트용으로 좋습니다.--max_messages 500: 500개의 메트릭으로 제한—유한하게 유지합니다.--batch_size 200: 한 번에 200개 처리—속도와 메모리의 균형을 맞춥니다.--daemon: 중단 없이 실행—실시간 모니터링에 이상적입니다.
예시:
php vendor/bin/runway apm:worker --daemon --batch_size 100 --timeout 3600
한 시간 동안 실행되며, 한 번에 100개의 메트릭을 처리합니다.
앱의 요청 ID
각 요청에는 추적을 위한 고유한 요청 ID가 있습니다. 이 ID를 사용하여 로그와 메트릭을 상호 연관시킬 수 있습니다. 예를 들어 오류 페이지에 요청 ID를 추가할 수 있습니다:
Flight::map('error', function($message) {
// 응답 헤더 X-Flight-Request-Id에서 요청 ID를 가져옵니다
$requestId = Flight::response()->getHeader('X-Flight-Request-Id');
// 추가로 Flight 변수에서 가져올 수도 있습니다
// 이 방법은 swoole 또는 기타 비동기 플랫폼에서 잘 작동하지 않습니다.
// $requestId = Flight::get('apm.request_id');
echo "Error: $message (Request ID: $requestId)";
});
업그레이드
APM의 최신 버전으로 업그레이드하는 경우, 실행해야 할 데이터베이스 마이그레이션이 있을 수 있습니다. 다음 명령을 실행하여 이 작업을 수행할 수 있습니다:
php vendor/bin/runway apm:migrate
이 명령은 데이터베이스 스키마를 최신 버전으로 업데이트하는 데 필요한 마이그레이션을 실행합니다.
참고: APM 데이터베이스가 크기가 큰 경우, 이러한 마이그레이션에 시간이 걸릴 수 있습니다. 피크 시간이 아닌 시간에 이 명령을 실행하는 것이 좋습니다.
0.4.3 -> 0.5.0으로 업그레이드
0.4.3에서 0.5.0으로 업그레이드하는 경우, 다음 명령을 실행해야 합니다:
php vendor/bin/runway apm:config-migrate
이 명령은 .runway-config.json 파일을 사용하는 이전 형식에서 config.php 파일에 키/값을 저장하는 새 형식으로 설정을 마이그레이션합니다.
오래된 데이터 삭제
데이터베이스를 깔끔하게 유지하려면 오래된 데이터를 삭제할 수 있습니다. 이는 바쁜 앱을 실행하고 데이터베이스 크기를 관리 가능한 상태로 유지하려는 경우 특히 유용합니다. 다음 명령을 실행하여 이 작업을 수행할 수 있습니다:
php vendor/bin/runway apm:purge
이 명령은 데이터베이스에서 30일 이상 된 모든 데이터를 제거합니다. --days 옵션에 다른 값을 전달하여 일 수를 조정할 수 있습니다:
php vendor/bin/runway apm:purge --days 7
이 명령은 데이터베이스에서 7일 이상 된 모든 데이터를 제거합니다.
문제 해결
막혔나요? 다음을 시도해 보세요:
-
대시보드 데이터가 없나요?
- 워커가 실행 중인가요?
ps aux | grep apm:worker를 확인하세요. - 설정 경로가 일치하나요?
.runway-config.jsonDSN이 실제 파일을 가리키는지 확인하세요. - 보류 중인 메트릭을 처리하기 위해
php vendor/bin/runway apm:worker를 수동으로 실행하세요.
- 워커가 실행 중인가요?
-
워커 오류?
- SQLite 파일을 확인하세요(예:
sqlite3 /tmp/apm_metrics.sqlite "SELECT * FROM apm_metrics_log LIMIT 5"). - PHP 로그에서 스택 트레이스를 확인하세요.
- SQLite 파일을 확인하세요(예:
-
대시보드가 시작되지 않나요?
- 포트 8001이 사용 중인가요?
--port 8080을 사용하세요. - PHP를 찾을 수 없나요?
--php-path /usr/bin/php를 사용하세요. - 방화벽이 차단하나요? 포트를 열거나
--host localhost를 사용하세요.
- 포트 8001이 사용 중인가요?
-
너무 느린가요?
- 샘플 비율을 낮추세요:
$Apm = new Apm($ApmLogger, 0.05)(5%). - 배치 크기를 줄이세요:
--batch_size 20.
- 샘플 비율을 낮추세요:
-
예외/오류를 추적하지 않나요?
- 프로젝트에 Tracy가 활성화되어 있는 경우, Flight의 오류 처리를 재정의합니다. Tracy를 비활성화한 다음
Flight::set('flight.handle_errors', true);가 설정되어 있는지 확인해야 합니다.
- 프로젝트에 Tracy가 활성화되어 있는 경우, Flight의 오류 처리를 재정의합니다. Tracy를 비활성화한 다음
-
데이터베이스 쿼리를 추적하지 않나요?
- 5번째 생성자 인수(옵션 배열)로
['trackApmQueries' => true]와 함께SimplePdo를 선호하세요. - 더 이상 사용되지 않는
PdoWrapper를 여전히 사용하는 경우, 5번째 인수로true를 전달하세요. - 연결을 생성한 후
$Apm->addPdoConnection($pdo)를 호출하세요.
- 5번째 생성자 인수(옵션 배열)로
Awesome-plugins/tracy
트레이시
트레이시는 Flight과 함께 사용할 수 있는 놀라운 오류 핸들러입니다. 애플리케이션 디버깅에 도움이 되는 여러 패널을 가지고 있습니다. 또한 확장하기가 매우 쉽고 자신만의 패널을 추가할 수 있습니다. Flight 팀은 flightphp/tracy-extensions 플러그인으로 Flight 프로젝트를 위한 몇 가지 패널을 만들었습니다(Flight 변수, DB 쿼리, 요청, 세션, 그리고 프로파일러 프로필을 전달할 때 선택적인 Twig 패널 - Tracy Extensions 참조).
설치
컴포저로 설치합니다. 그리고 Tracy는 프로덕션 오류 처리 컴포넌트를 제공하므로 실제로 개발 버전 없이 설치하는 것이 좋습니다.
composer require tracy/tracy
기본 설정
시작하기 위한 몇 가지 기본 설정 옵션이 있습니다. 자세한 내용은 Tracy 문서에서 확인할 수 있습니다.
require 'vendor/autoload.php';
use Tracy\Debugger;
// Tracy 활성화
Debugger::enable();
// Debugger::enable(Debugger::DEVELOPMENT) // 때때로 명시적으로 지정해야 합니다 (Debugger::PRODUCTION도 마찬가지)
// Debugger::enable('23.75.345.200'); // IP 주소 배열을 제공할 수도 있습니다
// 오류와 예외가 기록될 위치입니다. 이 디렉토리가 존재하고 쓰기 가능해야 합니다.
Debugger::$logDirectory = __DIR__ . '/../log/';
Debugger::$strictMode = true; // 모든 오류 표시
// Debugger::$strictMode = E_ALL & ~E_DEPRECATED & ~E_USER_DEPRECATED; // 더 이상 사용되지 않는 알림을 제외한 모든 오류
if (Debugger::$showBar) {
$app->set('flight.content_length', false); // 디버거 바가 표시되면 Flight에서 content-length를 설정할 수 없습니다
// flightphp/tracy-extensions를 포함한 경우 Flight용 Tracy 확장에 특화된 코드입니다
// 그렇지 않으면 이 부분을 주석 처리하세요.
new TracyExtensionLoader($app);
}
유용한 팁
코드를 디버깅할 때 데이터를 출력하는 데 매우 유용한 함수들이 있습니다.
bdump($var)- 변수를 별도의 패널로 Tracy 바에 덤프합니다.dumpe($var)- 변수를 덤프한 후 즉시 종료합니다.
Awesome-plugins/active_record
Flight Active Record
액티브 레코드는 데이터베이스 엔티티를 PHP 객체에 매핑하는 것입니다. 간단히 말해, 데이터베이스에 users 테이블이 있으면 테이블의 행을 코드베이스의 User 클래스와 $user 객체로 "번역"할 수 있습니다. 기본 예제를 참조하세요.
GitHub의 저장소를 보려면 여기를 클릭하세요.
기본 예제
다음과 같은 테이블이 있다고 가정해 보겠습니다:
CREATE TABLE users (
id INTEGER PRIMARY KEY,
name TEXT,
password TEXT
);
이제 이 테이블을 나타내는 새 클래스를 설정할 수 있습니다:
/**
* ActiveRecord 클래스는 보통 단수형입니다
*
* 테이블의 속성을 주석으로 여기에 추가하는 것이 강력히 권장됩니다
*
* @property int $id
* @property string $name
* @property string $password
*/
class User extends flight\ActiveRecord {
public function __construct($database_connection)
{
// 이렇게 설정할 수 있습니다
parent::__construct($database_connection, 'users');
// 또는 이렇게
parent::__construct($database_connection, null, [ 'table' => 'users']);
}
}
이제 마법이 일어나는 것을 보세요!
// sqlite의 경우
$database_connection = new PDO('sqlite:test.db'); // 이것은 예제일 뿐입니다. 실제 데이터베이스 연결을 사용할 가능성이 큽니다
// mysql의 경우
$database_connection = new PDO('mysql:host=localhost;dbname=test_db&charset=utf8bm4', 'username', 'password');
// 또는 mysqli
$database_connection = new mysqli('localhost', 'username', 'password', 'test_db');
// 또는 객체 기반이 아닌 mysqli 생성
$database_connection = mysqli_connect('localhost', 'username', 'password', 'test_db');
$user = new User($database_connection);
$user->name = 'Bobby Tables';
$user->password = password_hash('some cool password');
$user->insert();
// 또는 $user->save();
echo $user->id; // 1
$user->name = 'Joseph Mamma';
$user->password = password_hash('some cool password again!!!');
$user->insert();
// 여기서 $user->save()를 사용할 수 없으며, 업데이트로 인식할 것입니다!
echo $user->id; // 2
새 사용자를 추가하는 것이 이렇게 쉬웠습니다! 이제 데이터베이스에 사용자 행이 있으므로, 이를 어떻게 가져오나요?
$user->find(1); // 데이터베이스에서 id = 1을 찾아 반환합니다.
echo $user->name; // 'Bobby Tables'
모든 사용자를 찾고 싶다면?
$users = $user->findAll();
특정 조건으로?
$users = $user->like('name', '%mamma%')->findAll();
얼마나 재미있나요? 설치하고 시작해 보세요!
설치
Composer로 간단히 설치하세요
composer require flightphp/active-record
사용법
이것은 독립 라이브러리로 사용하거나 Flight PHP Framework와 함께 사용할 수 있습니다. 완전히 당신의 선택입니다.
독립 사용
생성자에 PDO 연결을 전달하기만 하세요.
$pdo_connection = new PDO('sqlite:test.db'); // 이것은 예제일 뿐입니다. 실제 데이터베이스 연결을 사용할 가능성이 큽니다
$User = new User($pdo_connection);
생성자에서 항상 데이터베이스 연결을 설정하지 않으려면? 데이터베이스 연결 관리를 참조하세요!
Flight에서 메서드로 등록
Flight PHP Framework를 사용 중이라면, ActiveRecord 클래스를 서비스로 등록할 수 있지만, 꼭 해야 하는 것은 아닙니다.
Flight::register('user', 'User', [ $pdo_connection ]);
// 그런 다음 컨트롤러, 함수 등에서 이렇게 사용할 수 있습니다.
Flight::user()->find(1);
runway 메서드
runway는 Flight를 위한 CLI 도구로, 이 라이브러리에 대한 사용자 지정 명령어를 가지고 있습니다.
# 사용법
php runway make:record database_table_name [class_name]
# 예제
php runway make:record users
이것은 app/records/ 디렉토리에 UserRecord.php로 새 클래스를 생성하며, 다음과 같은 내용을 포함합니다:
<?php
declare(strict_types=1);
namespace app\records;
/**
* users 테이블을 위한 ActiveRecord 클래스.
* @link https://docs.flightphp.com/awesome-plugins/active-record
*
* @property int $id
* @property string $username
* @property string $email
* @property string $password_hash
* @property string $created_dt
*/
class UserRecord extends \flight\ActiveRecord
{
/**
* @var array $relations 모델의 관계 설정
* https://docs.flightphp.com/awesome-plugins/active-record#relationships
*/
protected array $relations = [
// 'relation_name' => [ self::HAS_MANY, 'RelatedClass', 'foreign_key' ],
];
/**
* 생성자
* @param mixed $databaseConnection 데이터베이스 연결
*/
public function __construct($databaseConnection)
{
parent::__construct($databaseConnection, 'users');
}
}
CRUD 함수
find($id = null) : boolean|ActiveRecord
하나의 레코드를 찾아 현재 객체에 할당합니다. $id를 전달하면 해당 값으로 기본 키를 조회합니다. 아무것도 전달하지 않으면 테이블의 첫 번째 레코드를 찾습니다.
또한 테이블을 쿼리하기 위해 다른 도우미 메서드를 전달할 수 있습니다.
// 사전 조건으로 레코드 찾기
$user->notNull('password')->orderBy('id DESC')->find();
// 특정 id로 레코드 찾기
$id = 123;
$user->find($id);
findAll(): array<int,ActiveRecord>
지정된 테이블의 모든 레코드를 찾습니다.
$user->findAll();
isHydrated(): boolean (v0.4.0)
현재 레코드가 데이터베이스에서 로드(hydrated)되었는지 true를 반환합니다.
$user->find(1);
// 데이터가 있는 레코드가 발견되면...
$user->isHydrated(); // true
insert(): boolean|ActiveRecord
현재 레코드를 데이터베이스에 삽입합니다.
$user = new User($pdo_connection);
$user->name = 'demo';
$user->password = md5('demo');
$user->insert();
텍스트 기반 기본 키
텍스트 기반 기본 키(예: UUID)가 있으면, 삽입 전에 기본 키 값을 두 가지 방법 중 하나로 설정할 수 있습니다.
$user = new User($pdo_connection, [ 'primaryKey' => 'uuid' ]);
$user->uuid = 'some-uuid';
$user->name = 'demo';
$user->password = md5('demo');
$user->insert(); // 또는 $user->save();
또는 이벤트를 통해 기본 키를 자동으로 생성할 수 있습니다.
class User extends flight\ActiveRecord {
public function __construct($database_connection)
{
parent::__construct($database_connection, 'users', [ 'primaryKey' => 'uuid' ]);
// 위 배열 대신 이렇게 기본 키를 설정할 수도 있습니다.
$this->primaryKey = 'uuid';
}
protected function beforeInsert(self $self) {
$self->uuid = uniqid(); // 또는 고유 ID를 생성하는 방법에 따라
}
}
삽입 전에 기본 키를 설정하지 않으면 rowid로 설정되고 데이터베이스가 생성하지만, 테이블에 해당 필드가 없으면 지속되지 않습니다. 따라서 이벤트를 사용해 자동으로 처리하는 것이 권장됩니다.
update(): boolean|ActiveRecord
현재 레코드를 데이터베이스에 업데이트합니다.
$user->greaterThan('id', 0)->orderBy('id desc')->find();
$user->email = 'test@example.com';
$user->update();
save(): boolean|ActiveRecord
현재 레코드를 데이터베이스에 삽입하거나 업데이트합니다. 레코드에 id가 있으면 업데이트하고, 없으면 삽입합니다.
$user = new User($pdo_connection);
$user->name = 'demo';
$user->password = md5('demo');
$user->save();
참고: 클래스에 관계가 정의되어 있으면, 정의되고 인스턴스화된 관계 중 업데이트할 더티 데이터가 있는 경우 재귀적으로 저장합니다. (v0.4.0 이상)
delete(): boolean
현재 레코드를 데이터베이스에서 삭제합니다.
$user->gt('id', 0)->orderBy('id desc')->find();
$user->delete();
사전에 검색을 실행하여 여러 레코드를 삭제할 수도 있습니다.
$user->like('name', 'Bob%')->delete();
dirty(array $dirty = []): ActiveRecord
더티 데이터는 레코드에서 변경된 데이터를 가리킵니다.
$user->greaterThan('id', 0)->orderBy('id desc')->find();
// 이 시점에서 아무것도 "더티"하지 않습니다.
$user->email = 'test@example.com'; // 이제 이메일이 변경되어 "더티"로 간주됩니다.
$user->update();
// 이제 업데이트되어 데이터베이스에 지속되었으므로 더티 데이터가 없습니다
$user->password = password_hash()'newpassword'); // 이제 이것이 더티입니다
$user->dirty(); // 아무것도 전달하지 않으면 모든 더티 항목을 지웁니다.
$user->update(); // 아무것도 업데이트되지 않습니다. 더티로 캡처된 것이 없기 때문입니다.
$user->dirty([ 'name' => 'something', 'password' => password_hash('a different password') ]);
$user->update(); // 이름과 비밀번호가 모두 업데이트됩니다.
copyFrom(array $data): ActiveRecord (v0.4.0)
dirty() 메서드의 별칭입니다. 무엇을 하는지 더 명확합니다.
$user->copyFrom([ 'name' => 'something', 'password' => password_hash('a different password') ]);
$user->update(); // 이름과 비밀번호가 모두 업데이트됩니다.
isDirty(): boolean (v0.4.0)
현재 레코드가 변경되었는지 true를 반환합니다.
$user->greaterThan('id', 0)->orderBy('id desc')->find();
$user->email = 'test@email.com';
$user->isDirty(); // true
reset(bool $include_query_data = true): ActiveRecord
현재 레코드를 초기 상태로 재설정합니다. 루프 유형 동작에서 사용하기 좋습니다.
true를 전달하면 현재 객체를 찾는 데 사용된 쿼리 데이터도 재설정합니다 (기본 동작).
$users = $user->greaterThan('id', 0)->orderBy('id desc')->find();
$user_company = new UserCompany($pdo_connection);
foreach($users as $user) {
$user_company->reset(); // 깨끗한 상태로 시작
$user_company->user_id = $user->id;
$user_company->company_id = $some_company_id;
$user_company->insert();
}
getBuiltSql(): string (v0.4.1)
find(), findAll(), insert(), update(), 또는 save() 메서드를 실행한 후 생성된 SQL을 가져와 디버깅에 사용할 수 있습니다.
SQL 쿼리 메서드
select(string $field1 [, string $field2 ... ])
테이블의 일부 컬럼만 선택할 수 있습니다 (많은 컬럼이 있는 넓은 테이블에서 더 성능이 좋습니다)
$user->select('id', 'name')->find();
from(string $table)
기술적으로 다른 테이블을 선택할 수도 있습니다! 왜 안 되나요?!
$user->select('id', 'name')->from('user')->find();
join(string $table_name, string $join_condition)
데이터베이스의 다른 테이블에 조인할 수도 있습니다.
$user->join('contacts', 'contacts.user_id = users.id')->find();
where(string $where_conditions)
사용자 지정 where 인수를 설정할 수 있습니다 (이 where 문에서 params를 설정할 수 없습니다)
$user->where('id=1 AND name="demo"')->find();
보안 주의 - $user->where("id = '{$id}' AND name = '{$name}'")->find();처럼 할 수 있지만, 절대 이렇게 하지 마세요!!! 이것은 SQL 인젝션 공격에 취약합니다. 온라인에 많은 기사가 있습니다. "sql injection attacks php"를 Google하면 이 주제에 대한 많은 기사를 찾을 수 있습니다. 이 라이브러리에서 이를 처리하는 올바른 방법은 where() 메서드 대신 $user->eq('id', $id)->eq('name', $name)->find();처럼 하는 것입니다. 반드시 이렇게 해야 한다면, PDO 라이브러리의 $pdo->quote($var)를 사용해 이스케이프하세요. quote()를 사용한 후에만 where() 문에서 사용할 수 있습니다.
group(string $group_by_statement)/groupBy(string $group_by_statement)
특정 조건으로 결과를 그룹화합니다.
$user->select('COUNT(*) as count')->groupBy('name')->findAll();
order(string $order_by_statement)/orderBy(string $order_by_statement)
반환된 쿼리를 특정 방식으로 정렬합니다.
$user->orderBy('name DESC')->find();
limit(string $limit)/limit(int $offset, int $limit)
반환되는 레코드 수를 제한합니다. 두 번째 int가 주어지면 SQL처럼 offset, limit이 됩니다.
$user->orderby('name DESC')->limit(0, 10)->findAll();
WHERE 조건
equal(string $field, mixed $value) / eq(string $field, mixed $value)
Where field = $value
$user->eq('id', 1)->find();
notEqual(string $field, mixed $value) / ne(string $field, mixed $value)
Where field <> $value
$user->ne('id', 1)->find();
isNull(string $field)
Where field IS NULL
$user->isNull('id')->find();
isNotNull(string $field) / notNull(string $field)
Where field IS NOT NULL
$user->isNotNull('id')->find();
greaterThan(string $field, mixed $value) / gt(string $field, mixed $value)
Where field > $value
$user->gt('id', 1)->find();
lessThan(string $field, mixed $value) / lt(string $field, mixed $value)
Where field < $value
$user->lt('id', 1)->find();
greaterThanOrEqual(string $field, mixed $value) / ge(string $field, mixed $value) / gte(string $field, mixed $value)
Where field >= $value
$user->ge('id', 1)->find();
lessThanOrEqual(string $field, mixed $value) / le(string $field, mixed $value) / lte(string $field, mixed $value)
Where field <= $value
$user->le('id', 1)->find();
like(string $field, mixed $value) / notLike(string $field, mixed $value)
Where field LIKE $value 또는 field NOT LIKE $value
$user->like('name', 'de')->find();
in(string $field, array $values) / notIn(string $field, array $values)
Where field IN($value) 또는 field NOT IN($value)
$user->in('id', [1, 2])->find();
between(string $field, array $values)
Where field BETWEEN $value AND $value1
$user->between('id', [1, 2])->find();
OR 조건
조건을 OR 문으로 감쌀 수 있습니다. startWrap()와 endWrap() 메서드를 사용하거나 필드와 값 다음에 조건의 3번째 매개변수를 채워서 합니다.
// 방법 1
$user->eq('id', 1)->startWrap()->eq('name', 'demo')->or()->eq('name', 'test')->endWrap('OR')->find();
// 이는 `id = 1 AND (name = 'demo' OR name = 'test')`로 평가됩니다
// 방법 2
$user->eq('id', 1)->eq('name', 'demo', 'OR')->find();
// 이는 `id = 1 OR name = 'demo'`로 평가됩니다
관계
이 라이브러스를 사용해 여러 종류의 관계를 설정할 수 있습니다. 테이블 간 one->many 및 one->one 관계를 설정할 수 있습니다. 이는 클래스에서 약간의 추가 설정이 필요합니다.
$relations 배열 설정은 어렵지 않지만, 올바른 구문을 추측하는 것은 혼란스러울 수 있습니다.
protected array $relations = [
// 키 이름을 원하는 대로 지정할 수 있습니다. ActiveRecord 이름이 좋습니다. 예: user, contact, client
'user' => [
// 필수
// self::HAS_MANY, self::HAS_ONE, self::BELONGS_TO
self::HAS_ONE, // 이는 관계 유형입니다
// 필수
'Some_Class', // 이는 참조할 "다른" ActiveRecord 클래스입니다
// 필수
// 관계 유형에 따라 다름
// self::HAS_ONE = 조인 참조 외래 키
// self::HAS_MANY = 조인 참조 외래 키
// self::BELONGS_TO = 조인 참조 로컬 키
'local_or_foreign_key',
// 참고로, 이는 "다른" 모델의 기본 키에만 조인됩니다
// 선택
[ 'eq' => [ 'client_id', 5 ], 'select' => 'COUNT(*) as count', 'limit' 5 ], // 관계 조인 시 원하는 추가 조건
// $record->eq('client_id', 5)->select('COUNT(*) as count')->limit(5))
// 선택
'back_reference_name' // 이 관계를 다시 자신으로 백 참조하려면 예: $user->contact->user;
];
]
class User extends ActiveRecord{
protected array $relations = [
'contacts' => [ self::HAS_MANY, Contact::class, 'user_id' ],
'contact' => [ self::HAS_ONE, Contact::class, 'user_id' ],
];
public function __construct($database_connection)
{
parent::__construct($database_connection, 'users');
}
}
class Contact extends ActiveRecord{
protected array $relations = [
'user' => [ self::BELONGS_TO, User::class, 'user_id' ],
'user_with_backref' => [ self::BELONGS_TO, User::class, 'user_id', [], 'contact' ],
];
public function __construct($database_connection)
{
parent::__construct($database_connection, 'contacts');
}
}
이제 참조가 설정되었으므로 매우 쉽게 사용할 수 있습니다!
$user = new User($pdo_connection);
// 가장 최근 사용자 찾기.
$user->notNull('id')->orderBy('id desc')->find();
// 관계를 사용해 연락처 가져오기:
foreach($user->contacts as $contact) {
echo $contact->id;
}
// 또는 반대로 할 수 있습니다.
$contact = new Contact();
// 하나의 연락처 찾기
$contact->find();
// 관계를 사용해 사용자 가져오기:
echo $contact->user->name; // 이는 사용자 이름입니다
꽤 멋지지 않나요?
Eager Loading
개요
Eager loading은 관계를 미리 로드하여 N+1 쿼리 문제를 해결합니다. 각 레코드의 관계에 대해 별도의 쿼리를 실행하는 대신, 관계당 하나의 추가 쿼리로 모든 관련 데이터를 가져옵니다.
참고: Eager loading은 v0.7.0 이상에서만 사용할 수 있습니다.
기본 사용법
with() 메서드를 사용해 eager load할 관계를 지정하세요:
// N+1 대신 2개의 쿼리로 사용자와 연락처 로드
$users = $user->with('contacts')->findAll();
foreach ($users as $u) {
foreach ($u->contacts as $contact) {
echo $contact->email; // 추가 쿼리 없음!
}
}
다중 관계
한 번에 여러 관계 로드:
$users = $user->with(['contacts', 'profile', 'settings'])->findAll();
관계 유형
HAS_MANY
// 각 사용자에 대한 모든 연락처 eager load
$users = $user->with('contacts')->findAll();
foreach ($users as $u) {
// $u->contacts는 이미 배열로 로드됨
foreach ($u->contacts as $contact) {
echo $contact->email;
}
}
HAS_ONE
// 각 사용자에 대한 하나의 연락처 eager load
$users = $user->with('contact')->findAll();
foreach ($users as $u) {
// $u->contact는 이미 객체로 로드됨
echo $u->contact->email;
}
BELONGS_TO
// 모든 연락처에 대한 부모 사용자 eager load
$contacts = $contact->with('user')->findAll();
foreach ($contacts as $c) {
// $c->user는 이미 로드됨
echo $c->user->name;
}
find()와 함께
Eager loading은 findAll() 및 find() 와 모두 작동합니다:
$user = $user->with('contacts')->find(1);
// 사용자와 모든 연락처가 2개의 쿼리로 로드됨
성능 이점
Eager loading 없이 (N+1 문제):
$users = $user->findAll(); // 1 쿼리
foreach ($users as $u) {
$contacts = $u->contacts; // N 쿼리 (사용자당 하나!)
}
// 총: 1 + N 쿼리
Eager loading과 함께:
$users = $user->with('contacts')->findAll(); // 총 2 쿼리
foreach ($users as $u) {
$contacts = $u->contacts; // 추가 쿼리 0!
}
// 총: 2 쿼리 (사용자 1 + 모든 연락처 1)
10명의 사용자에 대해 쿼리를 11개에서 2개로 줄여 82% 감소!
중요한 주의사항
- Eager loading은 완전히 선택적입니다 - lazy loading은 이전처럼 작동합니다
- 이미 로드된 관계는 자동으로 건너뜁니다
- 백 참조는 eager loading과 작동합니다
- 관계 콜백은 eager loading 중 존중됩니다
제한사항
- 중첩 eager loading (예: with(['contacts.addresses']) )은 현재 지원되지 않습니다
- 클로저를 통한 eager load 제약은 이 버전에서 지원되지 않습니다
사용자 지정 데이터 설정
때때로 ActiveRecord에 고유한 것을 첨부해야 할 수 있습니다. 예를 들어 템플릿에 전달될 객체에 간단히 첨부할 수 있는 사용자 지정 계산입니다.
setCustomData(string $field, mixed $value)
setCustomData() 메서드로 사용자 지정 데이터를 첨부하세요.
$user->setCustomData('page_view_count', $page_view_count);
그런 다음 일반 객체 속성처럼 참조하세요.
echo $user->page_view_count;
이벤트
이 라이브러리의 또 다른 멋진 기능은 이벤트에 관한 것입니다. 이벤트는 호출하는 특정 메서드에 기반해 특정 시점에 트리거됩니다. 데이터를 자동으로 설정하는 데 매우 유용합니다.
onConstruct(ActiveRecord $ActiveRecord, array &config)
기본 연결을 설정해야 할 때 매우 유용합니다.
// index.php 또는 bootstrap.php
Flight::register('db', 'PDO', [ 'sqlite:test.db' ]);
//
//
//
// User.php
class User extends flight\ActiveRecord {
protected function onConstruct(self $self, array &$config) { // & 참조를 잊지 마세요
// 연결을 자동으로 설정하려면 이렇게 할 수 있습니다
$config['connection'] = Flight::db();
// 또는 이렇게
$self->transformAndPersistConnection(Flight::db());
// 테이블 이름도 이렇게 설정할 수 있습니다.
$config['table'] = 'users';
}
}
beforeFind(ActiveRecord $ActiveRecord)
각 쿼리 조작이 필요할 때만 유용할 것입니다.
class User extends flight\ActiveRecord {
public function __construct($database_connection)
{
parent::__construct($database_connection, 'users');
}
protected function beforeFind(self $self) {
// id >= 0을 항상 실행하려면
$self->gte('id', 0);
}
}
afterFind(ActiveRecord $ActiveRecord)
이 레코드가 가져올 때마다 항상 로직을 실행해야 할 때 더 유용할 것입니다. 무언가를 복호화해야 하나요? 매번 사용자 지정 카운트 쿼리를 실행해야 하나요 (성능이 좋지 않지만 어쨌든)?
class User extends flight\ActiveRecord {
public function __construct($database_connection)
{
parent::__construct($database_connection, 'users');
}
protected function afterFind(self $self) {
// 무언가 복호화
$self->secret = yourDecryptFunction($self->secret, $some_key);
// 쿼리처럼 사용자 지정 무언가를 저장?
$self->setCustomData('view_count', $self->select('COUNT(*) count')->from('user_views')->eq('user_id', $self->id)['count'];
}
}
beforeFindAll(ActiveRecord $ActiveRecord)
각 쿼리 조작이 필요할 때만 유용할 것입니다.
class User extends flight\ActiveRecord {
public function __construct($database_connection)
{
parent::__construct($database_connection, 'users');
}
protected function beforeFindAll(self $self) {
// id >= 0을 항상 실행하려면
$self->gte('id', 0);
}
}
afterFindAll(array<int,ActiveRecord> $results)
afterFind()와 유사하지만 모든 레코드에 적용할 수 있습니다!
class User extends flight\ActiveRecord {
public function __construct($database_connection)
{
parent::__construct($database_connection, 'users');
}
protected function afterFindAll(array $results) {
foreach($results as $self) {
// afterFind()처럼 멋진 일을 하세요
}
}
}
beforeInsert(ActiveRecord $ActiveRecord)
매번 기본 값을 설정해야 할 때 매우 유용합니다.
class User extends flight\ActiveRecord {
public function __construct($database_connection)
{
parent::__construct($database_connection, 'users');
}
protected function beforeInsert(self $self) {
// 합리적인 기본값 설정
if(!$self->created_date) {
$self->created_date = gmdate('Y-m-d');
}
if(!$self->password) {
$self->password = password_hash((string) microtime(true));
}
}
}
afterInsert(ActiveRecord $ActiveRecord)
삽입 후 데이터를 변경해야 하는 사용 사례가 있나요?
class User extends flight\ActiveRecord {
public function __construct($database_connection)
{
parent::__construct($database_connection, 'users');
}
protected function afterInsert(self $self) {
// 당신의 방식대로 하세요
Flight::cache()->set('most_recent_insert_id', $self->id);
// 또는 무엇이든....
}
}
beforeUpdate(ActiveRecord $ActiveRecord)
업데이트 시 매번 기본 값을 설정해야 할 때 매우 유용합니다.
class User extends flight\ActiveRecord {
public function __construct($database_connection)
{
parent::__construct($database_connection, 'users');
}
protected function beforeInsert(self $self) {
// 합리적인 기본값 설정
if(!$self->updated_date) {
$self->updated_date = gmdate('Y-m-d');
}
}
}
afterUpdate(ActiveRecord $ActiveRecord)
업데이트 후 데이터를 변경해야 하는 사용 사례가 있나요?
class User extends flight\ActiveRecord {
public function __construct($database_connection)
{
parent::__construct($database_connection, 'users');
}
protected function afterInsert(self $self) {
// 당신의 방식대로 하세요
Flight::cache()->set('most_recently_updated_user_id', $self->id);
// 또는 무엇이든....
}
}
beforeSave(ActiveRecord $ActiveRecord)/afterSave(ActiveRecord $ActiveRecord)
삽입 또는 업데이트 시 이벤트가 발생하기를 원할 때 유용합니다. 긴 설명은 생략하겠지만, 무엇인지 추측할 수 있을 것입니다.
class User extends flight\ActiveRecord {
public function __construct($database_connection)
{
parent::__construct($database_connection, 'users');
}
protected function beforeSave(self $self) {
$self->last_updated = gmdate('Y-m-d H:i:s');
}
}
beforeDelete(ActiveRecord $ActiveRecord)/afterDelete(ActiveRecord $ActiveRecord)
여기서 무엇을 하고 싶은지 모르겠지만, 판단하지 않습니다! 해보세요!
class User extends flight\ActiveRecord {
public function __construct($database_connection)
{
parent::__construct($database_connection, 'users');
}
protected function beforeDelete(self $self) {
echo 'He was a brave soldier... :cry-face:';
}
}
데이터베이스 연결 관리
이 라이브러리를 사용할 때 데이터베이스 연결을 여러 방식으로 설정할 수 있습니다. 생성자에서 연결을 설정하거나, $config['connection'] 설정 변수로 설정하거나 setDatabaseConnection() (v0.4.1)을 사용할 수 있습니다.
$pdo_connection = new PDO('sqlite:test.db'); // 예제용
$user = new User($pdo_connection);
// 또는
$user = new User(null, [ 'connection' => $pdo_connection ]);
// 또는
$user = new User();
$user->setDatabaseConnection($pdo_connection);
액티브 레코드를 호출할 때마다 항상 $database_connection을 설정하지 않으려면, 이를 피할 방법이 있습니다!
// index.php 또는 bootstrap.php
// Flight에서 등록된 클래스로 설정
Flight::register('db', 'PDO', [ 'sqlite:test.db' ]);
// User.php
class User extends flight\ActiveRecord {
public function __construct(array $config = [])
{
$database_connection = $config['connection'] ?? Flight::db();
parent::__construct($database_connection, 'users', $config);
}
}
// 이제 인수 불필요!
$user = new User();
참고: 단위 테스트를 계획 중이라면, 이 방식은 단위 테스트에 약간의 도전을 추가할 수 있지만,
setDatabaseConnection()또는$config['connection']으로 연결을 주입할 수 있으므로 전체적으로 나쁘지 않습니다.
데이터베이스 연결을 새로 고쳐야 할 때, 예를 들어 장기 실행 CLI 스크립트를 실행 중이고 연결을 주기적으로 새로 고쳐야 한다면, $your_record->setDatabaseConnection($pdo_connection)으로 재설정할 수 있습니다.
기여
참여해 주세요. :D
설정
기여할 때 composer test-coverage를 실행해 100% 테스트 커버리지를 유지하세요 (이것은 진짜 단위 테스트 커버리지가 아니라 통합 테스트에 가깝습니다).
또한 composer beautify와 composer phpcs를 실행해 린팅 오류를 수정하세요.
라이선스
MIT
Awesome-plugins/latte
Latte
Latte는 매우 사용하기 쉽고 Twig나 Smarty보다 PHP 문법에 더 가까운 느낌을 주는 완전한 기능을 갖춘 템플릿 엔진입니다. 또한 확장하기 쉽고 사용자 정의 필터와 함수를 추가할 수 있습니다.
설치
Composer를 사용하여 설치하세요.
composer require latte/latte
기본 구성
시작하기 위한 기본 구성 옵션이 있습니다. 이에 대해 더 자세히 읽으려면 Latte 문서를 참조하세요.
require 'vendor/autoload.php';
$app = Flight::app();
$app->map('render', function(string $template, array $data, ?string $block): void {
$latte = new Latte\Engine;
// Latte가 캐시를 저장하는 위치
$latte->setTempDirectory(__DIR__ . '/../cache/');
$finalPath = Flight::get('flight.views.path') . $template;
$latte->render($finalPath, $data, $block);
});
간단한 레이아웃 예제
다음은 레이아웃 파일의 간단한 예제입니다. 이는 다른 모든 뷰를 감싸는 데 사용될 파일입니다.
<!-- app/views/layout.latte -->
<!doctype html>
<html lang="en">
<head>
<title>{$title ? $title . ' - '}My App</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<header>
<nav>
<!-- 여기에 네비게이션 요소를 추가하세요 -->
</nav>
</header>
<div id="content">
<!-- 여기가 바로 마법입니다 -->
{block content}{/block}
</div>
<div id="footer">
© Copyright
</div>
</body>
</html>
이제 그 콘텐츠 블록 내부에 렌더링될 파일입니다:
<!-- app/views/home.latte -->
<!-- 이는 Latte에게 이 파일이 layout.latte 파일 "내부"에 있음을 알려줍니다 -->
{extends layout.latte}
<!-- 이는 레이아웃 내부 콘텐츠 블록에 렌더링될 콘텐츠입니다 -->
{block content}
<h1>Home Page</h1>
<p>Welcome to my app!</p>
{/block}
함수나 컨트롤러 내부에서 이를 렌더링할 때 다음과 같이 합니다:
// 간단한 라우트
Flight::route('/', function () {
Flight::render('home.latte', [
'title' => 'Home Page'
]);
});
// 또는 컨트롤러를 사용하는 경우
Flight::route('/', [HomeController::class, 'index']);
// HomeController.php
class HomeController
{
public function index()
{
Flight::render('home.latte', [
'title' => 'Home Page'
]);
}
}
Latte를 최대한 활용하는 방법에 대한 자세한 정보는 Latte 문서를 참조하세요!
Tracy를 사용한 디버깅
이 섹션에는 PHP 8.1+가 필요합니다.
Tracy를 사용하여 Latte 템플릿 파일을 바로 디버깅할 수도 있습니다! 이미 Tracy가 설치되어 있다면 Tracy에 Latte 확장을 추가해야 합니다.
// services.php
use Tracy\Debugger;
$app->map('render', function(string $template, array $data, ?string $block): void {
$latte = new Latte\Engine;
// Latte가 캐시를 저장하는 위치
$latte->setTempDirectory(__DIR__ . '/../cache/');
$finalPath = Flight::get('flight.views.path') . $template;
// 이는 Tracy 디버그 바로가 활성화된 경우에만 확장을 추가합니다
if (Debugger::$showBar === true) {
// 여기에 Latte 패널을 Tracy에 추가합니다
$latte->addExtension(new Latte\Bridges\Tracy\TracyExtension);
}
$latte->render($finalPath, $data, $block);
});Awesome-plugins/awesome_plugins
멋진 플러그인
Flight는 확장성이 매우 뛰어납니다. Flight 애플리케이션에 기능을 추가할 수 있는 다양한 플러그인이 있습니다. 일부는 Flight 팀이 공식적으로 지원하며, 다른 것들은 시작을 돕기 위한 마이크로/라이트 라이브러리입니다.
AI 도구
Flight는 AI 기반 플러그인으로 더욱 멋지게 만들 수 있습니다.
- Flight MCP - MCP(Model Control Protocol)를 Flight과 통합하기 위한 플러그인으로, 원활한 AI 기반 기능을 제공합니다. 주로 문서 페이지를 중심으로 Flight 프로젝트에 대한 최신 정보를 제공하여 토큰 비용을 절감하는 데 도움이 됩니다.
API 문서화
API 문서화는 모든 API에 필수적입니다. 개발자들이 API와 상호작용하는 방법과 예상 결과를 이해하는 데 도움이 됩니다. Flight 프로젝트의 API 문서를 생성하는 데 도움이 되는 몇 가지 도구가 있습니다.
- FlightPHP OpenAPI Generator - Daniel Schreiber가 작성한 블로그 포스트로, API 우선 접근 방식을 사용하여 FlightPHP와 OpenAPI 사양을 사용하여 API를 구축하는 방법을 설명합니다.
- SwaggerUI - Swagger UI는 Flight 프로젝트의 API 문서를 생성하는 데 도움이 되는 훌륭한 도구입니다. 사용하기 매우 쉽고 필요에 맞게 사용자 정의할 수 있습니다. Swagger 문서를 생성하는 데 도움이 되는 PHP 라이브러리입니다.
애플리케이션 성능 모니터링 (APM)
애플리케이션 성능 모니터링(APM)은 모든 애플리케이션에 필수적입니다. 애플리케이션이 어떻게 수행되고 있는지, 병목 현상이 어디에 있는지 이해하는 데 도움이 됩니다. Flight과 함께 사용할 수 있는 여러 APM 도구가 있습니다.
- official flightphp/apm - Flight APM은 Flight 애플리케이션을 모니터링하는 데 사용할 수 있는 간단한 APM 라이브러리입니다. 애플리케이션의 성능을 모니터링하고 병목 현상을 식별하는 데 사용할 수 있습니다.
비동기
Flight은 이미 빠른 프레임워크이지만 터보 엔진을 장착하면 모든 것이 더 재미있어집니다(그리고 도전적입니다)!
- flightphp/async - 공식 Flight 비동기 라이브러리입니다. 이 라이브러리는 애플리케이션에 비동기 처리를 추가하는 간단한 방법입니다. Swoole/Openswoole을 사용하여 비동기적으로 작업을 실행하는 간단하고 효과적인 방법을 제공합니다.
권한 부여/권한
권한 부여 및 권한은 누가 무엇에 접근할 수 있는지 제어해야 하는 모든 애플리케이션에 필수적입니다.
- official flightphp/permissions - 공식 Flight 권한 라이브러리입니다. 이 라이브러리는 사용자 및 애플리케이션 수준 권한을 애플리케이션에 추가하는 간단한 방법입니다.
인증
인증은 사용자 신원을 확인하고 API 엔드포인트를 보호해야 하는 애플리케이션에 필수적입니다.
- firebase/php-jwt - PHP용 JSON Web Token(JWT) 라이브러리입니다. Flight 애플리케이션에서 토큰 기반 인증을 구현하는 간단하고 안전한 방법입니다. 상태 비저장 API 인증, 미들웨어로 경로 보호, OAuth 스타일 권한 부여 흐름 구현에 적합합니다.
캐싱
캐싱은 애플리케이션 속도를 높이는 좋은 방법입니다. Flight과 함께 사용할 수 있는 여러 캐싱 라이브러리가 있습니다.
- official flightphp/cache - 가볍고 단순하며 독립 실행형 PHP 인파일 캐싱 클래스
CLI
CLI 애플리케이션은 애플리케이션과 상호작용하는 좋은 방법입니다. 컨트롤러를 생성하고, 모든 경로를 표시하고, 더 많은 작업을 수행하는 데 사용할 수 있습니다.
- official flightphp/runway - Runway는 Flight 애플리케이션을 관리하는 데 도움이 되는 CLI 애플리케이션입니다.
쿠키
쿠키는 클라이언트 측에 작은 데이터를 저장하는 좋은 방법입니다. 사용자 기본 설정, 애플리케이션 설정 등을 저장하는 데 사용할 수 있습니다.
- overclokk/cookie - PHP 쿠키는 쿠키를 관리하는 간단하고 효과적인 방법을 제공하는 PHP 라이브러리입니다.
디버깅
로컬 환경에서 개발할 때 디버깅은 필수적입니다. 디버깅 경험을 향상시킬 수 있는 몇 가지 플러그인이 있습니다.
- tracy/tracy - Flight과 함께 사용할 수 있는 기능이 풍부한 오류 핸들러입니다. 애플리케이션을 디버그하는 데 도움이 되는 여러 패널이 있습니다. 확장하기도 매우 쉽고 자신만의 패널을 추가할 수 있습니다.
- official flightphp/tracy-extensions - Tracy 오류 핸들러와 함께 사용되는 이 플러그인은 Flight 프로젝트의 디버깅을 위해 몇 가지 추가 패널을 추가합니다.
데이터베이스
데이터베이스는 대부분의 애플리케이션의 핵심입니다. 데이터를 저장하고 검색하는 방법입니다. 일부 데이터베이스 라이브러리는 쿼리를 작성하는 단순한 래퍼이며, 일부는 본격적인 ORM입니다.
- official flightphp/core SimplePdo - 코어의 일부인 공식 Flight PDO 헬퍼입니다. 이는
insert(),update(),delete(),transaction()과 같은 편리한 헬퍼 메서드가 있는 현대적인 래퍼로 데이터베이스 작업을 단순화합니다. 모든 결과는 유연한 배열/객체 접근을 위한 컬렉션으로 반환됩니다. ORM이 아니라 PDO로 작업하는 더 나은 방법입니다. - deprecated flightphp/core PdoWrapper - 코어의 일부인 공식 Flight PDO 래퍼(v3.18.0부터 더 이상 사용되지 않음). SimplePdo를 대신 사용하세요.
- official flightphp/active-record - 공식 Flight ActiveRecord ORM/매퍼입니다. 데이터베이스에서 데이터를 쉽게 검색하고 저장할 수 있는 훌륭한 작은 라이브러리입니다.
- byjg/php-migration - 프로젝트의 모든 데이터베이스 변경 사항을 추적하는 플러그인입니다.
- knifelemon/easy-query - 준비된 문을 위한 SQL 및 매개변수를 생성하는 경량의 유창한 SQL 쿼리 빌더입니다. SimplePdo와 잘 작동합니다.
암호화
암호화는 민감한 데이터를 저장하는 모든 애플리케이션에 필수적입니다. 데이터를 암호화하고 복호화하는 것은 그리 어렵지 않지만, 암호화 키를 적절히 저장하는 것은 어려울 수 있습니다 어려울 수 있습니다 어려울 수 있습니다. 가장 중요한 것은 암호화 키를 공개 디렉토리에 저장하거나 코드 저장소에 커밋하지 않는 것입니다.
- defuse/php-encryption - 데이터를 암호화하고 복호화하는 데 사용할 수 있는 라이브러리입니다. 데이터를 암호화하고 복호화하기 시작하기는 상당히 간단합니다.
작업 큐
작업 큐는 작업을 비동기적으로 처리하는 데 정말 도움이 됩니다. 이는 이메일 보내기, 이미지 처리, 또는 실시간으로 수행할 필요가 없는 모든 작업이 될 수 있습니다.
- n0nag0n/simple-job-queue - Simple Job Queue는 작업을 비동기적으로 처리하는 데 사용할 수 있는 라이브러리입니다. beanstalkd, MySQL/MariaDB, SQLite 및 PostgreSQL과 함께 사용할 수 있습니다.
세션
세션은 API에는 실제로 유용하지 않지만 웹 애플리케이션을 구축할 때는 상태 및 로그인 정보를 유지하는 데 세션이 필수적일 수 있습니다.
- official flightphp/session - 공식 Flight 세션 라이브러리입니다. 세션 데이터를 저장하고 검색하는 데 사용할 수 있는 간단한 세션 라이브러리입니다. PHP의 내장 세션 처리를 사용합니다.
- Ghostff/Session - PHP 세션 관리자(비차단, 플래시, 세그먼트, 세션 암호화). 선택적 세션 데이터 암호화/복호화를 위해 PHP open_ssl을 사용합니다.
템플릿 엔진
템플릿 엔진은 UI가 있는 모든 웹 애플리케이션의 핵심입니다. Flight과 함께 사용할 수 있는 여러 템플릿 엔진이 있습니다.
- deprecated flightphp/core View - 코어의 일부인 매우 기본적인 템플릿 엔진입니다. 프로젝트에 두 개 이상의 페이지가 있는 경우 사용하지 않는 것이 좋습니다.
- latte/latte - Latte는 사용하기 매우 쉽고 Twig이나 Smarty보다 PHP 구문에 더 가깝게 느껴지는 기능이 풍부한 템플릿 엔진입니다. 확장하기도 매우 쉽고 자신만의 필터와 함수를 추가할 수 있습니다.
- twig/twig - Twig은 유연하고 빠르며 안전한 템플릿 엔진(Symfony에서 사용하는 것과 동일)입니다. AI 도구와 많은 PHP 개발자들이 잘 알고 있으며, 기본적으로 출력을 자동 이스케이프하며 확장 생태계가 매우 큽니다.
- knifelemon/comment-template - CommentTemplate은 자산 컴파일, 템플릿 상속 및 변수 처리를 갖춘 강력한 PHP 템플릿 엔진입니다. 자동 CSS/JS 최소화, 캐싱, Base64 인코딩 및 선택적 Flight PHP 프레임워크 통합 기능을 제공합니다.
WordPress 통합
WordPress 프로젝트에서 Flight을 사용하고 싶으신가요? 이를 위한 편리한 플러그인이 있습니다!
- n0nag0n/wordpress-integration-for-flight-framework - 이 WordPress 플러그인을 사용하면 WordPress와 함께 Flight을 실행할 수 있습니다. Flight 프레임워크를 사용하여 WordPress 사이트에 사용자 정의 API, 마이크로서비스 또는 전체 앱을 추가하는 데 적합합니다. 두 가지 장점을 모두 원할 때 매우 유용합니다!
기여
공유하고 싶은 플러그인이 있으신가요? 목록에 추가하려면 풀 리퀘스트를 제출하세요!
Media
미디어
우리는 Flight와 관련된 인터넷상의 다양한 미디어 유형을 추적하려고 노력했습니다. Flight에 대해 더 알아보기 위해 사용할 수 있는 다양한 리소스를 아래에서 확인하세요.
기사 및 글
- Unit Testing and SOLID Principles by Brian Fenton (2015?)
- PHP Web Framework Flight by ojambo (2025)
- Define, Generate, and Implement: An API-First Approach with OpenAPI Generator and FlightPHP by Daniel Schreiber (2025)
- Best PHP Micro Frameworks for 2024 by n0nag0n (2024)
- Creating a RESTful API with Flight Framework by n0nag0n (2024)
- Building a Simple Blog with Flight Part 2 by n0nag0n (2024)
- Building a Simple Blog with Flight Part 1 by n0nag0n (2024)
- 🚀 Build a Simple CRUD API in PHP with the Flight Framework by soheil-khaledabadi (2024)
- Building a PHP Web Application with the Flight Micro-framework by Arthur C. Codex (2023)
- Best PHP Frameworks for Web Development in 2024 by Ravikiran A S (2023)
- Top 12 PHP Frameworks: A Comprehensive Guide for 2023 by marketing kbk (2023)
- 5 PHP Frameworks You've (Probably) Never Heard of by n0nag0n (2022)
- 12 top PHP frameworks for web developers to consider in 2023 by Anna Monus (2022)
- The Best PHP Microframeworks on a Cloud Server by Shahzeb Ahmed (2021)
- PHP framework: Top 15 powerful ones for your web development by AHT Tech (2020)
- Easy PHP Routing with FlightPHP by Lucas Conceição (2019)
- Trying Out New PHP Framework (Flight) by Leon (2017)
- Setting up FlightPHP to work with Backbonejs by Timothy Tocci (2015)
비디오 및 튜토리얼
- Build a Flight PHP App with MVC & MariaDB in 10 Minutes! (Beginner Friendly) by ojamboshop (2025)
- Create a REST API for IoT Devices Using PHP & FlightPHP - ESP32 API by IoT Craft Hub (2024)
- PHP Flight Framework Simple Introductory Video by n0nag0n (2024)
- Set header HTTP code in Flightphp (3 Solutions!!) by Roel Van de Paar (2024)
- PHP Flight Framework Tutorial. Super easy API Project! by n0nag0n (2022)
- Aplicación web CRUD con php y mysql y bootstrap usando flight by Devlopteca - Oscar Uh (2021)
- DevOps & SysAdmins: Lighttpd rewrite rule for Flight PHP microframework by Roel Van de Paar (2021)
- Tutorial REST API Flight PHP #PART2 INSERT TABLE Info #Code (Tagalog) by Info Singkat Official (2020)
- Tutorial REST API Flight PHP #PART1 Info #Code (Tagalog) by Info Singkat Official (2020)
- How To Create JSON REST API IN PHP - Part 2 by Codewife (2018)
- How To Create JSON REST API IN PHP - Part 1 by Codewife (2018)
- Teste Micro Frameworks PHP - Flight PHP, Lumen, Slim 3 e Laravel by Codemarket (2016)
- Tutorial 1 Flight PHP - Instalación by absagg (2014)
- Tutorial 2 Flight PHP - Route parte 1 by absagg (2014)
누락된 내용이 있나요?
우리가 놓친 당신이 작성하거나 녹음한 내용이 있나요? 이슈나 풀 리퀘스트로 알려주세요!
Examples
빠른 시작이 필요하신가요?
새로운 Flight 프로젝트를 시작하는 데 두 가지 옵션이 있습니다:
- Full Skeleton Boilerplate: 컨트롤러와 뷰가 포함된 더 완전한 예제.
- Single File Skeleton Boilerplate: 앱을 단일 간단한 파일로 실행하는 데 필요한 모든 것을 포함한 단일 파일.
커뮤니티 기여 예제:
- flightravel: Laravel 디렉토리와 PHP 도구 + GH Actions를 사용한 FlightPHP입니다.
- fleact - ReactJS 통합을 포함한 FlightPHP 스타터 키트입니다.
- flastro - Astro 통합을 포함한 FlightPHP 스타터 키트입니다.
- velt - Velt는 FlightPHP 백엔드를 사용하는 빠르고 쉬운 Svelte 스타터 템플릿입니다.
- vite-flightphp - FlightPHP와 모던 프론트엔드 (Vite + Tailwind CSS), 핫 리로드 기능 포함.
영감을 얻고 싶으신가요?
이것들은 Flight 팀에서 공식적으로 후원하지는 않지만, Flight로 구축된 자신의 프로젝트를 구조화하는 데 아이디어를 줄 수 있습니다!
- ASC REST API Spell Checker - FlightPHP와 ArPHP 라이브러리를 사용한 가벼운 REST API로 아랍어 철자 검사를 제공합니다. 이 API는 오타 감지와 수정 제안을 포함한 아랍어 텍스트 철자 검사 기능을 제공합니다.
- Eventify - Eventify는 이벤트 주최자와 참석자를 연결하는 싱글 페이지 앱입니다. PHP (FlightPHP), JavaScript, MySQL로 구축되었으며, JWT 인증, 이벤트 관리, OpenAPI를 사용한 RESTful API 문서를 특징으로 합니다.
- Ivox Car Rental - Ivox Car Rental은 PHP (FlightPHP), JavaScript, MySQL로 구축된 싱글 페이지, 모바일 친화적인 자동차 대여 웹 애플리케이션입니다. 사용자 등록, 자동차 검색, 예약을 지원하며, 관리자는 자동차, 사용자, 예약을 관리할 수 있습니다. 앱은 REST API, JWT 인증, 현대적인 대여 경험을 위한 반응형 디자인을 특징으로 합니다.
- Decay - 좀비에 관한 HTMX와 SleekDB를 사용한 Flight v3! (Demo)
- Flight Example Blog - 미들웨어, 컨트롤러, Active Record, Latte를 사용한 Flight v3.
- Flight CRUD RESTful API - Flight 프레임워크를 사용한 간단한 CRUD API 프로젝트로, 신규 사용자가 CRUD 작업과 데이터베이스 연결이 포함된 PHP 애플리케이션을 빠르게 설정할 수 있는 기본 구조를 제공합니다. 이 프로젝트는 RESTful API 개발을 위한 Flight 사용 방법을 보여주며, 초보자에게 이상적인 학습 도구이자 경험이 많은 개발자에게 유용한 스타터 키트입니다.
- Flight School Management System - Flight v3
- Paste Bin with Comments - Flight v3
- Basic Skeleton App
- Example Wiki
- The IT-Innovator PHP Framework Application
- LittleEducationalCMS (Spanish)
- Italian Yellow Pages API
- Generic Content Management System (with....very little documentation)
- A tiny php framework based on Flight and medoo.
- Example MVC Application
- Production ready Flight Boilerplate - 개발 주를 절약해주는 프로덕션 준비 인증 프레임워크. 기업급 보안 기능: 2FA/TOTP, LDAP 통합, Azure SSO, 지능형 속도 제한, 세션 지문 인식, 무차별 대입 공격 보호, 보안 분석 대시보드, 포괄적인 감사 로깅, 세밀한 역할 기반 액세스 제어.
자신의 예제를 공유하고 싶으신가요?
공유하고 싶은 프로젝트가 있으시면, 이 목록에 추가하기 위해 풀 리퀘스트를 제출해 주세요!
Install/install
설치 지침
Flight를 설치하기 전에 몇 가지 기본 사전 요구 사항이 있습니다. 즉, 다음이 필요합니다:
- 시스템에 PHP 설치
- 최고의 개발자 경험을 위해 Composer 설치
기본 설치
Composer를 사용 중이라면 다음 명령을 실행할 수 있습니다:
composer require flightphp/core
이렇게 하면 Flight 코어 파일만 시스템에 설치됩니다. 프로젝트 구조, 레이아웃, 의존성, 구성, 오토로딩 등을 직접 정의해야 합니다. 이 방법은 Flight 외에 다른 의존성이 설치되지 않도록 보장합니다.
파일을 직접 다운로드하여 웹 디렉토리에 압축을 풀 수도 있습니다.
기본 설치는 학습, 마이크로 API, 복사-붙여넣기 실험에 적합합니다. 인간과 AI 코딩 도구가 동일한 방식으로 따를 수 있는 전체 앱 레이아웃을 원한다면 아래 권장 스켈레톤을 사용하세요.
권장 설치
새 프로젝트에는 flightphp/skeleton 앱으로 시작하는 것을 적극 권장합니다. 설치는 매우 간단합니다.
composer create-project flightphp/skeleton my-project/
cd my-project/
composer start
# 선택적 샘플 DB + posts 데모
php runway migrate
이 단계는 프로젝트 구조, Composer PSR-4 오토로딩, 구성, 그리고 Tracy, Tracy Extensions, Runway 같은 도구를 설정합니다. 또한 루트 AGENTS.md 파일(그리고 app/ 아래의 범위 복사본)을 포함하여 AI 어시스턴트가 사용자와 동일한 레이아웃을 공유하도록 합니다—AI 및 개발자 경험을 참조하세요.
스켈레톤이 제공하는 것
project-root/
├── AGENTS.md # AI / 에이전트 정보 원본
├── SECURITY.md # 보안 기대 사항
├── .env.example # 비밀 / 배포 오버레이 (.env로 복사됨)
├── public/index.php # 웹 진입점 전용
├── app/
│ ├── config/ # bootstrap, routes, services, config_sample.php
│ ├── Controller/ # App\Controller\* (폴더명 파스칼 표기법!)
│ ├── Middleware/ # App\Middleware\*
│ ├── Model/ # App\Model\* (ActiveRecord)
│ ├── Utils/ # Config, Env, DatabaseFactory
│ ├── commands/ # Runway CLI 명령
│ ├── views/ # Twig 템플릿 (*.twig)
│ ├── cache/
│ └── log/
├── migrations/ # SQL 마이그레이션 (.sql / .mysql.sql)
└── tests/ # PHPUnit
네임스페이스는 폴더 대소문자를 따릅니다. Composer는 "App\\": "app/"로 매핑하므로:
| 디스크 경로 | 네임스페이스 |
|---|---|
app/Controller/HomeController.php |
App\Controller\HomeController |
app/Middleware/… |
App\Middleware\… |
app/Model/… |
App\Model\… |
app/Utils/… |
App\Utils\… |
Linux에서 app/controller/는 app/Controller/와 같지 않습니다. 오토로딩은 대소문자를 구분하므로 스켈레톤의 PascalCase 폴더를 그대로 따르세요. 자세한 내용: 오토로딩.
스택 기본값(새 프로젝트): Twig 뷰, SimplePdo + ActiveRecord, Engine 주입을 사용하는 Dice(앱 클래스 내부에서 Flight:: 사용을 지양), php runway migrate 후 선택적 SQLite.
create-project는 일반적으로 app/config/config_sample.php → config.php로, .env.example → .env로 복사합니다(있는 경우). 라우트는 app/config/routes.php에 있고, 서비스와 DI는 app/config/services.php에 있습니다.
문서 ↔ 스켈레톤: 이 문서는 Flight API를 가르칩니다(종종 짧은
Flight::예제 사용). 스켈레톤은 애플리케이션 구조를 고정합니다.app/아래에 코드를 추가할 때는 스켈레톤 트리를 따르고, 메서드 이름, 옵션, 플러그인에 대해서는 문서를 사용하세요.
웹 서버 구성
PHP 내장 개발 서버
이것이 가장 간단한 실행 방법입니다. 내장 서버를 사용하여 애플리케이션을 실행하고 SQLite를 데이터베이스로 사용할 수도 있습니다(sqlite3가 시스템에 설치되어 있기만 하면). 거의 아무것도 필요하지 않습니다! PHP가 설치되면 다음 명령을 실행하세요:
php -S localhost:8000
# 또는 스켈레톤 앱 사용 시
composer start
그런 다음 브라우저를 열고 http://localhost:8000으로 이동하세요.
프로젝트의 문서 루트를 다른 디렉토리로 지정하려면(예: 프로젝트가 ~/myproject이고 문서 루트가 ~/myproject/public/인 경우) ~/myproject 디렉토리에서 다음 명령을 실행할 수 있습니다:
php -S localhost:8000 -t public/
# 스켈레톤 앱에서는 이미 구성되어 있음
composer start
그런 다음 브라우저를 열고 http://localhost:8000으로 이동하세요.
Apache
시스템에 Apache가 이미 설치되어 있는지 확인하세요. 없다면 시스템에 Apache를 설치하는 방법을 검색하세요.
Apache의 경우 .htaccess 파일을 다음과 같이 편집하세요:
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ index.php [QSA,L]
참고: 하위 디렉토리에서 Flight를 사용해야 하는 경우
RewriteEngine On바로 다음에RewriteBase /subdir/줄을 추가하세요.
참고: DB나 env 파일과 같은 모든 서버 파일을 보호하려면
.htaccess파일에 다음을 넣으세요:
RewriteEngine On
RewriteRule ^(.*)$ index.php
Nginx
시스템에 Nginx가 이미 설치되어 있는지 확인하세요. 없다면 시스템에 Nginx를 설치하는 방법을 검색하세요.
Nginx의 경우 서버 선언에 다음을 추가하세요:
server {
location / {
try_files $uri $uri/ /index.php;
}
}
index.php 파일 만들기
기본 설치를 진행하는 경우 시작할 수 있는 몇 가지 코드가 필요합니다.
<?php
// Composer를 사용한다면 오토로더를 불러오세요.
require 'vendor/autoload.php';
// Composer를 사용하지 않는다면 프레임워크를 직접 불러오세요.
// require 'flight/Flight.php';
// 그런 다음 라우트를 정의하고 요청을 처리할 함수를 할당하세요.
Flight::route('/', function () {
echo 'hello world!';
});
// 마지막으로 프레임워크를 시작합니다.
Flight::start();
스켈레톤 앱에서는 공개 진입점이 앱을 부팅만 합니다. 라우트는 app/config/routes.php에 등록됩니다(일반적으로 [App\Controller\…::class, 'method'] 형태로 Dice가 의존성을 주입할 수 있게 함). 서비스, Twig, SimplePdo, 컨테이너는 app/config/services.php에서 연결됩니다. 이 구조는 AI 도구와 인간이 항상 동일한 위치를 편집하도록 의도된 것입니다.
PHP 설치
시스템에 이미 php가 설치되어 있다면 이 지침을 건너뛰고 다운로드 섹션으로 이동하세요.
macOS
Homebrew로 PHP 설치
-
Homebrew 설치(아직 설치되지 않은 경우):
- 터미널을 열고 실행:
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
- 터미널을 열고 실행:
-
PHP 설치:
- 최신 버전 설치:
brew install php - 특정 버전(예: PHP 8.1)을 설치하려면:
brew tap shivammathur/php brew install shivammathur/php/php@8.1
- 최신 버전 설치:
-
PHP 버전 간 전환:
- 현재 버전을 unlink하고 원하는 버전을 link:
brew unlink php brew link --overwrite --force php@8.1 - 설치된 버전 확인:
php -v
- 현재 버전을 unlink하고 원하는 버전을 link:
Windows 10/11
PHP 수동 설치
-
PHP 다운로드:
- PHP for Windows를 방문하여 최신 또는 특정 버전(예: 7.4, 8.0)을 non-thread-safe zip 파일로 다운로드하세요.
-
PHP 압축 해제:
- 다운로드한 zip 파일을
C:\php에 압축 해제하세요.
- 다운로드한 zip 파일을
-
시스템 PATH에 PHP 추가:
- 시스템 속성 > 환경 변수로 이동하세요.
- 시스템 변수에서 Path를 찾아 편집을 클릭하세요.
C:\php(또는 PHP를 압축 해제한 위치) 경로를 추가하세요.- 확인을 클릭하여 모든 창을 닫으세요.
-
PHP 구성:
php.ini-development를php.ini로 복사하세요.- 필요에 따라
php.ini를 편집하여 PHP를 구성하세요(예:extension_dir설정, 확장 기능 활성화).
-
PHP 설치 확인:
- 명령 프롬프트를 열고 실행:
php -v
- 명령 프롬프트를 열고 실행:
여러 PHP 버전 설치
-
각 버전에 대해 위 단계를 반복하고 각각 별도의 디렉토리(예:
C:\php7,C:\php8)에 배치하세요. -
시스템 PATH 변수를 원하는 버전 디렉토리를 가리키도록 조정하여 버전 간 전환하세요.
Ubuntu (20.04, 22.04 등)
apt로 PHP 설치
-
패키지 목록 업데이트:
- 터미널을 열고 실행:
sudo apt update
- 터미널을 열고 실행:
-
PHP 설치:
- 최신 PHP 버전 설치:
sudo apt install php - 특정 버전(예: PHP 8.1)을 설치하려면:
sudo apt install php8.1
- 최신 PHP 버전 설치:
-
추가 모듈 설치(선택 사항):
- 예를 들어 MySQL 지원을 설치하려면:
sudo apt install php8.1-mysql
- 예를 들어 MySQL 지원을 설치하려면:
-
PHP 버전 간 전환:
update-alternatives사용:sudo update-alternatives --set php /usr/bin/php8.1
-
설치된 버전 확인:
- 실행:
php -v
- 실행:
Rocky Linux
yum/dnf로 PHP 설치
-
EPEL 저장소 활성화:
- 터미널을 열고 실행:
sudo dnf install epel-release
- 터미널을 열고 실행:
-
Remi 저장소 설치:
- 실행:
sudo dnf install https://rpms.remirepo.net/enterprise/remi-release-8.rpm sudo dnf module reset php
- 실행:
-
PHP 설치:
- 기본 버전 설치:
sudo dnf install php - 특정 버전(예: PHP 7.4)을 설치하려면:
sudo dnf module install php:remi-7.4
- 기본 버전 설치:
-
PHP 버전 간 전환:
dnf모듈 명령 사용:sudo dnf module reset php sudo dnf module enable php:remi-8.0 sudo dnf install php
-
설치된 버전 확인:
- 실행:
php -v
- 실행:
일반 참고 사항
- 개발 환경에서는 프로젝트 요구 사항에 맞게 PHP 설정을 구성하는 것이 중요합니다.
- PHP 버전을 전환할 때는 사용하려는 특정 버전에 맞는 모든 관련 PHP 확장 기능이 설치되어 있는지 확인하세요.
- PHP 버전을 전환하거나 구성을 업데이트한 후에는 웹 서버(Apache, Nginx 등)를 다시 시작하여 변경 사항을 적용하세요.
Guides
가이드
Flight PHP는 간단하면서도 강력하게 설계되었으며, 우리 가이드는 실세계 애플리케이션을 단계적으로 구축하는 데 도움이 됩니다. 이러한 실용적인 튜토리얼은 완전한 프로젝트를 통해 Flight를 효과적으로 사용할 수 있는 방법을 보여줍니다.
공식 가이드
블로그 빌딩
Flight PHP로 기능적인 블로그 애플리케이션을 만드는 방법을 배웁니다. 이 가이드는 다음을 안내합니다:
- 프로젝트 구조 설정
- Latte를 사용하여 템플릿 작업
- 게시물에 대한 라우트 구현
- 데이터 저장 및 검색
- 폼 제출 처리
- 기본 오류 처리
이 튜토리얼은 모든 부분이 실제 애플리케이션에서 어떻게 연결되는지 보고 싶은 초보자에게 적합합니다.
Unit Testing and SOLID Principles
이 가이드에서는 Flight PHP 애플리케이션의 단위 테스팅 기본을 다룹니다. 포함 내용:
- PHPUnit 설정
- SOLID 원리를 사용하여 테스트 가능한 코드 작성
- 종속성 mocking
- 피해야 할 일반적인 함정
- 애플리케이션이 성장함에 따라 테스트 확대 이 튜토리얼은 코드 품질과 유지 보수성을 향상시키고 싶은 개발자에게 이상적입니다.
비공식 가이드
이 가이드들은 Flight 팀에 의해 공식적으로 유지되지 않지만, 커뮤니티에서 만들어진 귀중한 자원입니다. 다양한 주제와 용례를 다루며, Flight PHP 사용에 대한 추가 통찰을 제공합니다.
Flight Framework를 사용한 RESTful API 생성
이 가이드는 Flight PHP 프레임워크를 사용하여 RESTful API를 생성하는 과정을 안내합니다. API 설정 기본, 라우트 정의, JSON 응답 반환을 다룹니다.
간단한 블로그 빌딩
이 가이드는 Flight PHP 프레임워크를 사용하여 기본 블로그를 만드는 과정을 안내합니다. 실제로 2개의 부분으로 구성되어 있습니다: 하나는 기본을 다루고, 다른 하나는 프로덕션 준비 블로그를 위한 고급 주제와 개선을 다룹니다.
- 간단한 블로그 빌딩 with Flight - Part 1 - 간단한 블로그 시작.
- 간단한 블로그 빌딩 with Flight - Part 2 - 프로덕션용 블로그 개선.
PHP로 Pokémon API 빌딩: 초보자 가이드
이 재미있는 가이드는 Flight PHP를 사용하여 간단한 Pokémon API를 만드는 과정을 안내합니다. API 설정 기본, 라우트 정의, JSON 응답 반환을 다룹니다.
기여
가이드 아이디어가 있나요? 실수를 발견했나요? 기여를 환영합니다! 우리 가이드는 FlightPHP 문서 저장소에서 유지됩니다.
Flight로 흥미로운 것을 만들었고, 가이드로 공유하고 싶다면 풀 리퀘스트를 제출하세요. 지식을 공유하면 Flight 커뮤니티가 성장합니다.
API 문서 찾기?
Flight의 핵심 기능과 메서드에 대한 구체적인 정보를 찾고 있다면, 우리 문서의 Learn 섹션을 확인하세요.