분류 php

PHP로 간단한 REST API 빌드

컨텐츠 정보

  • 조회 336 (작성일 )

본문

REST (REpresentational State Transfer)는 웹 서비스를 만드는 데 사용할 제약 조건 집합을 정의하는 소프트웨어 아키텍처 스타일입니다. REST API는 현대 웹 개발의 기둥 중 하나입니다. 요즘 대부분의 웹 애플리케이션은 다른 언어로 작성된 백엔드 API에 연결된 프런트 엔드의 단일 페이지 애플리케이션으로 개발됩니다.

몇 분 만에 REST API를 구축하는 데 도움이 되는 다양한 PHP 프레임 워크가 있습니다. 하지만 핵심 PHP에서 간단한 REST API를 빌드하는 방법을 알아 보겠습니다.

이것은 핵심 PHP를 사용하여 PHP에서 REST API를 빌드 하는 방법을 배우는 PHP 학습 시리즈의 첫 번째 부분입니다.


예정된 두 가지 :

  • Magic으로 API를 보호하세요.
  • Heroku에 API를 배포합니다.

전제 조건 

PHP REST API 스켈레톤 


/src 디렉토리와 하나의 종속성이 있는 최상위 디렉토리에 composer.json 파일을 만듭니다. 즉, .env 파일에 보안 정보를 저장할 수 있는 DotEnv 라이브러리입니다.


composer.json

{
  "require": {
    "vlucas/phpdotenv": "^2.4"
  },
  "autoload": {
    "psr-4": {
      "Src\\": "src/"
    }
  }
}


PSR-4 자동 로더는 /src 디렉토리에서 PHP 클래스를 자동으로 찾습니다.


지금 종속성 설치 :

composer install


/vendor 디렉토리를 만들고 DotEnv 종속성이 설치됩니다 (자동 로더는 include() 호출 없이 /src에서 클래스를 로드합니다).


두 줄로 된 프로젝트에 대한 .gitignore 파일을 만들면 /vendor 디렉토리와 로컬 .env 파일이 무시됩니다.


.gitignore

vendor/
.env


다음으로 Secret 변수에 대한 .env.example 파일을 만듭니다.


.env.example

DB_HOST=localhost
DB_PORT=3306
DB_DATABASE=
DB_USERNAME=
DB_PASSWORD=


나중에 실제 세부 정보를 입력 할 .env 파일 (Git에서 무시하므로 저장소에 저장되지 않음).


환경 변수를 로드하는 start.php 파일을 만듭니다.


start.php 

<?php
require 'vendor/autoload.php';
use Dotenv\Dotenv;

use Src\Database;

$dotenv = new DotEnv(__DIR__);
$dotenv->load();

// test code:
// it will output: localhost
// when you run $ php start.php
echo getenv('DB_HOST');


PHP REST API 용 데이터베이스 구성 


MySQL을 사용하여 간단한 API를 구동합니다. 앱에 대한 새 데이터베이스 및 사용자를 만듭니다.

mysql -u root -p
CREATE DATABASE blog CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'rest_api_user'@'localhost' identified by 'rest_api_password';
GRANT ALL on blog.* to 'rest_api_user'@'localhost';
quit


REST API에는 id, title, body, author, author_picture, created_at 필드가 있는 블로그 애플리케이션의 게시물이 포함됩니다. 사용자가 블로그 애플리케이션에 블로그를 게시 할 수 있습니다.


MySQL에서 데이터베이스 테이블을 만듭니다.

mysql -u rest_api_user -p;
// Enter your password
use blog;

CREATE TABLE `post` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `title` varchar(255) NOT NULL,
  `body` text NOT NULL,
  `author` varchar(255),
  `author_picture` varchar(255),
  `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`)
);


.env 파일에 데이터베이스 연결 변수를 추가합니다.


.env 


DB_HOST=localhost
DB_PORT=3306
DB_DATABASE=blog
DB_USERNAME=rest_api_user
DB_PASSWORD=rest_api_password


데이터베이스 연결을 유지하는 클래스를 만들고 연결 초기화를 start.php 파일에 추가합니다.


src/Database.php

<?php
namespace Src;

class Database {

  private $dbConnection = null;

