레거시 PHP 프로젝트용 REST API 빌드
게시 됨: 2022-03-11REST API를 빌드하거나 설계하는 것은 쉬운 일이 아닙니다. 특히 레거시 PHP 프로젝트에서 수행해야 하는 경우에는 더욱 그렇습니다. 오늘날 REST API를 쉽게 구현할 수 있는 타사 라이브러리가 많이 있지만 기존 레거시 코드베이스에 통합하는 것은 다소 어려울 수 있습니다. 그리고 Laravel 및 Symfony와 같은 최신 프레임워크로 작업할 수 있는 여유가 항상 있는 것은 아닙니다. 레거시 PHP 프로젝트를 사용하면 이전 버전의 PHP에서 실행되는 더 이상 사용되지 않는 사내 프레임워크의 중간 어딘가에 있는 자신을 종종 발견할 수 있습니다.
이 기사에서는 REST API를 처음부터 구현하려는 몇 가지 일반적인 문제, 이러한 문제를 해결하는 몇 가지 방법 및 레거시 프로젝트를 위한 사용자 지정 PHP 기반 API 서버를 구축하기 위한 전반적인 전략을 살펴보겠습니다.
이 기사는 PHP 5.3 이상을 기반으로 하지만 핵심 개념은 버전 5.0 이후의 모든 PHP 버전에 유효하며 PHP가 아닌 프로젝트에도 적용될 수 있습니다. 여기에서는 REST API가 무엇인지 일반적으로 다루지 않을 것이므로 익숙하지 않은 경우 먼저 읽어보십시오.
쉽게 따라할 수 있도록 이 기사 전체에서 사용되는 몇 가지 용어와 그 의미의 목록이 있습니다.
- API 서버: API를 제공하는 기본 REST 애플리케이션(이 경우 PHP로 작성됨).
- API 끝점: 클라이언트가 작업을 수행하고 결과를 생성하기 위해 통신하는 백엔드 "메서드"입니다.
- API 엔드포인트 URL: 백엔드 시스템이 전 세계에 액세스할 수 있는 URL입니다.
- API 토큰: 사용자를 식별할 수 있는 HTTP 헤더 또는 쿠키를 통해 전달되는 고유 식별자입니다.
- 앱: API 엔드포인트를 통해 REST 애플리케이션과 통신할 클라이언트 애플리케이션. 이 기사에서는 웹 기반(데스크톱 또는 모바일)이며 JavaScript로 작성되었다고 가정합니다.
초기 단계
경로 패턴
가장 먼저 결정해야 할 사항 중 하나는 API 엔드포인트를 사용할 수 있는 URL 경로입니다. 2가지 인기 있는 방법이 있습니다.
- api.example.com과 같은 새 하위 도메인을 만듭니다.
- example.com/api와 같은 경로를 만듭니다.
언뜻보기에 첫 번째 변형이 더 인기 있고 매력적으로 보일 수 있습니다. 그러나 실제로 프로젝트별 API를 빌드하는 경우 두 번째 변형을 선택하는 것이 더 적절할 수 있습니다.
두 번째 접근 방식을 취하는 가장 중요한 이유 중 하나는 쿠키를 자격 증명을 전송하는 수단으로 사용할 수 있다는 것입니다. 브라우저 기반 클라이언트는 XHR 요청 내에서 자동으로 적절한 쿠키를 보내므로 추가 인증 헤더가 필요하지 않습니다.
또 다른 중요한 이유는 사용자 지정 헤더가 일부 프록시 서버에 의해 제거될 수 있는 하위 도메인 구성 또는 관리 문제와 관련하여 아무 것도 할 필요가 없다는 것입니다. 이것은 레거시 프로젝트에서 지루한 시련이 될 수 있습니다.
REST 요청은 상태 비저장이어야 하므로 쿠키를 사용하는 것은 "unRESTful" 관행으로 간주될 수 있습니다. 이 경우 사용자 지정 헤더를 통해 전달하는 대신 쿠키에서 타협하고 토큰 값을 전달할 수 있습니다. 사실상 우리는 session_id 대신 토큰 값을 직접 전달하는 방법으로 쿠키를 사용하고 있습니다. 이 접근 방식은 상태 비저장으로 간주될 수 있지만 사용자의 기본 설정에 따라 둘 수 있습니다.
API 끝점 URL의 버전도 관리할 수 있습니다. 또한 경로 이름의 확장자로 예상 응답 형식을 포함할 수 있습니다. 이것이 중요하지는 않지만 특히 API 개발의 초기 단계에서 장기적으로 이러한 세부 사항은 확실히 성과를 거둘 수 있습니다. 특히 새로운 기능을 구현해야 할 때. 클라이언트가 예상하는 버전을 확인하고 이전 버전과의 호환성에 필요한 형식을 제공하는 것이 최상의 솔루션이 될 수 있습니다.
API 엔드포인트 URL 구조는 다음과 같을 수 있습니다.
example.com/api/${version_code}/${actual_request_path}.${format}
그리고 실제 예:
example.com/api/v1.0/records.json
라우팅
API 엔드포인트에 대한 기본 URL을 선택한 후 다음으로 해야 할 일은 라우팅 시스템에 대해 생각하는 것입니다. 기존 프레임워크에 통합할 수 있지만 너무 번거로운 경우 문서 루트에 "api"라는 폴더를 만드는 것이 잠재적인 해결 방법입니다. 그렇게 하면 API가 완전히 별도의 논리를 가질 수 있습니다. 다음과 같이 API 로직을 자체 파일에 배치하여 이 접근 방식을 확장할 수 있습니다.
"www/api/Apis/Users.php"를 특정 API 엔드포인트에 대한 별도의 "컨트롤러"로 생각할 수 있습니다. 데이터베이스와 통신하기 위해 프로젝트에 이미 구현된 모델을 재사용하는 것과 같이 기존 코드베이스에서 구현을 재사용하는 것이 좋습니다.
마지막으로 "/api/*"에서 들어오는 모든 요청이 "/api/index.php"를 가리키도록 합니다. 이것은 웹 서버 구성을 변경하여 수행할 수 있습니다.
API 클래스
버전 및 형식
API 엔드포인트가 허용하는 버전과 형식과 기본 버전을 항상 명확하게 정의해야 합니다. 이를 통해 이전 기능을 유지하면서 미래에 새로운 기능을 구축할 수 있습니다. API 버전은 기본적으로 문자열일 수 있지만 더 나은 이해와 비교를 위해 숫자 값을 사용할 수 있습니다. 몇 가지 사항만 다르다는 것을 분명히 나타내기 때문에 부 버전에 대한 여분의 숫자를 갖는 것이 좋습니다.
- v1.0은 첫 번째 버전을 의미합니다.
- v1.1 첫 번째 버전에 약간의 변경 사항이 있습니다.
- v2.0은 완전히 새로운 버전이 될 것입니다.
형식은 JSON, XML 및 CSV를 포함하되 이에 국한되지 않는 클라이언트가 필요로 하는 모든 것이 될 수 있습니다. URL을 통해 파일 확장자로 제공함으로써 API 끝점 url은 가독성을 보장하고 API 소비자가 예상할 수 있는 형식을 쉽게 알 수 있습니다.
- "/api/v1.0/records.json"은 레코드의 JSON 배열을 반환합니다.
- "/api/v1.0/records.xml"은 레코드의 XML 파일을 반환합니다.
이러한 각 형식에 대한 응답에서 적절한 Content-Type 헤더도 보내야 한다는 점을 지적할 가치가 있습니다.
들어오는 요청을 받으면 가장 먼저 해야 할 일 중 하나는 API 서버가 요청한 버전과 형식을 지원하는지 확인하는 것입니다. 들어오는 요청을 처리하는 기본 메서드에서 $_SERVER['PATH_INFO'] 또는 $_SERVER['REQUEST_URI'] 를 구문 분석하여 요청된 형식과 버전이 지원되는지 확인합니다. 그런 다음 계속하거나 4xx 응답(예: 406 "Not Acceptable")을 반환합니다. 여기서 가장 중요한 부분은 항상 클라이언트가 기대하는 것을 반환하는 것입니다. 이에 대한 대안은 URL 경로 확장 대신 요청 헤더 "수락"을 확인하는 것입니다.
허용된 경로
모든 것을 API 컨트롤러에 투명하게 전달할 수 있지만 허용된 경로의 화이트리스트 세트를 사용하는 것이 더 나을 수 있습니다. 이렇게 하면 유연성이 약간 줄어들지만 다음에 코드로 돌아갈 때 API 끝점 URL이 어떻게 보이는지에 대한 매우 명확한 통찰력을 제공합니다.
private $public_routes = array( 'system' => array( 'regex' => 'system', ), 'records' => array( 'regex' => 'records(?:/?([0-9]+)?)', ), );
이것들을 별도의 파일로 이동하여 더 깔끔하게 만들 수도 있습니다. 위의 구성은 다음 URL에 대한 요청을 활성화하는 데 사용됩니다.
/api/v1.0/system.json /api/v1.0/records.json /api/v1.0/records/7.json
PUT 데이터 처리
PHP는 들어오는 POST 데이터를 자동으로 처리하고 $_POST 슈퍼글로벌 아래에 배치합니다. 그러나 PUT 요청의 경우에는 그렇지 않습니다. 모든 데이터는 php://input 에 "묻혀 있습니다". 실제 API 메서드를 호출하기 전에 별도의 구조나 배열로 구문 분석하는 것을 잊지 마십시오. 간단한 parse_str 이면 충분할 수 있지만 클라이언트가 다중 부분 요청을 보내는 경우 양식 경계를 처리하기 위해 추가 구문 분석이 필요할 수 있습니다. 멀티파트 요청의 일반적인 사용 사례에는 파일 업로드가 포함됩니다. 멀티파트 요청 감지 및 처리는 다음과 같이 수행할 수 있습니다.
self::$input = file_get_contents('php://input'); // For PUT/DELETE there is input data instead of request variables if (!empty(self::$input)) { preg_match('/boundary=(.*)$/', $_SERVER['CONTENT_TYPE'], $matches); if (isset($matches[1]) && strpos(self::$input, $matches[1]) !== false) { $this->parse_raw_request(self::$input, self::$input_data); } else { parse_str(self::$input, self::$input_data); } }
여기에서 parse_raw_request 는 다음과 같이 구현할 수 있습니다.
/** * Helper method to parse raw requests */ private function parse_raw_request($input, &$a_data) { // grab multipart boundary from content type header preg_match('/boundary=(.*)$/', $_SERVER['CONTENT_TYPE'], $matches); $boundary = $matches[1]; // split content by boundary and get rid of last -- element $a_blocks = preg_split("/-+$boundary/", $input); array_pop($a_blocks); // loop data blocks foreach ($a_blocks as $id => $block) { if (empty($block)) { continue; } // parse uploaded files if (strpos($block, 'application/octet-stream') !== false) { // match "name", then everything after "stream" (optional) except for prepending newlines preg_match("/name=\"([^\"]*)\".*stream[\n|\r]+([^\n\r].*)?$/s", $block, $matches); // parse all other fields } else { // match "name" and optional value in between newline sequences preg_match('/name=\"([^\"]*)\"[\n|\r]+([^\n\r].*)?\r$/s', $block, $matches); } $a_data[$matches[1]] = $matches[2]; } }
이를 통해 Api::$input 에서 원시 입력으로, Api::$input_data 에서 연관 배열로 필요한 요청 페이로드를 가질 수 있습니다.

가짜 PUT/DELETE
때때로 서버가 표준 GET/POST HTTP 메소드 외에 아무것도 지원하지 않는 상황에 처한 자신을 볼 수 있습니다. 이 문제에 대한 일반적인 솔루션은 PUT/DELETE 또는 기타 사용자 지정 요청 방법을 "가짜"하는 것입니다. 이를 위해 "_method"와 같은 "magic" 매개변수를 사용할 수 있습니다. $_REQUEST 배열에 있는 경우 요청이 지정된 유형이라고 가정하기만 하면 됩니다. Laravel과 같은 최신 프레임워크에는 이러한 기능이 내장되어 있습니다. 서버 또는 클라이언트에 제한이 있는 경우(예: PUT 요청을 허용하지 않는 회사 프록시 뒤에서 작업의 Wi-Fi 네트워크를 사용하는 사람) 뛰어난 호환성을 제공합니다.
특정 API로 전달
기존 프로젝트 자동 로더를 재사용할 여유가 없다면 spl_autoload_register 함수를 사용하여 직접 생성할 수 있습니다. "api/index.php" 페이지에서 정의하고 "api/Api.php"에 있는 API 클래스를 호출합니다. API 클래스는 미들웨어 역할을 하며 실제 메소드를 호출합니다. 예를 들어 "/api/v1.0/records/7.json"에 대한 요청은 매개변수 7을 사용하여 "Apis/Records.php" GET 메서드를 호출하는 것으로 끝나야 합니다. 이렇게 하면 우려 사항을 분리하고 로직 클리너. 물론 이것을 사용 중인 프레임워크에 더 깊이 통합하고 특정 컨트롤러나 경로를 재사용할 수 있다면 그 가능성도 고려해야 합니다.
기본 자동 로더가 있는 "api/index.php"의 예:
<?php // Let's define very primitive autoloader spl_autoload_register(function($classname){ $classname = str_replace('Api_', 'Apis/', $classname); if (file_exists(__DIR__.'/'.$classname.'.php')) { require __DIR__.'/'.$classname.'.php'; } }); // Our main method to handle request Api::serve();
그러면 Api 클래스가 로드되고 기본 프로젝트와 독립적으로 제공되기 시작합니다.
OPTIONS 요청
클라이언트가 사용자 정의 헤더를 사용하여 고유 토큰을 전달할 때 브라우저는 서버가 해당 헤더를 지원할 때마다 먼저 확인해야 합니다. 그것이 OPTIONS 요청이 들어오는 곳입니다. 그 목적은 클라이언트와 API 서버 모두에 대해 모든 것이 정상이고 안전한지 확인하는 것입니다. 따라서 클라이언트가 무엇이든 시도할 때마다 OPTIONS 요청이 실행될 수 있습니다. 그러나 클라이언트가 자격 증명에 쿠키를 사용할 때 브라우저는 이 추가 OPTIONS 요청을 보내지 않아도 됩니다.
클라이언트가 쿠키를 사용하여 POST /users/8.json을 요청하는 경우 요청은 매우 표준적입니다.
- 앱은 /users/8.json에 대한 POST 요청을 수행합니다.
- 브라우저는 요청을 수행하고 응답을 받습니다.
그러나 사용자 지정 권한 부여 또는 토큰 헤더 사용:
- 앱은 /users/8.json에 대한 POST 요청을 수행합니다.
- 브라우저는 요청 처리를 중지하고 대신 OPTIONS 요청을 시작합니다.
- OPTIONS 요청은 /users/8.json으로 전송됩니다.
- 브라우저는 API에서 정의한 대로 사용 가능한 모든 메서드 및 헤더 목록과 함께 응답을 받습니다.
- 브라우저는 사용자 정의 헤더가 사용 가능한 헤더 목록에 있는 경우에만 원래 POST 요청을 계속합니다.
그러나 쿠키를 사용하는 경우에도 PUT/DELETE를 사용하면 추가 OPTIONS 요청을 계속 받을 수 있습니다. 따라서 이에 대응할 준비를 하십시오.
레코드 API
기본 구조
예제 Records API는 매우 간단합니다. 여기에는 모든 요청 메서드가 포함되고 동일한 기본 API 클래스로 출력을 반환합니다. 예를 들어:
<?php class Api_Records { public function __construct() { // In here you could initialize some shared logic between this API and rest of the project } /** * Get individual record or records list */ public function get($id = null) { if ($id) { return $this->getRecord(intval($id)); } else { return $this->getRecords(); } } /** * Update record */ public function put($record_id = null) { // In real world there would be call to model with validation and probably token checking // Use Api::$input_data to update return Api::responseOk('OK', array()); } // ...
따라서 각 HTTP 메소드를 정의하면 REST 스타일로 API를 더 쉽게 빌드할 수 있습니다.
출력 형식 지정
데이터베이스에서 클라이언트로 받은 모든 것에 순진하게 응답하는 것은 치명적인 결과를 초래할 수 있습니다. 우발적인 데이터 노출을 방지하기 위해 화이트리스트에 있는 키만 반환하는 특정 형식 메서드를 만듭니다.
화이트리스트 키의 또 다른 이점은 이를 기반으로 문서를 작성하고 모든 유형 검사를 수행할 수 있다는 것입니다. 예를 들어, user_id는 항상 정수이고, is_banned 플래그는 항상 부울 true 또는 false이며, 날짜 시간에는 하나의 표준이 있습니다. 응답 형식.
결과 출력
헤더
헤더 출력을 위한 별도의 방법은 브라우저에 전송된 모든 것이 올바른지 확인합니다. 이 방법은 사용자 지정 인증 헤더를 받을 가능성을 계속 유지하면서 동일한 도메인을 통해 API에 액세스할 수 있도록 하는 이점을 사용할 수 있습니다. HTTP_ORIGIN 및 HTTP_REFERER 서버 헤더를 사용하여 동일한 도메인 또는 타사 도메인 중에서 선택할 수 있습니다. 앱이 클라이언트가 x-authorization(또는 다른 사용자 정의 헤더)을 사용하고 있음을 감지하는 경우 모든 출처에서 액세스를 허용해야 하고 사용자 정의 헤더를 허용해야 합니다. 따라서 다음과 같이 보일 수 있습니다.
header('Access-Control-Allow-Origin: *'); header('Access-Control-Expose-Headers: x-authorization'); header('Access-Control-Allow-Headers: origin, content-type, accept, x-authorization'); header('X-Authorization: '.YOUR_TOKEN_HERE);
그러나 클라이언트가 쿠키 기반 자격 증명을 사용하는 경우 헤더가 약간 다를 수 있으므로 자격 증명에 대해 요청된 호스트 및 쿠키 관련 헤더만 허용합니다.
header('Access-Control-Allow-Origin: '.$origin); header('Access-Control-Expose-Headers: set-cookie, cookie'); header('Access-Control-Allow-Headers: origin, content-type, accept, set-cookie, cookie'); // Allow cookie credentials because we're on the same domain header('Access-Control-Allow-Credentials: true'); if (strtolower($_SERVER['REQUEST_METHOD']) != 'options') { setcookie(TOKEN_COOKIE_NAME, YOUR_TOKEN_HERE, time()+86400*30, '/', '.'.$_SERVER['HTTP_HOST']); }
OPTIONS 요청은 쿠키를 지원하지 않으므로 앱에서 쿠키와 함께 쿠키를 보내지 않습니다. 그리고 마지막으로 이렇게 하면 원하는 모든 HTTP 메서드에 액세스 제어가 만료됩니다.
header('Access-Control-Allow-Methods: POST, GET, OPTIONS, PUT, DELETE'); header('Access-Control-Max-Age: 86400');
신체
본문 자체는 성공 시 2xx HTTP 상태, 클라이언트로 인한 실패 시 4xx 상태, 서버로 인한 실패 시 5xx 상태로 클라이언트가 요청한 형식의 응답을 포함해야 합니다. 응답의 구조는 다양할 수 있지만 "상태" 및 "응답" 필드를 지정하는 것도 도움이 될 수 있습니다. 예를 들어 클라이언트가 새 사용자를 등록하려고 하고 사용자 이름이 이미 사용 중인 경우 HTTP 상태 200인 응답을 보낼 수 있지만 본문에는 다음과 같은 JSON이 있습니다.
{“status”: “ERROR”, “response”: ”username already taken”}
… HTTP 4xx 오류 대신 직접.
결론
정확히 같은 프로젝트는 없습니다. 이 기사에서 설명하는 전략은 귀하의 사례에 적합할 수도 있고 적합하지 않을 수도 있지만 핵심 개념은 유사해야 합니다. 모든 페이지에 최신 트렌드 또는 최신 프레임워크가 있을 수는 없으며 "내 REST Symfony 번들이 여기에서 작동하지 않는 이유"에 대한 분노가 유용한 무언가를 구축하기 위한 동기로 바뀔 수 있습니다. 작동하는 것. 최종 결과는 항상 일부 사용자 지정 및 프로젝트별 구현이기 때문에 반짝이지 않을 수 있지만 결국 솔루션은 실제로 작동하는 것이 될 것입니다. 그리고 이와 같은 시나리오에서는 모든 API 개발자의 목표가 되어야 합니다.
여기에서 논의된 개념의 구현 예는 편의를 위해 GitHub 리포지토리에 업로드되었습니다. 이러한 샘플 코드를 프로덕션에서 그대로 사용하고 싶지 않을 수 있지만 이는 다음 레거시 PHP API 통합 프로젝트의 시작점으로 쉽게 작동할 수 있습니다.
최근에 일부 레거시 프로젝트에 대해 REST API 서버를 구현해야 했습니까? 아래 의견 섹션에서 경험을 공유하십시오.