tag: php

Micro-Framework, Lumen 살펴보기

Laravel에서 발표한 경량 프레임워크 Lumen을 간단하게 정리. 작고 간단하면서도 강력한 기능을 제공.

2015년 4월 17일

Micro-framework의 전성기라고 할 만큼 다양한 환경과 언어로 프레임워크가 쏟아지고 있다. PHP에도 micro-framework가 많이 나와 있는데1 최근 Laravel에서 Lumen을 발표했다. 발표 자료에서는 symfony2 기반인 silex보다 1.9배 빠르다고 하는데 문법적으로는 Silm과 상당히 유사한 느낌도 든다. 기존에 나왔던 프레임워크와 엄청나게 큰 구조 차이를 가지고 있는 것은 아니지만 Laravel과의 호환을 염두한 부분도 많다는 느낌을 받았다. 또한 구조적으로도 silex나 여타 기존에 나온 micro-framework 보다 훨씬 깔끔하고 미려하다는 느낌을 받았다.

lumen logo

이 포스트는 lumen 문서에서 쉽게 볼 수 있는 부분만 다뤘고 더 깊은 내용을 보고 싶다면 Lumen 공식 문서를 보는게 도움이 된다. Lumen는 PHP >= 5.4를 요구하며 Mcrypt, OpenSSL, mbstring, tokenizer 확장을 필요로 한다.

Lumen 설치

lumen을 설치하기 위해서는 composer가 설치되어 있어야 한다.

lumen을 사용해 프로젝트를 시작하는 방법은 lumen installer를 사용하는 방법과 composer의 create-project로 생성하는 방법이 있다. 결과물은 동일한데 installer 속도가 더 빠르다.

composer.json, phpunit.xml 등 단순한 스캐폴딩을 함께 제공한다.

Lumen Installer

다음 명령어로 Lumen Installer를 설치한다. 이 installer는 커맨드라인 환경에서 lumen 프로젝트를 시작할 수 있도록 기능을 제공한다.

composer global require "laravel/lumen-installer=~1.0"

설치가 완료되면 lumen new 명령어로 프로젝트를 생성할 수 있다. 여기서 helloworld라는 이름으로 프로젝트를 생성했다.

lumen new helloworld

다음과 같이 프로젝트가 생성된 것을 확인할 수 있다.

lumen init

composer

위 인스톨러를 사용할 수 없다면 composer create-project 생성할 수 있다.

composer create-project laravel/lumen --prefer-dist

설정하기

lumen은 laravel과 다르게 모든 설정을 .env에 저장한다. 쉽게 활용할 수 있도록 .env.example 템플릿이 제공된다. 데이터베이스, 캐시, 큐, 세션과 관련한 설정값을 지정할 수 있다. 설정에서 가장 먼저 할 일로 해당 템플릿을 열어 APP_KEY에 32자 무작위 문자열을 입력한 후 .env로 저장한다.

URL 설정

Apache 환경을 사용하고 있다면 public/.htaccess를 활용할 수 있고 Nginx에서는 다음과 같이 설정할 수 있다.

location / {
    try_files $uri $uri/ /index.php?$query_string;
}

HTTP 라우팅

라우팅 기초

route는 app/Http/routes.php에 작성한다. 여타 micro-Framework과 크게 다르지 않은 문법이다.

$app->get('/', function() {
  return 'Hello World';  
});
$app->post('foo/bar', function() {
  // do something
});
$app->patch('foo/bar', function() {
  // do something
});
$app->put('foo/bar', function() {
  // do something
});
$app->delete('foo/bar', function() {
  // do something
});

이렇게 생성한 route에서 URL을 다른 곳에서 사용하고 싶다면 url 헬퍼를 쓴다.

$list_link = url('foo');

router에서 파라미터는 다음과 같이 사용한다.

$app->get('user/{id}', function($id) {
  return 'User '.$id;
});

$app->get('user/{name:[A-Za-z]+}', function($name) {
  // do something
  // 이 문법은 라라벨과 호환되지 않는다.
});

컨트롤러와도 쉽게 연결할 수 있다.

$app->get('user/{id}', 'UserController@showProfile');

위 route와 같이 복잡한 URL 구조를 가지고 있다면 route에 as로 별칭을 지정해 쉽게 활용할 수 있다.

