SitePoint에 게시된, Bruno Skvorc의 Starting a New PHP Package The Right Way 포스트를 번역한 글이다. PHP는 autoload를 이용한 composer를 비롯 다양한 모듈화 방법이 논의되어 실제로 많이 활용하고 있다. PHP의 최근 동향을 살펴 볼 수 있는 포스트라 생각해 한국어로 옮겼다. 연재는 총 3회 분이며 이 포스트는 1회에 해당한다.

Bruno and Ophélie at SitePoint! Thanks for giving me the opportunity to translate your great article. :)


시각적 인공지능 기계학습 크롤러인 Diffbot을 이야기 할 때, 이 라이브러리를 사용할 수 있는 많은 프로그래밍 언어를 언급했다. 이 많은 언어를 동시에 보고 있으면 거기엔 미끄러져 흠집이 난, 상한 사과도 있기 마련이다. 그 사과 중 하나인 PHP 라이브러리는 이 연재에 기반해 더 나은 라이브러리를 만들고자 한다.

이 튜토리얼은 좋은 패키지를 어떻게 작성할 것인가에 중점을 두고 있으며 코드는 실제적이고 실무적일 것일 것이지만 Diffbot 자체에 지나치게 집중하진 않을 것이다. Diffbot의 API는 충분히 단순하고 Guzzle의 인터페이스는 PHP 라이브러리 없이도 사용하기 간편하다. 양질의 PHP 패키지를 개발하는데 어떻게 사용할 것인가에 대한 접근 방식은 당신의 프로젝트에서 어떻게 활용해야 할지 배울 수 있다. Diffbot이 패키지의 주제로 선택된 이유는 실제적인 예제로서 입증하는 것이지 단순히 다른 “홍길동” 패키지를 만드는 것을 의미하지 않는다.

좋은 패키지 디자인

최근 몇 년 동안, PHP 패키지 디자인을 위한 좋은 표준들이 많이 나왔다. Composer, Packagist, The league 그리고 가장 최근에 The Checklist까지 말이다. 다음 나오는 항목들을 준수한다면 The League를 제외하고는 패키지를 제출할 수 있다. (물론 여기서 작성하는 패키지는 제출하지 않기 바란다. – 우린 단지 서드 파티 API 제공자를 위해 만들었고 또한 아주 제한적인 컨텍스트만 제공하기 때문이다.) 우리가 따를 규칙은 다음과 같다:

  1. 저작권을 포함할 것
  2. 오픈소스여야 할 것
  3. 개발 관련 부분을 배포본과 분리할 것
  4. PSR-4 autoloading 을 사용할 것
  5. Composer 설치를 위해 Packagist에서 호스팅 할 것
  6. 프레임워크에 상관 없이 사용 가능할 것
  7. PSR-2 코딩 표준을 준수할 것
  8. 깊이 있는 주석을 포함할 것
  9. 유의적 버전을 사용할 것
  10. CI와 유닛 테스트를 사용할 것

이 항목들에 대해 더 자세히 알고 싶다면 다음 문서를 참고한다.

시작하기

여기서 Homestead Improved 환경을 다시 사용할 것인데 이는 가장 빠르게 통일된 환경에서 개발할 수 있도록 도와준다. 참고로 다음과 같은 가상 환경을 통해 이 튜토리얼을 진행할 예정이다:

sites:
  - map: test.app
    to: /home/vagrant/Code/diffbot_lib
  - map: test2.app
    to: /home/vagrant/Code/diffbot_test

이렇게 VM을 통해 hacking을 해보자.

바닥부터 시작하기 위해 League Skeleton을 사용한다. League의 규칙이 적용된 템플릿 패키지로 쉽게 시작하는데 도움이 된다. 원래 리포지터리에서 fork한 이 리포지터리는 원래 템플릿보다 개선된 .gitignore가 첨부되어 있고 몇가지 소소한 수정이 포함되어 있다. 만약 원하지 않는다면 원본을 사용하고 차이점은 직접 비교해보기 바란다.

git clone https://github.com/Swader/php_package_skeleton diffbot_lib

이제 composer.json을 다음과 같이 수정할 차례다:

{
  "name": "swader/diffbot_client",
  "description": "A PHP wrapper for using Diffbot's API",
  "keywords": [
    "diffbot", "api", "wrapper", "client"
  ],
  "homepage": "https://github.com/swader/diffbot_client",
  "license": "MIT",
  "authors": [
    {
      "name": "Bruno Skvorc",
      "email": "[email protected]",
      "homepage": "http://bitfalls.com",
      "role": "Developer"
    }
  ],
  "require": {
    "php" : ">=5.5.0"
  },
  "require-dev": {
    "phpunit/phpunit" : "4.*"
  },
  "autoload": {
      "psr-4": {
          "Swader\Diffbot\": "src"
      }
  },
  "autoload-dev": {
      "psr-4": {
          "Swader\Diffbot\Test\": "tests"
      }
  },
  "extra": {
      "branch-alias": {
          "dev-master": "1.0-dev"
      }
  }
}