  public function __construct()
  {
    $host = getenv('DB_HOST');
    $port = getenv('DB_PORT');
    $db   = getenv('DB_DATABASE');
    $user = getenv('DB_USERNAME');
    $pass = getenv('DB_PASSWORD');

    try {
      $this->dbConnection = new \PDO(
          "mysql:host=$host;port=$port;dbname=$db",
          $user,
          $pass
      );
    } catch (\PDOException $e) {
      exit($e->getMessage());
    }
  }

  public function connet()
  {
    return $this->dbConnection;
  }
}


start.php

<?php
require 'vendor/autoload.php';
use Dotenv\Dotenv;

use Src\Database;

$dotenv = new DotEnv(__DIR__);
$dotenv->load();

$dbConnection = (new Database())->connet();


Post 테이블에 대한 클래스 추가 및 PHP REST API 구현 


객체 지향 컨텍스트에서 데이터베이스와 상호 작용하는 방법은 여러 가지가 있지만 모든 게시물을 반환하고 특정 게시물을 반환하고 게시물을 추가 / 업데이트 / 삭제하는 방법을 구현하는 간단한 방법을 사용하겠습니다.


또한 프런트 엔드 api/index.php에서 처리 할 API 엔드 포인트도 있습니다.


다음 엔드 포인트가 있는 REST API :


귀하의 API 


APICRUDDescription
GET /postsREADGet all the Posts from post table
GET /post/{id}READGet a single Post from post table
POST /postCREATECreate a Post and insert into post table
PUT /post/{id}UPDATEUpdate the Post in post table
DELETE /post/{id}DELETEDelete a Post from post table

api/index.php

<?php
require "../start.php";
use Src\Post;

header("Access-Control-Allow-Origin: *");
header("Content-Type: application/json; charset=UTF-8");
header("Access-Control-Allow-Methods: OPTIONS,GET,POST,PUT,DELETE");
header("Access-Control-Max-Age: 3600");
header("Access-Control-Allow-Headers: Content-Type, Access-Control-Allow-Headers, Authorization, X-Requested-With");

$uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$uri = explode( '/', $uri );

// all of our endpoints start with /post or /posts
// everything else results in a 404 Not Found
if ($uri[1] !== 'post') {

    if($uri[1] !== 'posts'){
        header("HTTP/1.1 404 Not Found");
        exit();
    }

}

if ($uri[1] == 'posts' and isset($uri[2])) {
    header("HTTP/1.1 404 Not Found");
    exit();
}

// the post id is, of course, optional and must be a number:
$postId = null;
if (isset($uri[2])) {
    $postId = (int) $uri[2];
}

$requestMethod = $_SERVER["REQUEST_METHOD"];

// pass the request method and post ID to the Post and process the HTTP request:
$controller = new Post($dbConnection, $requestMethod, $postId);
$controller->processRequest();


src/Post.php

<?php
namespace Src;

class Post {
  private $db;
  private $requestMethod;
  private $postId;

  public function __construct($db, $requestMethod, $postId)
  {
    $this->db = $db;
    $this->requestMethod = $requestMethod;
    $this->postId = $postId;
  }

  public function processRequest()
  {
    switch ($this->requestMethod) {
      case 'GET':
        if ($this->postId) {
          $response = $this->getPost($this->postId);
        } else {
          $response = $this->getAllPosts();
        };
        break;
      case 'POST':
        $response = $this->createPost();
        break;
      case 'PUT':
        $response = $this->updatePost($this->postId);
        break;
      case 'DELETE':
        $response = $this->deletePost($this->postId);
        break;
      default:
        $response = $this->notFoundResponse();
        break;
    }
    header($response['status_code_header']);
    if ($response['body']) {
        echo $response['body'];
    }
  }

  private function getAllPosts()
  {
    $query = "
      SELECT
          id, title, body, author, author_picture, created_at
      FROM
          posts;
    ";

    try {
      $statement = $this->db->query($query);
      $result = $statement->fetchAll(\PDO::FETCH_ASSOC);
    } catch (\PDOException $e) {
      exit($e->getMessage());
    }

    $response['status_code_header'] = 'HTTP/1.1 200 OK';
    $response['body'] = json_encode($result);
    return $response;
  }

  private function getPost($id)
  {
    $result = $this->find($id);
    if (! $result) {
        return $this->notFoundResponse();
    }
    $response['status_code_header'] = 'HTTP/1.1 200 OK';
    $response['body'] = json_encode($result);
    return $response;
  }