$app->get('user/profile', ['as' => 'profile', function() {
  // show user profile
}]);

$app->get('user/dashboard', [
  'as' => 'dashboard',
  'uses' => 'UserController@showDashboard'
]);

이제 위 route는 profile이라는 이름으로 활용할 수 있다. 파라미터가 있는 경우는 두번째 파라미터에 array로 값을 넣으면 된다.

$url = route('profile');
$redirect = redirect()->route('profile');

$profile_url = route('profile', ['id' => 1]);

라우팅 그룹 묶기

route를 그룹으로 묶어 미들웨어나 네임스페이스를 지정할 수 있다. 여기에서 $app->group()를 활용한다.

Closure를 기반으로 한 미들웨어는 다음과 같이 사용할 수 있다.

$app->group(['middleware' => 'foolbar'], function($app) {
  $app->get('/', function() {
    // do something
  });
  $app->get('user/profile', function() {
    // do something
  });
});

namespace로 특정 네임스페이스에 있는 컨트롤러를 불러 활용할 수 있다.

$app->group(['namespace' => 'Admin'], function() {
  // "App\Http\Controllers\Admin" 네임스페이스에 있는 컨트롤러
});

HTTP 예외 발생하기

abort 헬퍼를 이용한다. 응답을 같이 보내줄 수도 있다.

abort(404);
abort(403, 'Unauthorised action.');

이 헬퍼는 상태 코드와 함께 Symfony\Component\HttpFoundation\Exception\HttpException 예외를 던진다.

뷰는 resources/views에 php 파일로 저장한다.

<!-- View stored in resources/views/greeting.php -->

<!doctype html>
<html>
    <head>
        <title>Welcome!</title>
    </head>
    <body>
        <h1>Hello, <?php echo $name; ?></h1>
    </body>
</html>

다음과 같이 사용할 수 있다.

$app->get('/', function() {
  return view('greeting', ['name' => 'James']);
});

$app->get('/admin', function() {
  /* ... */

  // resources/views/admin/dashboard.php
  return view('admin.dashboard', $data);
});

데이터 바인딩을 배열로 넘길 수 있지만 with 메소드나 매직 메소드를 활용할 수도 있다.

$view = view('greeting')
          ->with('name', 'Edward')
          ->with('age', 20)
          ->withLocation('Jeju'); // 매직 메소드

컨트롤러

규모가 커지면 routes.php 파일 하나로만 로직을 다루는 것보다 컨트롤러를 활용해서 구조화하는 것이 낫다. 컨트롤러로 HTTP 요청을 조작하는 로직을 쉽게 묶을 수 있다. 컨트롤러는 app/Http/Conterollers에 저장한다.

컨트롤러는 App\Http\Conterollers\Controller 기초 클래스를 필수적으로 상속해야 한다. 기본적인 컨트롤러는 다음과 같다.

<?php namespace App\Http\Controllers;

use App\User;
use App\Http\Controllers\Controller;

class UserController extends Controller {

    /**
     * Show the profile for the given user
     *
     * @param  int  $id
     * @return Response
     */
    public function showProfile($id)
    {
        return view('user.profile', ['user' => User::findOrFail($id)]);
    }

}

UserController를 라우터에서 다음과 같이 연결할 수 있다.

$app->get('user/{id}', 'App\Http\Controllers\UserController@showProfile');

의존성 주입

Lumen과 Laravel의 서비스 컨테이너는 타입 힌트를 통해 의존성을 해결해준다. 생성자 주입, 메소드 주입 둘 다 사용 가능하다. 다음 코드는 생성자의 타입 힌트 UserRepository, store 메소드의 타입힌트 Request로 의존성이 주입되는 예제다.

<?php namespace App\Http\Controllers;

use App\Http\Controllers\Controller;
use App\Repositories\UserRepository;
use Illuminate\Http\Request;

class UserController extends Controller {

    /**
     * The user repository instance.
     */
    protected $users;

    /**
     * Create a new controller instance.
     *
     * @param  UserRepository  $users
     * @return void
     */
    public function __construct(UserRepository $users)
    {
        $this->users = $users;
    }

    /**
     * Store a new user.
     *
     * @param  Request  $request
     * @return Response
     */
    public function store(Request $request)
    {
        $name = $request->input('name');
    }

}

미들웨어