우리는 일반적인 메타 데이터를 설정하고 요구 항목을 정의했고 PSR-4 autoloading을 설정했다. 이 과정이 위 목록에서 1-6번 항목에 해당한다. 여기서 Diffbot API를 호출할 수 있도록 돕는 HTTP 클라이언트 라이브러리 Guzzle을 요구 항목에 추가한다.

"require": {
  "php" : ">=5.5.0",
  "guzzlehttp/guzzle": "~5.0"
},

composer install을 실행하면 모든 의존성을 내려 받는다. 테스트를 위해 포함한 PHPUnit도 포함이 되는데 이 모든 것이 동작하는지 확인하기 위해 다음과 같이 src/SkeletonClass.php를 변경할 수 있다:

<?php
namespace SwaderDiffbot;
class SkeletonClass
{
  /**
   * Create a new Skeleton Instance
   */
  public function __consturct()
  {
  }

  /**
   * Friendly welcome
   *
   * @param string $phrase Phrase to return
   * @return string Returns the phrase passed in
   */
  public function echoPhrase($phrase)
  {
    return $phrase;
  }
}

그리고 프로젝트 최상위 경로에 index.php도 다음과 같이 추가한다:

<?php
require_once "vender/autoload.php";
$instance = new SwaderDiffbotSkeletonClass();
echo $instance->echoPhrase("It's working");

이제 브라우저로 test.app:8000에 접속해보면 “It’s working” 메시지를 볼 수 있다.

아직 public 디렉토리나 이와 같은 파일을 만들지 않았다고 걱정하지 말자. 패키지를 만들 때에는 중요하지 않다. 라이브러리를 만들 때에는 패키지에만 집중해야 하고 또한 프레임워크나 MVC가 아닌 패키지만 있어야 한다. 그리고 index.php를 잠깐 잠깐 테스트 용도로 사용하겠지만 대부분 PHPUnit을 통해 라이브러리를 개발할 것이다. 여기서는 index.php를 실수로 리포지터리에 보내는 일이 없도록 .gitignoreindex.php를 추가하도록 한다.

PSR-2

현대적인 표준을 따르기 위해 PSR-2를 준수해야 한다. PhpStorm을 사용한다면 엄청 쉬운 일이다. 내장되어 있는 PSR1/PSR2 표준을 선택하거나 CodeSniffer 플러그인을 설치해 PhpStorm 인스팩션으로 활성화시켜 사용할 수도 있다. 아쉽게도 예전 방법을 활용해야 하는데 PHPStorm이 아직 phpcs의 원격 실행을 지원하지 않기 때문이다. (Vagrant VM도 결국 원격 환경이므로. 만약 이 기능을 PHPStorm에 탑재하기 원한다면 여기에서 투표할 수 있다.)

어찌 되었든 CodeSniffer가 평소처럼 프로젝트에 필요하기 때문에 composer를 통해 설치하고 VM의 커맨드라인에서 구동하자:

composer global require "squizlabs/php_codesniffer=*"
phpcs src/
# 코드를 검사해 리포트를 보여준다

계획하기

지금까지 만든 틀로 본격적인 개발을 시작할 수 있다. 이제 필요한 부분을 생각해보자.

시작점

Diffbot API를 어떻게 사용하든지 사용자는 API 클라이언트의 인스턴스를 생성하길 원할 것이다. 이런 경우 미리 만들어진 API를 호출하는 것 이외에는 할 수 있는 것이 없다. 각각의 API를 사용해서 요청을 쿼리 파라미터로 보내려면 폼에서 ?token=xxxxxx와 같이 개발자 토큰이 필요하다. 단일 사용자는 하나의 토큰만 사용할 것이기 때문에 새 API 클라이언트 인스턴스를 생성할 때 토큰을 생성자에 넣어 생성해야 한다. 물론 모든 인스턴스를 생성할 때 활용하기 위해 전역 토큰으로 생성해 사용할 수도 있다. 위에서 이야기 한 두 가지 방법을 다음과 같이 표현 할 수 있다:

$token = xxxx;

// approach 1
$api1 = new Diffbot($token);
$api2 = new Diffbot($token);

// approach 2
Diffbot::setToken($token);
$api1 = new Diffbot();
$api2 = new Diffbot();