  private function createPost()
  {
    $input = (array) json_decode(file_get_contents('php://input'), TRUE);
    if (! $this->validatePost($input)) {
      return $this->unprocessableEntityResponse();
    }

    $query = "
      INSERT INTO posts
          (title, body, author, author_picture)
      VALUES
          (:title, :body, :author, :author_picture);
    ";

    try {
      $statement = $this->db->prepare($query);
      $statement->execute(array(
        'title' => $input['title'],
        'body'  => $input['body'],
        'author' => $input['author'],
        'author_picture' => 'https://secure.gravatar.com/avatar/'.md5(strtolower($input['author'])).'.png?s=200',
      ));
      $statement->rowCount();
    } catch (\PDOException $e) {
      exit($e->getMessage());
    }

    $response['status_code_header'] = 'HTTP/1.1 201 Created';
    $response['body'] = json_encode(array('message' => 'Post Created'));
    return $response;
  }

  private function updatePost($id)
  {
    $result = $this->find($id);
    if (! $result) {
      return $this->notFoundResponse();
    }
    $input = (array) json_decode(file_get_contents('php://input'), TRUE);
    if (! $this->validatePost($input)) {
      return $this->unprocessableEntityResponse();
    }

    $statement = "
      UPDATE posts
      SET
        title = :title,
        body  = :body,
        author = :author,
        author_picture = :author_picture
      WHERE id = :id;
    ";

    try {
      $statement = $this->db->prepare($statement);
      $statement->execute(array(
        'id' => (int) $id,
        'title' => $input['title'],
        'body'  => $input['body'],
        'author' => $input['author'],
        'author_picture' => 'https://secure.gravatar.com/avatar/'.md5($input['author']).'.png?s=200',
      ));
      $statement->rowCount();
    } catch (\PDOException $e) {
      exit($e->getMessage());
    }
    $response['status_code_header'] = 'HTTP/1.1 200 OK';
    $response['body'] = json_encode(array('message' => 'Post Updated!'));
    return $response;
  }

  private function deletePost($id)
  {
    $result = $this->find($id);
    if (! $result) {
      return $this->notFoundResponse();
    }

    $query = "
      DELETE FROM posts
      WHERE id = :id;
    ";

    try {
      $statement = $this->db->prepare($query);
      $statement->execute(array('id' => $id));
      $statement->rowCount();
    } catch (\PDOException $e) {
      exit($e->getMessage());
    }
    $response['status_code_header'] = 'HTTP/1.1 200 OK';
    $response['body'] = json_encode(array('message' => 'Post Deleted!'));
    return $response;
  }

  public function find($id)
  {
    $query = "
      SELECT
          id, title, body, author, author_picture, created_at
      FROM
          posts
      WHERE id = :id;
    ";

    try {
      $statement = $this->db->prepare($query);
      $statement->execute(array('id' => $id));
      $result = $statement->fetch(\PDO::FETCH_ASSOC);
      return $result;
    } catch (\PDOException $e) {
      exit($e->getMessage());
    }
  }

  private function validatePost($input)
  {
    if (! isset($input['title'])) {
      return false;
    }
    if (! isset($input['body'])) {
      return false;
    }

    return true;
  }

  private function unprocessableEntityResponse()
  {
    $response['status_code_header'] = 'HTTP/1.1 422 Unprocessable Entity';
    $response['body'] = json_encode([
      'error' => 'Invalid input'
    ]);
    return $response;
  }

  private function notFoundResponse()
  {
    $response['status_code_header'] = 'HTTP/1.1 404 Not Found';
    $response['body'] = null;
    return $response;
  }
}


PHP 서버를 시작하고 Postman과 같은 도구로 API를 테스트 해 보겠습니다.


php -S localhost:8008 -t api


Done 


축하합니다!! REST API를 성공적으로 빌드했습니다.


GitHub : https://github.com/shahbaz17/php-rest-api


향후 계획 


이제 이 애플리케이션을 구축 했으므로 이러한 엔드 포인트가 보호되지 않으며 인터넷의 모든 사용자가 데이터를 활용하고 업데이트 / 삭제 / 삽입 할 수 있음을 알 수 있습니다. 따라서 인증 및 권한 부여를 통해 이 API를 보호합시다.


다음 기사에서는 이러한 코드를 몇 줄만 변경하여 Magic의 Passwordless 기능을 활용하고 API를 보호하는 방법을 다룰 것입니다.