PSR-7

Programming/PHP 2016. 2. 24. 23:19

PSR-7 : HTTP 메시지 인터페이스 (http://www.php-fig.org/psr/psr-7/)


웹브라우저와 cURL 같은 HTTP 클라이언트는 HTTP 응답 메시지를 제공하는 웹 서버로 보낼 HTTP 요청 메시지를 생성한다.

서버 사이드 코드는 요청 메시지를 받고 HTTP 응답을 반환한다.

모든 HTTP 요청 메시지는 특정한 형태로 되어 있다.


1
2
3
4
POST /path HTTP/1.1
Host: example.com
 
foo=bar&baz=bat
cs


첫번째 줄은 HTTP 요청 메소드, 요청 타겟(서버 경로나 절대주소), HTTP 프로토콜 버전 순으로 구성된다.

다음에 하나 이상의 HTTP 헤더와 빈줄과 요청 메시지 본문이 온다.


HTTP 응답 메시지는 아래와 유사한 구조로 되어 있다.


1
2
3
4
HTTP/1.1 200 OK
Content-Type: text/plain
 
This is the response body
cs


첫번째 줄은 상태줄(status line) 로 HTTP 프로토콜 버전, HTTP 상태 코드 번호, HTTP 상태 코드 문구 순으로 구성된다.

다음엔 요청 메시지처럼 하나 이상의 HTTP 헤더와 빈줄, 응답 메시지 본문이 온다.


이 문서에 설명된 인터페이스는 HTTP 메시지와 구성 요소들을 추상화 한다.



Messages


HTTP 메시지는 클라이언트가 서버로 보내는 요청이던지, 서버가 클라이언트로 보내는 응답이다.

이 명세는 HTTP 메시지를 위한 인터페이스를 정의한다.


Psr\Http\Message\RequestInterface

Psr\Http\Message\ResponseInterface


둘다 Psr\Http\Message\MessageInterface 인터페이스를 확장한다.

MessageInterface 는 직접 구현할 수도 있지만, RequestInterface 와 ResponseInterface 를 구현하도록 한다.



HTTP Headers


Case-insensitive header field names

HTTP 메시지는 대소문자 구분없는 헤더 필드 이름들을 포함한다.

헤더는 MessageInterface 를 구현하는 클래스에서 대소문자 구분없이 이름으로 검색된다. (Foo 와 FoO 는 같은 이름이다.)

대소문자를 구분하지 않지만, 특히 getHeaders() 로 이름을 검색할 때 원래 이름은 보존해야만 한다.

관행을 따르지 않는 HTTP 어플리케이션은 대소문자를 구분할 수도 있기 때문이다.


Headers with multiple values

헤더에 여러 값이 주어질 때는 MessageInterface 인스턴스에서 배열이나 문자열로 검색할 수 있다.

대소문자 구분없이 모든 헤더 값에서 콤마로 연결된 문자열로 검색하려면 getHeaderLine() 메소드를 사용한다.

대소문자 구분없이 모든 헤더 값에서 배열로 검색하려면 getHeaderLine() 메소드를 사용한다.

Set-Cookie 같은 헤더는 콤마로 연결할 수 없으므로, 이런 여러 값을 가지는 헤더를 검색할 때는 getHeader() 메소드를 사용하도록 한다.


1
2
3
4
5
6
7
8
9
10

$message = $message
    ->withHeader('foo''bar')
    ->withAddedHeader('foo''baz');
 
$header = $message->getHeaderLine('foo');
// $header contains: 'bar, baz'
 
$header = $message->getHeader('foo');
// ['bar', 'baz']
cs


Host header

요청에서, Host 헤더는 TCP 로 연결되었을 때(establishing) 사용된 host 처럼 URI 의 host 컴포넌트를 말한다.

Host 헤더가 제공되지 않았다면 제공된 URI 로부터 Host 헤더를 세팅해야 한다.

기본적으로 RequestInterface::withUri() 는 전달된 UriInterface 의 host 컴포넌트와 일치하는 Host 헤더로 반환된 요청 Host 헤더를 대신한다.

두번째 매개변수($preserveHost)를 true 로 하여 Host 헤더의 원래 상태를 유지할 수 있다.

우선순위 : 요청 Host 헤더 > 요청 Host 컴포넌트 > URI Host 컴포넌트



Streams


HTTP 메시지는 시작줄, 헤더, 본문으로 구성된다. 본문은 매우 짧거나 길 수 있다.

본문은 모두 메모리에 저장되어야 하기 때문에 문자열로 메시지 본문을 작성하면 예상보다 더 많은 메모리를 쉽게 소모한다.

메모리에 요청이나 응답 본문을 저장하려고면 큰 메시지 본문에서 작동이 가능하도록 구현시 제외한다.

StreamInterface 는 데이터 스트림을 읽거나 쓸 때, 상세 구현을 숨기기 위해 사용된다.

문자열이 적절한 메시지 구현이 될 경우 php://memory 나 php://temp 등 내장된 스트림이 사용될 것이다.


스트림은 isReadable(), isWritable(), isSeekable() 메소드 등의 기능을 가지며, 가능하다면 결합하여 사용할 수도 있다.

각 스트림 인스턴스는 읽기 전용, 쓰기 전용, 읽기-쓰기, 순차 검색, 병렬 검색, 소켓/파이프/콜백 등 다양한 기능을 가질 것이다.

마지막으로, StreamInterface 는 전체 본문 내용을 즉시 검색하거나 내보내는 것을 간소화 하기 위해 __toString() 메소드를 정의한다.

메시지의 상태가 변경될 수 있으므로, 구현시 요청과 응답은 읽기 전용 스트림으로 사용하는 것이 좋다.



Request Targets and URIs


요청 메시지의 첫번째 라인 두번째 매개변수에 "요청-타겟(request-target)" 이 포함된다.

요청 타겟은 다음 형식 중 하나가 될 수 있다.


origin-form

경로나 쿼리 스트링으로 구성되며 상대 URL 로도 불히며 가장 일반적인 형식이다.

TCP 로 전송된 메시지는 origin-form 이다.

scheme 과 authority 데이터는 CGI 변수로 표현된다.


absolute-form

scheme 과 authority([user-info@]host[:port]), 경로, 쿼리 스트링, 매개변수 등으로 구성된다.

절대 경로로 불리며, RFC 3986 에 설명된 URI 를 지정하기 위한 유일한 형태이다.

HTTP 프록시로 요청 생성시 사용된다.


authority-form

authority 만으로 구성된다. HTTP 클라이언트와 프록시 서버에 연결하기 위한 CONNECT 요청에만 사용된다.


asterisk-form

웹서버의 일반적인 기능을 결정하는 OPTIONS 메소드를 사용하는 문자열 * 만으로 구성된다.


이 외에도 effective URL 이 있는데, 이 형식은 HTTP 메시지로 전달되지 않고, UriInterface로 나타낸다.

effective URL 은 요청을 생성하는 프로토콜(http/https), 호스트이름, 포트를 결정하는데 사용된다.

UriInterface 는 RFC 3986 에 지정된대로 HTTP 와 HTTPS URI 를 모델링한다.

인터페이스는 URI 의 반복된 파싱을 제거하고 여러 URI 부분들을 처리하기 위한 메소드들을 제공한다.

또한 문자열로 표현하기 위해 모델링된 URI 를 캐스팅할 ___toString() 메소드를 지정한다.


getRequestTarget() 으로 요청 타겟을 검색할 때, 이 메소드는 URI 객체를 사용하고 origin-form 구성에 필요한 모든 컴포넌트를 추출할 것이다.

다른 세가지 형식을 사용하려거나, 요청 타겟을 덮어씌우길 원하면 withRequestTarget() 을 사용한다.

이 메소드를 호출하는 것은 getUri() 로 반환되는것 처럼 URI 에 영향을 미치지 않는다.


예를 들어 사용자가 서버에 asterisk-form 요청하도록 하려면,


1
2
3
4
5

$request = $request
    ->withMethod('OPTIONS')
    ->withRequestTarget('*')
    ->withUri(new Uri('https://example.org/'));
cs


이 예는 다음과 같은 요청을 나타낼 것이다.


1
OPTIONS * HTTP/1.1
cs


그러나 HTTP 클라이언트는 프로토콜, 호스트이름, tcp 포트를 결정하기 위해 getuRI() 로 effective URL 을 사용할 수도 있을 것이다.

HTTP 클라이언트는 Uri::getPath() 와 Uri::getQuery() 값을 생략하고, 대신 이 두 값을 연결하는 getRequestTarget() 의 반환값을 사용해야 한다.


4가지 요청 타겟 형식 중 하나 이상을 구현하지 않을 클라이언트는 getRequestTarget() 을 사용해야 한다.

이 클라이언트들은 지원하지 않는 요청 타겟을 거부해야 하고, getUri() 로 값을 반환해서는 안된다.


RequestInterface 는 요청 타겟을 검색하거나 제공된 요청 타겟으로 새 인스턴스를 생성하는 메소드를 제공한다.

인스턴스에 요청 타겟이 없드면, getRequestTarget() 은 URI 의 origin-form 이나 슬래시(/) 를 반환할 것이다.

withRequestTarget($requestTarget) 은 지정된 요청 타겟으로 새 인스턴스를 생성하고, 다른 세 요청 타겟 형식을 나타내는 요청 메시지를 생성할 수 있게 한다.

서버로 연결을 생성하는데 사용될 클라이언트에서 구성된 URI 인스턴스는 여전히 사용될 수 있다.



Server-side Requests


RequestInterface 는 HTTP 요청의 일반적 표현을 제공한다.

그러나 서버 측 환경의 특성 때문에, 서버 측 요청은 추가 처리가 필요하다.

서버 측 처리는 CGI 와 PHP 의 추상화, SAPI 를 통한 CGI 확장을 고려해야 한다.

PHP 는 다음처럼 슈퍼 전역을 통한 입력을 단순화 할 수 있다.


$_COOKIE : HTTP 쿠키로 쉽게 접근 가능

$_GET : 쿼리 스트링 매개변수로 쉽게 접근 가능

$_POST : HTTP POST 로 전달된 urlencoded 파라미터에 쉽게 접근 가능

$_FILES : 파일 업로드의 메타 데이터에 접근 가능

$_SERVER : CGI/SAPI 환경 변수에 접근 가능 (요청 메소드, 스킴, URI, 헤더 포함)


ServerRequestInterface 는 이러한 슈퍼 전역 변수들의 추상화를 위해 RequestInterface 를 확장한다.

이 작업은 슈퍼 전역 변수로의 연결을 줄이는데 도움이 되고 요청을 테스트 하기 위한 기능을 향상한다.


서버 요청은 어플리케이션의 특정 규칙에 대하여 요청을 검사하기 위해 "attributes" 라는 하나의 속성을 추가한다.

이처럼, 서버 요청은 다수의 요청 간에 메시지를 제공할 수 있다.



Uploaded files


ServerRequestInterface 는 UploadedFileInterface 의 각각의 인스턴스로 표준화된 구조의 업로드 파일 트리를 검색하기 위한 메소드를 지정한다.


$_FILES 슈퍼 전역 변수는 input 태그 에서 파일 배열을 처리할 때 약간의 문제가 발생한다.

files 란 이름의 배열로 파일들이 전송되는 폼이 있다면 PHP 는 다음과 같이 표현될 것이다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14

array(
    'files' => array(
        'name' => array(
            0 => 'file0.txt',
            1 => 'file1.html',
        ),
        'type' => array(
            0 => 'text/plain',
            1 => 'text/html',
        ),
        /* etc. */
    ),
)
cs


하지만 우리가 원하는 것은 이런 형태일 것이다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

array(
    'files' => array(
        0 => array(
            'name' => 'file0.txt',
            'type' => 'text/plain',
            /* etc. */
        ),
        1 => array(
            'name' => 'file1.html',
            'type' => 'text/html',
            /* etc. */
        ),
    ),
)
cs


이 구현 세부 사항을 알고, 주어진 업로드에 대한 데이터를 수집하기 위해 코드를 작성해야 한다.

추가적으로, 파일이 업로드될 때 $_FILES 가 존재하지 않는 시나리오가 있다.


  • HTTP 메소드가 POST 가 아닌 경우
  • 유닛 테스트인 경우 
  • ReactPHP 같은 non-SAPI 환경에서 동작하는 경우


이 경우 데이터는 다르게 시드해야 할 것이다. 예를 들면,


  • 프로세스는 파일 업로드를 발견하기 위해 메시지 본문을 파싱할 것이다. 이 때, 구현은 파일 시스템으로 파일 업로드를 작성하지 않을 것이고, 메모리, I/O, 스토리지 오버헤드를 줄이기 위해 스트림으로 포장할 것이다.
  • 유닛 테스트에서 다른 경우들을 확인하고 검증하기 위해 파일 업로드 메타 데이터를 속일 수 있어야 한다.


getUploadedFiles() 는 정규화된 구조를 제공한다.

  • 전달된 파일 업로드의 모든 정보를 모아서, Psr\Http\Message\UploadedFileInterface 인스턴스를 생성하기 위해 사용하라.
  • 트리의 주어진 위치에 적절한 Psr\Http\Message\UploadedFileInterface 의 각각의 인스턴스로 전송된 트리구조를 재생성하라.


아래는 배열이 아닌 이름으로 폼 전송하는 예이다.


1
<input type="file" name="avatar" />
cs


$_FILES 의 구조는 다음과 같다.


1
2
3
4
5
6
7
8
9
10

array(
    'avatar' => array(
        'tmp_name' => 'phpUxcOty',
        'name' => 'my-avatar.png',
        'size' => 90996,
        'type' => 'image/png',
        'error' => 0,
    ),
)
cs


getUploadedFiles() 로 반환된 정규화된 형태는 다음과 같다.


1
2
3
4

array(
    'avatar' => /* UploadedFileInterface instance */
)
cs


배열 이름의 표기를 사용한 예이다.


1
<input type="file" name="my-form[details][avatar]" />
cs


$_FILES 의 구조는 다음과 같다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14

array(
    'my-form' => array(
        'details' => array(
            'avatar' => array(
                'tmp_name' => 'phpUxcOty',
                'name' => 'my-avatar.png',
                'size' => 90996,
                'type' => 'image/png',
                'error' => 0,
            ),
        ),
    ),
)
cs


getUploadedFiles() 로 반환된 트리는 다음과 같다.


1
2
3
4
5
6
7
8

array(
    'my-form' => array(
        'details' => array(
            'avatar' => /* UploadedFileInterface instance */
        ),
    ),
)
cs


자바스크립트로 파일 멀티 업로드를 추가하는 예에서처럼 파일 타입의 배열을 지정해 본다.


1
2
Upload an avatar: <input type="file" name="my-form[details][avatars][]" />
Upload an avatar: <input type="file" name="my-form[details][avatars][]" />
cs


이러한 경우, $_FILES 정상적인 구조로부터 벗어나기 때문에 주어진 인덱스에 파일 관련 모든 정보를 나타낸다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

array(
    'my-form' => array(
        'details' => array(
            'avatars' => array(
                'tmp_name' => array(
                    0 => '...',
                    1 => '...',
                    2 => '...',
                ),
                'name' => array(
                    0 => '...',
                    1 => '...',
                    2 => '...',
                ),
                'size' => array(
                    0 => '...',
                    1 => '...',
                    2 => '...',
                ),
                'type' => array(
                    0 => '...',
                    1 => '...',
                    2 => '...',
                ),
                'error' => array(
                    0 => '...',
                    1 => '...',
                    2 => '...',
                ),
            ),
        ),
    ),
)
cs


다음은 위의 $_FILES 배열이 getUploadedFiles() 로부터 반환된 구조이다.


1
2
3
4
5
6
7
8
9
10
11
12

array(
    'my-form' => array(
        'details' => array(
            'avatars' => array(
                0 => /* UploadedFileInterface instance */,
                1 => /* UploadedFileInterface instance */,
                2 => /* UploadedFileInterface instance */,
            ),
        ),
    ),
)
cs


그리고 index 1 의 배열은 다음처럼 접근한다.


1
2
<?
$request->getUploadedFiles()['my-form']['details']['avatars'][1];
cs


업로드된 파일들은 $_FILES 나 요청 본문으로부터 파생되었기 때문에, withUploadedFiles() 는 다른 처리로 표준화 위임을 허용하기 위해 인터페이스에 존재한다.


1
2
3
4
5
6
7
8
9
10
11
<?
$file0 = $request->getUploadedFiles()['files'][0];
$file1 = $request->getUploadedFiles()['files'][1];
 
printf(
    "Received the files %s and %s",
    $file0->getClientFilename(),
    $file1->getClientFilename()
);
 
// "Received the files file0.txt and file1.html"
cs


위 예는 non-SAPI 환경에서도 작동함을 알아야 한다.

따라서, UploadedFileInterface는 환경에 관계없이 작동할 수 있는 메소드를 제공한다.


moveTo($targetPath)

임시 업로드 파일에 move_uploaded_file() 을 직접 호출하기 위한 대안으로 안전하다.

구현은 환경에 기반하여 사용할 수있는 올바른 동작을 감지한다.


getStream() 

StreamInterface 인스턴스를 반환한다. non-SAPI 환경에서 한가지 가능한 것은 php://temp 스트림에 개별 업로드 파일을 파싱하는 것이다.

따라서 getStream() 은 환경에 관계없이 동작할 것이다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?
// Move a file to an upload directory
$filename = sprintf(
    '%s.%s',
    create_uuid(),
    pathinfo($file0->getClientFilename(), PATHINFO_EXTENSION)
);
$file0->moveTo(DATA_DIR . '/' . $filename);
 
// Stream a file to Amazon S3.
// Assume $s3wrapper is a PHP stream that will write to S3, and that
// Psr7StreamWrapper is a class that will decorate a StreamInterface as a PHP
// StreamWrapper.
$stream = new Psr7StreamWrapper($file1->getStream());
stream_copy_to_stream($stream$s3wrapper);
cs




WRITTEN BY
손가락귀신
정신 못차리면, 벌 받는다.

,