HTTP 미들웨어는 HTTP 요청과 응답을 제어할 수 있도록 돕는 구조로 micro-Framework에서는 흔히 사용되고 있다. (Python의 uWSGI, .Net의 OWIN) 미들웨어는 app/Http/Middleware에 위치한다.

미들웨어를 활용하기 위해 구성해야 하는 메소드는 handle이다.

<?php namespace App\Http\Middleware;

class OldMiddleware {

    /**
     * Run the request filter.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        if ($request->input('age') < 200) {
            return redirect('home');
        }

        return $next($request);
    }

}

미들웨어로 HTTP 요청과 응답을 모두 제어할 수 있는데 $next($request)를 기준으로 그 앞에서는 요청을 제어하고 뒤에서는 응답을 제어할 수 있다.

다음은 요청을 제어하는 미들웨어로 이 내용을 실행한 후 어플리케이션에 접근하게 된다.

<?php namespace App\Http\Middleware;

class BeforeMiddleware implements Middleware {

    public function handle($request, Closure $next)
    {
        // Perform action

        return $next($request);
    }
}

다음은 응답을 제어하는 미들웨어 예시로 어플리케이션에서 처리가 끝난 후 클라이언트에게 전달되는 응답을 제어할 수 있다.

<?php namespace App\Http\Middleware;

class AfterMiddleware implements Middleware {

    public function handle($request, Closure $next)
    {
        $response = $next($request);

        // Perform action

        return $response;
    }
}

Service Provider와 Service Container

Lumen은 Service ProviderService Container로 기초를 구성하고 있다. Service Provider는 어플리케이션을 시작하기 전에 준비해야 할 작업을 처리할 수 있다. bootstrap/app.php를 열어보면 $app->register() 메소드를 확인할 수 있는데 이 메소드를 통해 추가적인 service provider를 등록할 수 있다.

Service Container는 클래스 간의 의존성을 해결하기 위한 도구로 생성자 또는 “setter” 메소드를 통해 의존성을 주입해준다. $app->bind(), $app->singleton()을 통해 resolver를 등록할 수 있다. 컨테이너에서 의존성을 주입 받기 위해서는 $foobar = $app->make('FooBar'); 방식으로 make 메소드를 사용하는 방법이 있고, 앞서 살펴본 방식인 생성자나 개별 메소드에서 타입 힌팅을 이용해 의존성을 주입할 수 있다. 자세한 내용은 각 문서를 참고하자.


Micro-framework지만 그 말이 무색할 만큼 현대적인 PHP 개발에서 필요한 필수적인 요소는 모두 포함된 강력함을 보여주고 있다. 이 포스트에서 자세하게 다루진 않았지만 많은 서비스도 제공하고 있으며 기존에 laravel을 사용하고 있다면 더 간편하게 사용할 수 있을 것이라 본다. 지금까지 나온 micro-framework 중에서는 가장 마음에 드는 구성이라 차기 프로젝트에서 사용하는 것을 생각해보고 있다. 앞으로 기대가 많이 되는 프레임워크다.

  • PHP 기반의 Micro Frameworks 정리 
  • PHP 네임스페이스

    PHP에서 네임스페이스 문법 소개. `namespace`, `use` 사용하기.

    2015년 4월 15일

    PHP 5.3에서 새로운 기능으로 네임스페이스가 추가되었다. (= 이미 오래된 기능이다.) 많은 현대 언어는 이미 이 기능을 추가한지 오래지만 PHP는 조금 늦게 추가되었다. 최근에 개발되는 대다수의 PHP 라이브러리는 네임스페이스로 패키징해 composer, League 등을 통해 제공되고 있어 현대 PHP를 사용하려고 한다면 필수적으로 알아야 하는 기능이다.

    PHP에서는 같은 이름을 가진 두 클래스를 동시에 사용할 수 없다. 클래스는 항상 유일해야만 한다. 이 제한으로 인해 서드파티 라이브러리에서 User라는 클래스명을 사용하고 있을 때는 User라는 클래스명을 사용할 수 없었다. 이렇게 간단한 클래스명을 사용하지 못하는건 불편하다.

    PHP 네임스페이스는 위와 같은 클래스명 중복 문제를 해결한다. 그 뿐 아니라 코드를 패키징하거나 벤더명을 지정해 소유권을 표시하는데도 사용할 수 있다.

    전역 네임스페이스

    여기 간단한 클래스가 있다.

    <?php
    class Edward
    {
    
    }

    별로 특별한 부분이 없다. 다음과 같이 사용할 수 있다.

    <?php
    
    $edward = new Edward();

    이 상황에서 이 클래스는 전역 네임스페이스를 가졌다고 볼 수 있다. 즉 이 클래스는 네임스페이스 없이 존재한다. 그냥 일반 클래스와 같다.

    단순한 네임스페이스

    이제 네임스페이스 밑으로 클래스를 만들어보자.

    <?php
    namespace Haruair;
    
    class Edward
    {
    
    }

    위에서 만든 클래스와 유사하지만 작은 차이가 있다. namespace 라는 지시문이 추가되었다. namespace Haruair;는 여기서 작성한 모든 PHP 코드가 Haruair 네임스페이스와 관련이 되어 있음을 뜻하고 이 파일에서 생성한 클래스가 모두 Haruair 네임스페이스에 포함되어 있음을 뜻한다. 네임스페이스를 통해 클래스를 생성하면 다음과 같이 사용할 수 있다.

    <?php
    $edward = new Haurair\Edward();

    위 코드와 같이 네임스페이스와 함께 클래스를 선언할 수 있다. 네임스페이스와 클래스 사이에는 백슬래시()로 구분이 된다. 위와 같은 방법으로 네임스페이스로 클래스를 다룰 수 있다.

    이와 같은 방법으로 여러 단계의 위계를 활용하고 있는 경우를 많이 볼 수 있다.

    This\Namespace\And\Class\Combination\Is\Silly\But\Works

    의존성 원칙

    PHP는 현재의 namespace에 따라 상대적으로 동작한다.

    <?php
    namespace Haruair;
    
    $edward = new Edward();

    Haruair 네임스페이스 내에서 개체를 생성했다. 동일한 네임스페이스에 속해 있는 상황이기 때문에 Haurair\EdwardEdward로 호출해 사용할 수 있다.

    이런 상황에서 반대로 생각해볼 수 있는 부분은 네임스페이스 내부에서 상위 또는 최상위에 있는 네임스페이스나 클래스는 어떻게 접근할 지에 대해서다.

    PHP는 클래스명 앞에 백스래시()를 넣어 전역 클래스 또는 글로벌 네임스페이스를 사용하고 있음을 명시적으로 선언할 수 있다.

    <?php
    $edward = new \Edward();

    만약 다른 네임스페이스에 속한 클래스인 Drink\CokeHaruair 네임스페이스 내에서 사용한다면 앞서 예제와 같이 작성할 수 있다.

    <?php
    namespace Haruair;
    
    $coke = new \Drink\Coke();

    매번 전체 위계를 입력하는 것이 번거롭다면 use를 활용할 수 있다.

    <?php
    namespace Haruair;
    
    use Drink\Coke;
    
    $coke = new Coke();

    use를 활용하면 다른 네임스페이스에 있는 하나의 클래스를 현재 네임스페이스 내에서 사용할 수 있게 해준다. 동일한 클래스명을 불러오게 되는 경우가 온다면 다음과 같이 활용할 수 있다.

    <?php
    namespace Haruair;
    
    use Drink\Pepsi as BlueCoke;
    
    $pepsi = new BlueCoke();

    위와 같이 as 키워드를 쓰면 Drink\Pepsi 클래스에 별칭 BlueCoke를 지정해 사용할 수 있다. 같은 이름의 클래스 여럿을 동시에 사용한다 해도 문제 없다.

    <?php
    namespace Facebook;
    
    use Twitter\User as TwitterUser;
    
    class User {}
    
    $twitter_user = new TwitterUser();
    $facebook_user = new User();

    Twitter 네임스페이스에 있는 UserTwitterUser 별칭으로 불러오면서 충돌을 회피했다. 이와 같이 충돌을 피하고 의도와 필요에 따라 기능을 모아서 사용할 수 있다.

    use는 필요한 만큼 넣어서 사용할 수 있다.

    <?php
    namespace Haruair;
    
    use Twitter\Follower;
    use Facebook\WallPost;
    use Cyworld\WallPost as CyPost;

    구조

    네임스페이스는 단순히 충돌을 피하기 위해서만 사용하는 것이 아니라 조직이나 소유권을 표기하기 위해 사용하기도 한다.

    오픈소스 라이브러리를 만든다고 가정하자. 내가 만든 코드를 다른 사람이 사용한다면 분명 좋을 것이다. 다만 내 코드를 사용하는 사람들에게 불편함을 주지 않았으면 좋겠다. 클래스명이 충돌하게 되면 엄청나게 불편할 것이 확실하다. 그래서 다음과 같이 네임스페이스를 구분하기로 했다.

    Haruair\Blog\Content\Post
    Haruair\Blog\Content\Page
    Haruair\Blog\Tag

    여기서 내 아이디를 사용해서 이 코드가 누가 만들었는지 표시하는 것과 동시에 내 라이브러리 안에 만들어 코드를 사용하고자 하는 사람의 코드와 충돌하지 않도록 돕는다. 내 기초 네임스페이스에 여러개의 서브 네임스페이스를 만들어 내부 구조를 잡았다.

    Composer를 사용하면 PSR-0, PSR-4를 통해 정해진 규칙에 따라 네임스페이스를 통해 클래스 정의를 자동으로 불러오는 등의 작업을 할 수 있다. 위 두 문서에서 이 유용한 방식을 확인해보는 것을 강력하게 추천한다.

    제한

    PHP가 제공하는 네임스페이스에는 한계가 있다. 다른 언어들에서의 구현과는 거의 유사하지만 약간 다른 점이 존재한다. Java의 경우, wildcard(*)를 이용해 해당 네임스페이스에 속해 있는 모든 클래스를 한번에 불러올 수 있다. 또한 Java에서의 import는 앞서 살펴 본 use와 같은 역할을 해서 패키지나 네임스페이스 내에 있는 클래스를 쉽게 이용할 수 있게 돕는다. 다음은 Java의 예시다.

    import haruair.blog.*;

    위와 같은 코드로 haruair.blog의 모든 패키지를 로드할 수 있다.

    PHP는 이와 같은 방법으로 불러올 수 없다. 대신 상위의 네임스페이스를 use로 불러와 유사하게 사용할 수 있다.

    <?php
    namespace weirdmeetup;
    
    use Haruair\Blog as Cms;
    
    $post = new Cms\Content\Post;
    $page = new Cms\Content\Page;
    $tag = new Cms\Tag;

    한 네임스페이스에서 많은 클래스를 동시에 사용할 때 위 방법이 도움이 된다.

    더 읽을 거리

    새로운 PHP 패키지를 작성하는 올바른 방법

    PHP 패키지가 준수해야 할 점, League Skeleton과 phpcs 활용하기

    2015년 4월 6일

    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": "bruno@skvorc.me",
          "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 Swader\Diffbot;
    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 \Swader\Diffbot\SkeletonClass();
    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 Swader\Diffbot;
    use Swader\Diffbot\Exceptions\DiffbotException;
    
    /**
     * Class Diffbot
     *
     * The main class for API consumption
     *
     * @package Swader\Diffbot
     */
    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 Swader\Diffbot\Exceptions;
    