전자는 단일 API 클라이언트 인스턴스를 생성할 때 또는 여러 토큰을 사용할 때(예를 들면, 토큰 하나는 Crawlbot에 다른 하나는 일반적인 API를 사용할 때) 도움이 된다. 후자는 자신의 어플리케이션이 여러 API 엔드포인트에서 여러 차례 사용될 때 매번 토큰을 주입하는게 번거로울 때 활용할 수 있다.

위 내용을 생각하며 다음과 같이 첫 클래스를 작성할 수 있다. src/Diffbot.php를 작성한다.

<?php 
namespace SwaderDiffbot;
use SwaderDiffbotExceptionsDiffbotException;

/**
 * Class Diffbot
 *
 * The main class for API consumption
 *
 * @package SwaderDiffbot
 */
class Diffbot
{
  /** @var string The API access token */
  private static $token = null;

  /** @var string The instance token, settable once per new instance */
  private $instanceToken;

  /**
   * @param string|null $token The API access token, as obtained on diffbot.com/dev
   * @throws DiffbotException When no token is provided
   */
  public function __consturct($token = null)
  {
    if ($token === null) {
      if (self::$token === null) {
        $msg = 'No token provided, and none is globally set. ';
        $msg .= 'Use Diffbot::setToken, or instantiate the Diffbot class with a $token parameter.';
        throw new DiffbotException($msg);
      }
    } else {
      self::validateToken($token);
      $this->instanceToken = $token;
    }
  }

  /**
   * Sets the token for all future new instances
   * @param $token string The API access token, as obtained on diffbot.com/dev
   * @return void
   */
  public static function setToken($token)
  {
    self::validateToken($token);
    self::$token = $token;
  }

  private static function validateToken($token)
  {
    if (!is_string($token)) {
      throw new InvalidArgumentException('Token is not a string.');
    }
    if (strlen($token) < 4) {
      throw new InvalidArgumentException('Token "' . $token . '" is too short, and thus invalid.');
    }
    return true;
  }
}
?>

메소드에서 DiffbotException을 참조한다. src/exceptions/DiffbotException.php를 다음 내용으로 작성한다.

<?php 
namespace SwaderDiffbotExceptions;

/**
 * Class DiffbotException
 * @package SwaderDiffbotExceptions
 */
class DiffbotException extends Exception
{

}
?>

Diffbot 클래스에 대해 간단하게 설명하면, token 정적 프로퍼티는 새 인스턴스를 생성하면서 토큰을 넣지 않았을 때 기본값으로 사용한다. 인스턴스를 생성하면서 토큰을 넣으면 instanceToken 프로퍼티로 저장한다.

생성자에서 토큰을 전달 받는지 확인한다. 만약 받지 않았다면, 미리 선언된 기본 토큰이 있는지 확인하고 만약 없다면 DiffbotException 예외를 발생하게 된다. 이 예외를 위해 위 예외 코드를 작성했다. 만약 토큰이 괜찮다면 인스턴스에 토큰이 설정된다. 반면 토큰이 전달 되었다면 instanceToken으로 저장된다. 위 두 경우 모두 validateToken 정적 메소드를 통해 검증 절차를 거치게 된다. 토큰의 길이가 3글자 이상인지 체크하는, 단순한 프라이빗 메소드로 통과하지 못하면 아규먼트 검증 실패 예외를 발생하게 된다.

마지막으로 setToken 정적 메소드로 앞에서 말한 전역 토큰으로 활용한다. 이 역시 토큰 검증 과정을 거친다.

Diffbot 토큰을 보면 API를 호출하는 상황에서 기존에 있던 token을 변경이 가능한데 이렇게 되면 기존에 존재하는 Diffbot 인스턴스는 의도와 다르게 동작할 수 있다. 이런 경우를 대비해 토큰을 인스턴스 생성할 때마다 주입을 하거나 전역적으로 주입하고 Diffbot을 사용하거나 해야 한다. 물론 토큰을 전역적으로 설정하면 인스턴스의 이 설정을 덮어 쓸 수 있다. 물론 전역 토큰도 수정 가능하기 때문에 여러가지 환경에 따라 인스턴스의 토큰이 변경되도록, 이미 존재하는 인스턴스에는 영향을 주진 않을 것이다.

이 모든 내용이 블럭 주석으로 문서화 된 것을 볼 수 있는데 과하게 문서화 할 필요는 없지만 모두가 이해할 수 있도록 간단하게 작성을 해야 한다.

결론

이 파트에서 몇가지 간단한 기능과 환경설정이 들어 있는 스켈레톤 프로젝트로 PHP 패키지 개발을 시작했다. 파트 1에서의 결과는 다음 링크에서 받을 수 있다. 파트 2는 테스트와 실질적인 기능들을 작성하고 기초적인 TDD를 할 예정이다.