    /**
     * Class DiffbotException
     * @package Swader\Diffbot\Exceptions
     */
    class DiffbotException extends \Exception
    {
    
    }
    ?>

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

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

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

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

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

    결론

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

    Terminal에서 MAMP의 php 사용하기

    2015년 1월 2일

    요즘 대부분의 php 도구들이 콘솔에서 사용할 수 있도록 제공되고 있다. OSX에는 기본적으로 php가 설치되어 있고 별다른 설정이 없다면 이 php를 사용하게 된다. composer 같은 도구는 php 버전이나 모듈과는 큰 영향이 없어서 기본 설치 과정을 따라해도 큰 문제가 없지만 데이터베이스 연결이 필요하거나 하는 경우에는 문제가 발생할 수 있다.

    MAMP 환경을 설치해서 개발에 사용하고 있다면 이 MAMP에서 사용하고 있는 php를 간단하게 연결해서 활용할 수 있다.

    이와 관련해서 검색해보면 직접 컴파일해서 패키지를 설치하라거나 mamp-php 등의 이름으로 심볼릭 링크를 연결해주는 등 여러 방법이 있었는데 다 장단점이 있었고 가장 간단하고 별 문제 없는 방식이 기존 설치되어 있는 php를 mamp 안에 있는 php로 심볼릭 링크를 생성해주는 방법이었다.

    먼저 php가 설치되어 있는 위치를 찾는다.

    $ which php
    /usr/local/opt/php56/bin/php

    내 경우는 brew로 php5.6을 설치해 경로가 다른데 /usr/local/bin/php 쯤 될 것이다. 파일명을 변경하고 심볼릭 링크를 생성한다.

    $ cd /usr/local/opt/php56/bin/
    $ mv php php_backup
    $ ln -s /Applications/MAMP/bin/php/php<사용하는 php version>/bin/php php

    이렇게 변경하면 기본 php를 mamp에서 사용하는 php로 사용할 수 있다.


    2015년 1월 20일 추가.

    환경변수를 추가해주는 방법도 있었다. (왜 이걸 생각하지 못했는지 ㅠㅠ) 다음 두 값을 zsh 사용자는 .zshrc, bash 사용자는 .bash_profile에 추가하면 된다.

    export MAMP_PHP=/Applications/MAMP/bin/php/php<사용하는 php version>/bin
    export PATH="$MAMP_PHP:$PATH"

    환경변수에 선언되어 있는 순서대로 명령어를 실행할 수 있는 프로그램이 있는지 확인한다. 정상적으로 연결되어 있는지는 앞서 사용했던 which 명령어로 확인할 수 있다.

    PHP 클래스 자동으로 불러오기 (Autoloading)

    2014년 9월 15일

    객체 지향 프로그래밍에 익숙한 개발자라면 하나의 파일에 하나의 클래스를 작성하는 방식에 익숙할 것이다. 다만 php는 다른 언어와 같이 라이브러리를 일괄적으로 불러오는 방법이 없어 위와 같은 접근 방법으로는 require 또는 include를 이용해 수많은 단일 파일을 불러들여야만 했었다.

    PHP5에서는 클래스 또는 인터페이스 등을 호출했을 때 해당 파일을 자동으로 불러올 수 있도록 여러 함수를 제공한다. 먼저 __autoload 함수를 이용한 예제다.

    <?php
    function __autoload($className){
      include $className . '.php';
    }
    
    $foo = new Foo();
    $bar = new Bar();
    ?>

    위와 같이 함수를 선언하면 new Foo()와 같이 클래스를 사용하는 순간 해당 클래스명으로 __autoload 함수가 실행, Foo.php 파일을 include한다.

    다만 __autoload 함수는 spl_autoload_register 함수를 통해 대체될 수 있기 때문에 권장되지 않는다. spl_autoload_register는 다음과 같이 사용한다.

    <?php
    function my_autoloader($className){
      include 'classes/' . $className . '.class.php';
    }
    
    spl_autoload_register('my_autoloader');
    ?>

    PSR-0 Autoloading Standard

    위 함수를 통해 모듈화가 가능하도록 PHP Framework Interop Group(PHP-FIG)에서 PSR-0 Autoloading Standard가 제안되었다. 해당 제안은 다음의 규약을 포함하고 있다.

    • 네임스페이스와 클래스명으로의 자격을 갖추기 위해서는 다음의 구조를 따라야 한다. \<Vendor Name>\(<Namespace>\)*<Class Name>
    • 각 네임스페이스는 최상위 네임스페이스를 가져야 한다. (“Vendor Name”)
    • 각 네임스페이스는 필요에 따라 서브 네임스페이스를 가질 수 있다.
    • 각 네임스페이스 구분자는 파일 시스템에서 해당 파일을 불러오기 위한 디렉토리 구분자로 사용된다.
    • 클래스명에 들어있는 _ 글자도 디렉토리 구분자로 사용된다. 네임스페이스에서의 _는 특별한 의미가 없다.(PEAR 구현을 포함)
    • 완전한 네임스페이스와 클래스는 파일 시스템에서 불러올 때 .php를 접미어로 붙여 불러온다.
    • 알파벳으로 구성된 벤더명, 네임스페이스, 클래스명은 대소문자를 구분한다.

    위 규약에 따른 예제는 PSR-0 문서에서 제공되고 있으며 SplClassLoader 구현도 해당 문서에서 확인할 수 있다.

    다음은 문서에서 제공되는 autoload 함수 예제다.

    <?php
    function autoload($className) {
      $className = ltrim($className, '\\');
      $fileName = '';
      $namespace = '';
      if($lastNsPos = strpos($className, '\\')) {
        $namespace = substr($className, 0, $lastNsPos);
        $className = substr($className, $lastNsPos + 1);
        $fileName = str_replace('\\', DIRECTORY_SEPARATOR, $namespace) . DIRECTORY_SEPARATOR;
      }
      $fileName .= str_replace('_', DIRECTORY_SEPARATOR, $className) . '.php';
      require $fileName;
      }
    }
    ?>

    Composer 활용하기

    Composer는 PSR-4 Autoloader 제안과 함께 위에서 살펴본 PSR-0를 준수하고 있어서 간단한 설정으로 PSR-0 방식을 사용할 수 있다. composer.json에 autoload 경로를 등록하면 composer의 ClassLoader와 맵핑되어 자동으로 불러온다.

    {
      ...
      "autoload": {
        "psr-0": {"": "<path>/"}
      },
      ...
    }

    위 내용을 추가한 후 composer update 등을 통해 갱신하면 vendor/composer/autoload_namespaces.php 파일 안에 composer.json에서 작성한 경로가 추가된 것을 확인할 수 있다.

    더 읽을 거리

    MAMP에서 Sublime Text로 Xdebug 사용하기

    2014년 7월 15일

    PHP를 디버깅하기 위해서는 Xdebug와 같은 확장을 서버에 설정해야 하고 리모트로 디버깅 하기 위한 클라이언트 프로그램이 요구된다. 이 글에서는 서버로 MAMP를 활용하며 클라이언트로 Sublime text를 활용한다.

    Xdebug 활성화하기

    MAMP에는 이미 Xdebug가 포함되어 있기 때문에 php.ini를 찾아 수정해주면 바로 활성화 할 수 있다. php.ini의 위치는 MAMP 기본 설치 시 /Applications/MAMP/bin/php/php<버전>/conf/php.ini에 있다. 해당 파일을 열어 가장 마지막줄로 이동하면 다음과 같이 주석처리 되어 있는 것을 확인할 수 있다.

    [xdebug]
    ;zend_extension="/Applications/MAMP/bin/php/php5.5.10/lib/php/extensions/no-debug-non-zts-20121212/xdebug.so"

    PHP 버전에 따라 위 내용이 다를 수 있다. 해당 확장을 불러올 수 있도록 맨 앞에 ;를 지운다. 그리고 리모트 디버깅을 위해 다음과 같이 내용을 추가해 저장한다.

    [xdebug]
    zend_extension="/Applications/MAMP/bin/php/php5.5.10/lib/php/extensions/no-debug-non-zts-20121212/xdebug.so"
    xdebug.remote_autostart=1
    xdebug.remote_connect_back=1
    xdebug.remote_enable=1
    xdebug.remote_port=9000

    Sublime text에 Xdebug Client 설치 및 디버깅

    Screen Shot 2014 07 15 at 12 19 17 pm

    Cmd + Shift + P 를 눌러 Command Palette를 열고 Install Package를 실행해 Xdebug Client를 설치한다.

    Screen Shot 2014 07 15 at 12 33 03 pm

    설치 후 메뉴에서 Tools > Xdebug > Start Debugging을 실행해 디버깅 세션을 시작한다.

    세션이 정상적으로 시작되었다면 디버깅 하고자 하는 페이지에 다음과 같이 디버깅 쿼리 스트링을 붙여 다시 접속해보면 Sublime Text에서 디버깅이 가능한걸 확인할 수 있다.

    http://localhost/example.php?XDEBUG_SESSION_START=sublime.xdebug

    Screen Shot 2014 07 15 at 12 28 19 pm

    하단 4개의 창에서 Context, Watch, Stack, Breakpoint를 확인할 수 있으며 코드에서 오른쪽 클릭으로 Breakpoint나 watch expression을 추가해 확인할 수 있다.

    OSX에서 사용 가능한 클라이언트

    근래의 대다수 에디터와 IDE에서 기본적으로 내장되어 있어 현재 사용하고 있는 에디터나 IDE에 있는 기능을 먼저 확인해보도록 하자. 다음은 OSX에서 사용 가능한 standalone 클라이언트다.