Laravel 웹 크롤러

spiderman-crawling
Standard

API 를 호출하여 JSON 이나 XML 포맷의 결과를 받아오는 경우가 아니라, HTML 로 구성된 웹페이지를 파싱해서 원하는 데이터 베이스를 구축할 필요가 있는 경우에는 웹크롤링 작업이 필요한데, 이번에는 간단한 웹 크롤러를 만들어본다.

이를 위해 사용한 패키지는 아래의 3가지 패키지이다.

  1. sunra/php-simple-html-dom-parser
  2. egeloen/google-map
  3. willdurand/geocoder

우선 composer.json 에 아래 라인을 추가하고, composer update 명령으로 디펜던시를 불러온다.

"sunra/php-simple-html-dom-parser": "v1.5.0",
"egeloen/google-map": "dev-master",
"willdurand/geocoder": "*",

아래는 골프장 정보를 모업체의 웹페이지 결과로 부터 받아서 추출하는 샘플 내용이다. 키포인트는 sunra/php-simple-html-dom-parser 의 사용으로, DOM element selector 가 jQuery 의 그것과 매우 유사하게 동작하여 매우 쉽게 이용할 수 있다는 것이다. 약간의 regular expression 과의 조합으로 훌륭한 web crawling 이 가능하다. 나머지 2개 패키지는 주소 값을 이용하여 그 위치의 위도/경도 값을 구해오는 geocoding 을 위해서 사용했다.

<?php

use Sunra\PhpSimple\HtmlDomParser;
use Ivory\GoogleMap\Services\Geocoding\Geocoder;
use Ivory\GoogleMap\Services\Geocoding\GeocoderProvider;
use Geocoder\HttpAdapter\CurlHttpAdapter;

class HomeController extends BaseController {

    public function home()
    {
        for($i = 1; $i <= 7; $i++) {
            $this->update($i);
        }
    }

    public function update($club_region)
    {
        $club_type = 3;

        // init the resource
        $url = sprintf("http://gcinfo.xxx.com/gz/searchcc/ajax/area/0000%d/1/1", $club_region);
        $postData = array(
            'pagesize' => '100',
            'membership' => $club_type + 1,
            'facility' => '0',
            'gfeeoption' => '0',
            'gfeefrom' => '0',
            'gfeeto' => '250000',
            'openfrom' => '1960',
            'opento' => '2014'
        );
        $postOutput = $this->postPage($url, $postData);

        $html = HtmlDomParser::str_get_html( $postOutput );
        $elements = $html->find('div[class=onc_cc_data]');

        $idx = 0;
        foreach ($elements as $element) {
            $idx++;

            $club_id = sprintf("%02d%02d%03d", $club_type, $club_region, $idx);
            echo $club_id;
            echo "\n";

            echo $club_type;
            echo "\n";

            echo $club_region;
            echo "\n";

            $href = $element->find('a', 0)->href;

            $pos = strripos($href, '/');
            $webidx = substr($href, $pos + 1);

            $url = 'http://gcinfo.xxx.com'.$href;
            $url2 = 'http://gcinfo.xxx.com/gz/ajax/html/searchcc/home/'.$webidx;
            $url3 = 'http://gcinfo.xxx.com/gz/ajax/html/searchcc/booking/'.$webidx;

            $info = trim($element->find('a', 1)->plaintext);
            $ret = preg_match("/\[(.+)\]\s+(\([A-Z]+\))([^|]+)\s+\|\s+(.+)/", $info, $matches);

            if (!$ret)
                dd('Fatal error. Can\'t match with the regular expression.');

            echo $matches[2];
            echo "\n";

            $region = $matches[1]; 
            echo $matches[1];
            echo "\n";

            $name = $matches[3]; 
            echo $matches[3];
            echo "\n";

            $holes = intval($matches[4]);
            echo intval($matches[4]);
            echo "\n";

            $info = trim($element->find('dd.course_name', 0)->plaintext);
            $info = str_replace(' ', '', $info);
            $courses = ($info) ?: 'n/a';
            echo $courses;
            echo "\n";

            $info = $element->find('dd.infophone', 0)->plaintext;
            $phone = trim(str_replace('문의전화 ', '', $info));
            echo ($phone);
            echo "\n";

            //
            // sub page
            //

            $getOutput = $this->getPage($url);
            $html2 = HtmlDomParser::str_get_html( $getOutput );

            $address = trim($html2->find('div.detail_gc table td', 0)->plaintext);
            echo ($address);
            echo "\n";

            $geocoder = new Geocoder();
            $geocoder->registerProviders(array(
                new GeocoderProvider(new CurlHttpAdapter()),
            ));
            $response = $geocoder->geocode($address);

            $latitude = 0;
            $longitude = 0;

            $results = $response->getResults();
            foreach ($results as $result) {
                $location = $result->getGeometry()->getLocation();
                $latitude = $location->getLatitude();
                $longitude = $location->getLongitude();
            }

            $detail = $html2->find('div.detail_gc a.btn_go_homepage', 0)->href;
            $webpage = ($detail) ?: 'n/a';
            echo $webpage;
            echo "\n";

            //
            // sub page
            //

            $getOutput2 = $this->getPage($url3);
            $html3 = HtmlDomParser::str_get_html( $getOutput2 );

            $weekday_fee = 0;
            $weekend_fee = 0;
            $cart_fee = 0;
            $caddie_fee = 0;

            if ($html3->find('div.greenfee_data td', 0)) {

                $detail = trim($html3->find('div.greenfee_data td', 0)->plaintext);
                $detail = trim(str_replace(',', '', $detail));
                $weekday_fee = intval($detail);
                echo $weekday_fee;
                echo "\n";

                $detail = trim($html3->find('div.greenfee_data td', 1)->plaintext);
                $detail = trim(str_replace(',', '', $detail));
                $weekend_fee = intval($detail);
                echo $weekend_fee;
                echo "\n";

                $detail = trim($html3->find('div.rounding_info li', 0)->plaintext);
                $detail = preg_replace("/\s+/", '', $detail);
                $detail = trim(str_replace('카트피:', '', $detail));
                $detail = trim(str_replace(',', '', $detail));
                $cart_fee = intval($detail);
                echo $cart_fee;
                echo "\n";

                $detail = trim($html3->find('div.rounding_info li', 1)->plaintext);
                $detail = preg_replace("/\s+/", '', $detail);
                $detail = trim(str_replace('캐디피:', '', $detail));
                $detail = trim(str_replace(',', '', $detail));
                $caddie_fee = intval($detail);
                echo $caddie_fee;
                echo "\n";

            } else {
                echo "0\n0\n0\n0\n";
            }

            // write into the DB table here
        }
    }


    public function postPage($url, $data) {
        // init the resource
        $ch = curl_init();

        curl_setopt_array($ch, array(
            CURLOPT_URL => $url,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_POST => true,
            CURLOPT_POSTFIELDS => $data,
            CURLOPT_FOLLOWLOCATION => true
        ));

        $output = curl_exec($ch);

        // free
        curl_close($ch);

        return $output;
    }


    public function getPage($url) {
        // init the resource
        $ch = curl_init();

        curl_setopt_array($ch, array(
            CURLOPT_URL => $url,
            CURLOPT_RETURNTRANSFER => true,
        ));

        $output = curl_exec($ch);

        // free
        curl_close($ch);

        return $output;
    }
}

Laravel 로 웹싸이트 개발하기 #2

devil
Standard

Stapler 패키지의 설명 내용과는 다르게, 여기서는 1-to-1 관계인 User 와 Avatar 를 사용하지 않고, 1-to-many 관계인 User 와 Picture 모델을 정의하여 사용한다. 현재 작업중인 프로젝트의 기술 명세서를 보면 접속한 회원이 다수의 사진을 자유롭게 올릴 수 있어야 한다. Jeffrey Way 의 Generator 를 이용하여 다음과 같이 Picture 모델의 마이그레이션을 생성한다.

php artisan generate:migration create_pictures_table --fields="user_id:integer, title:string:nullable, image_file_name:string, image_file_size:integer, image_content_type:string, image_updated_at:timestamp"

마이그레이션을 하면 DB 는 설정이 되었고, Picture 모델의 정의는 아래와 같이 했다.

<?php namespace Teeshot\Users;

use Codesleeve\Stapler\ORM\StaplerableInterface;
use Codesleeve\Stapler\ORM\EloquentTrait;
use Eloquent;

class Picture extends Eloquent implements StaplerableInterface {

    use EloquentTrait;

    protected $table = 'pictures';
    public $timestamps = true;
    protected $fillable = [ 'image' ];

    /**
     * Constructor
     */
    public function __construct(array $attributes = array()) {
        $this->hasAttachedFile('image', [
            'styles' => [
                'medium' => '320x320#',
                'thumb' => '100x100#'
            ]
        ]);

        parent::__construct($attributes);
    }

    public function user()
    {
        return $this->belongsTo('User');
    }
}

궁극적으론, 뷰에서 아래와 같은 형태의 접근이 가능한 시나리오다.

{{ $user->pictures[index]->image->url() }}
{{ $user->pictures[index]->image->url('thumb') }}

뷰 역시 추가되는 부분이 필요한데, DB 에 이미 기록된 이미지 파일들을 출력하기 위한 내용이다.

<div class="image-list">
    @if (count($pictures) > 0)
        @foreach ($pictures->chunk(4) as $pictureSet)
            <div class="row pictures">
                @foreach ($pictureSet as $picture)
                    <div class="col-sm-3 col-md-3 user-block">
                        <a class="btn btn-danger btn-small" href="#" rel="" rev="{{$picture->id}}"><span class="glyphicon glyphicon-remove" aria-hidden="true"></span></a>
                        <img src="{{ $picture->image->url('thumb') }}" />
                    </div>
                @endforeach
            </div>
        @endforeach
    @else
        <div class="row pictures"></div>
    @endif
</div>
<hr />

해당 뷰 하단에 JavaScript 함수도 아래와 같이 추가 변경한다.

$('#uploader').fileapi({
    url: '{{ route("users.upload") }}',
    accept: 'image/*',
    autoUpload: true,
    multiple: true,
    maxFiles: 5,
    maxSize: 6 * FileAPI.MB,
    imageTransform: { // resize by max side
        maxWidth: 800,
        maxHeight: 600
    },
    elements: {
        empty: { show: '.b-upload__hint' },
        list: '.js-files',
        file: {
            tpl: '.js-file-tpl',
            preview: { el: '.b-thumb__preview', width: 64, height: 64 },
            upload: { show: '.progress' },
            complete: { hide: '.progress' },
            progress: '.js-progress'
        }
    },
    onFileComplete: function (evt, uiEvt){
        var json = uiEvt.result;
        var element = '<div class="col-md-3 user-block"><img src="'+json.name+'"></div>';
        var lastDiv = $(".image-list > div:last");
        var count = lastDiv.children().length;

        if (count < 4) {
            lastDiv.append(element);
        } else {
            var newDiv = $('<div class="row pictures">');
            $(".image-list").append(newDiv);
            newDiv.append(element);
        }
        evt.widget.remove(uiEvt.file);
    }
});

이제 마지막으로 콘트롤러 UsersController 의 upload() 메서드를 다음과 같이 변경하고, 뷰에 필요한 객체 콜렉션을 넘겨줄 edit() 메서드도 추가한다.

public function upload()
{
    $file = Input::file('filedata');

    $picture = new Picture();
    $picture->image = $file;
    $picture->user_id = Auth::user()->id;
    $picture->save();

    return Response::json(['name' => $picture->image->url('thumb')], 200);

}

public function edit()
{
    $user = Auth::user();
    $pictures = $user->pictures()->get();
    return View::make('users.edit', compact('pictures'));
}

지금까지의 작업만으로 프론트앤드를 위한 FileAPI JQuery 라이브러리와 백앤드를 위한 Stapler 패키지가 조합되어 제법 쓸만한 파일 업로드 기능이 동작하게 된다. 이제 /app/config/packages/codesleeve/laravel-stapler 디렉토리에서 stapler.php 파일을 열어서, Stapler 의 스토리지 드라이버를 ‘storage’ => ‘filesystem’ 에서 ‘storage’ => ‘s3’ 로 바꾸면, 그 이후부터의 모든 업로드는 S3로 업로드 된다. 지금까지 구현된 내용의 스크린샷은 아래와 같다.

As simple as that.

Laravel 로 웹싸이트 개발하기 #1

devil
Standard

Laravel 로 웹싸이트 개발하기

2014년 12월 구로동 사무실에서

현재 모바일부터 웹애플리케이션에 이르는 풀스택 개발 프로젝트를 진행하고 있기에, 최신 기술 트랜드를 적용한 부분들 중 일부 내용을 블로그에 써보기로 했다. 웹개발 플랫폼은 Laravel 4.2 프레임웍을 근간으로하여, 온갖 오픈 소스 패키지들과, 유료 3rd party API 들을 최대한 가져다 쓰고 있다. 금번 프로젝트 개발의 백엔드 부분 개발은 물론이며, 프론트엔드 부분까지도 나혼자 스스로 결정해가면서 진행하고 있어서, 그간 안해봤던 내용들을 두루 경험해 볼 수 있는 좋은 시간이 되고 있다. 개를 보기만 하다가 먹어도 보는 그런 느낌? 암튼 그런 묘한… 그런게 있다. 이번 글에서 이야기할 내용은 사용자가 설정 페이지에서 다수의 사진들을 올리고 관리하기 위한, 사용자의 사진을 유지/관리하는데 필요한 내용들이다. 웹 애플리케이션에서 흔히들 많이 구현하는 그런 내용이 되겠다.

우선, 우연히 봤던 이 블로그 (http://revoltvisual.com/journal/5-must-have-laravel-4-packages) 에서 Laravel 5대 패키지 중 하나라고 언급된 Stapler 패키지를 사용한다. 딴 놈이 추천한 패키지를 사용한다는 것만으로도 시간 헛낭비하는게 아닌듯 싶어 뿌듯하다. 이 패키지를 이용해서 User 모델에 Photo 모델을 붙이는 과정을 정리해 보겠다. 특이한 점은, 이미지 파일 저장을 위해서 서버의 파일 시스템을 사용하지 않고, 아마존 S3 를 이용하려는 점이다. 부담스런 서버 호스팅 관리, 효율적인 파일 접근을 위한 디렉토리 구조 생성, 모델간의 연계성 상호 유지 등 관련된 여러가지 고민거리들을 한방에 해결하는 것이 목표다.

한방에 훅

우선 프론트엔드 부에서, 다수의 파일 업로드를 위해 선택한 라이브러리는 mailru/FileAPI 이다. 초기에는 (그래봤자 하루 전 이야기지만 ㅎㅎ) blueimp/jQuery-File-Upload 또는 enyo/dropzone 를 사용할까도 생각해 봤지만, 내가 선호하는 스타일로 커스터마이징에 보다 플렉서블한 FileAPI 데모 페이지를 보고 단숨에 미련없이 결정했다. 존나 멋지다. 근데, 예제의 HTML view 를 보니, 템플릿 엔진도 들어가 있고, 이거 뭐냐 다른 디펜던시가 또 있는건가? 생각하다 한참 헤맸다. 만든 사람도 있는데 셋업을 못한다면 얼마나 쪽팔린 일이냐… 근데 또 다른 디펜던시 그런건 없다. 그냥 예제처럼 쓰면 된다. 늘 그렇듯 오버하면 안된다.

우선 아래 bower 명령으로 jquery 용 FileAPI 를 가져온다.

bower install jquery.fileapi

이제 FileAPI 라이브러리를 이용하는 뷰가 필요하다. 아래는 현재 제작중인 프로젝트에서 사용하는 뷰에서 설명에 필요한 내용 일부를 발췌한 것이다. FileAPI 예제와 비교해 보면, 이 라이브러리의 사용법을 익히는데 많은 도움이 되리라본다.

@extends('layouts.default')

@section('stylesheets')
    <style>
        #uploader .btn {
            cursor: pointer;
            display: inline-block;
            position: relative;
            overflow: hidden;
        }
        #uploader .btn input
        {
            top: -10px;
            right: -40px;
            z-index: 2;
            position: absolute;
            cursor: pointer;
            opacity: 0;
            filter: alpha(opacity=0);
            font-size: 50px;
        }
        #uploader .b-thumb__name {
            font-size: 0.8em;
        }
    </style>
@stop

@section('content')
    <div class="canvas">
        <div class="page-header">
            <div>Title Header</div>
        </div>

        <div class="panel panel-default">
            <div class="panel-heading clearfix">
                <div class="panel-title pull-left">사진</div>
                <button type="button" class="btn btn-default btn-xs pull-right"><span class="glyphicon glyphicon-pencil"></span> 수정</button>
            </div>
            <div class="panel-body">

                <div class="row centered-form">
                    <div class="col-xs-12 col-sm-8 col-md-4 col-sm-offset-2 col-md-offset-4">
                        <div class="panel panel-warning">
                            <div class="panel-heading">
                                <h3 class="panel-title">더많은 사진 올리기</h3>
                            </div>
                            <div class="panel-body">
                                <div id="uploader">

                                  <div class="b-upload__hint">사진을 올리면 주목받을 수 있습니다.</div>
                                  <div class="js-files b-upload__files">
                                     <div class="js-file-tpl b-thumb" data-id="<%=uid%>" title="<%-name%>, <%-sizeText%>">
                                        <div class="b-thumb__preview">
                                           <div class="b-thumb__preview__pic"></div>
                                        </div>
                                        <div class="b-thumb__name"><%-name%></div>
                                        <div class="b-thumb__progress progress progress-striped progress-small active">
                                            <div class="js-progress progress-bar progress-bar-warning"></div>
                                        </div>
                                     </div>
                                  </div>
                                  <hr>
                                  <div class="btn btn-warning btn-small js-fileapi-wrapper">
                                     <span>불러오기</span>
                                     <input type="file" name="filedata">
                                  </div>

                                </div>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        </div>

    </div>
@stop

@section('scripts')

    <script>
    window.FileAPI = {
          debug: true,
          staticPath: '/js/jquery.fileapi/FileAPI/' // path to *.swf
    };
    </script>
    <script src="/js/jquery.fileapi/FileAPI/FileAPI.min.js"></script>
    <script src="/js/jquery.fileapi/FileAPI/FileAPI.exif.js"></script>
    <script src="/js/jquery.fileapi/jquery.fileapi.js"></script>

    <script>
        $(function() {
            $('#uploader').fileapi({
                url: '{{ route("users.upload") }}',
                accept: 'image/*',
                autoUpload: true,
                multiple: true,
                imageTransform: { // resize by max side
                    maxWidth: 800,
                    maxHeight: 600
                },
                elements: {
                    empty: { show: '.b-upload__hint' },
                    emptyQueue: { hide: '.js-upload' },
                    list: '.js-files',
                    file: {
                        tpl: '.js-file-tpl',
                        preview: { el: '.b-thumb__preview', width: 64, height: 64 },
                        upload: { show: '.progress' },
                        complete: { hide: '.progress' },
                        progress: '.js-progress'
                    }
                }
            });
        });
    </script>

@stop

일단, 이번 회에는 서버 파일 시스템의 /upload 폴더에 파일 이름을 난수명으로 변경하여 저장하는 가장 일반적인 이미지 파일 처리 방법에 대해서 설명을 하고, 이후에 Stapler 패키지를 이용해서 /system 폴더에 저장하는 방법, 계속해서 서버의 파일 시스템이 아닌 아마존 S3를 사용하는 방법으로 진화시켜 보겠다. 서버로 올라간 파일에 대한 처리는 아래 컨트롤러의 upload 메서드에서 처리한다. 현재는, 단순히 /uploads 폴더에 난수명을 갖는 파일로 옮겨 저장한다.

<?php

use Teeshot\Users\UserRepository;

class UsersController extends \BaseController {

    /**
     * @var UserRepository
     */
    protected $userRepository;

    /**
     * @param UserRepository $userRepository
     */
    function __construct(UserRepository $userRepository)
    {
        $this->userRepository = $userRepository;
    }

    public function upload()
    {
        $file = Input::file('filedata');

        $destinationPath = 'uploads';
        $filename  = str_random(16);
        $extension = $file->getClientOriginalExtension();
        $size      = $file->getSize();
        $fullName  = $filename.'.'.$extension;
        $upload_success = $file->move($destinationPath, $fullName);

        if( $upload_success ) {
            return Response::json(['name' => $fullName, 'size' => $size], 200);
        } else {
            return Response::json('error', 400);
        }
    }
}

이 방법이 이미지 파일 업로드를 위해서 가장 많이 사용하는 기초적인 방법이 되겠다. 물론 이렇게 사용해도 된다. 하지만, 아까도 말했듯이 고민해야할 문제들을 한방에 훅가도록 처리하기 위해서 Stapler 를 사용해 보자. 당장에 아마존S3 를 이용할 것은 아니지만, 궁극적인 목적이 아마존S3 를 이용하는 것이니, 필요한 내용을 미리 미리 셋업하면서 진행해 가겠다. Stapler 메뉴얼에 미안할 정도로 설명이 잘 나와있다. composer.json 에 다음 2줄을 추가하고 업데이트를 시켜서 해당파일 및 디펜던시들을 설치하면 일단, 백엔드쪽 준비는 끝이다.

"codesleeve/laravel-stapler": "1.0.*",
"aws/aws-sdk-php": "2.7.*"

아마존S3 가입하지 않은 사용자라면, 브라우저를 열어 가입하기로 한다. 아마존 AWS 에 가입 후, S3 메뉴를 선택하고 새로운 bucket을 생성한다. 다음은 IAM 메뉴를 선택하여 새로운 사용자 그룹을 만든다. 이 그룹이 갖을 퍼미션은 S3 에 full access 권한이다. 그 다음은 새로운 사용자를 만들어서 access credentials 를 받는다. php artisan config:publish codesleeve/laravel-stapler 명령으로 생성한 config 파일들 중 s3.php 에 필요한 user credentials 값들이므로 카피해 놓는다. region 값은 역시 Stapler 설명서에 잘 나와 있듯이 http://docs.aws.amazon.com/general/latest/gr/rande.html#s3_region 에서 찾아보면 된다. 사용자를 생성한 뒤에는 반드시 그 사용자 그룹을 바로 전단계에서 생성한 사용자 그룹으로 설정시킨다. 이로써 이 아마존 S3 에 full access 권한을 갖는 사용자가 탄생하게 되었다.

다음 회에서는 Stapler 를 사용하여, 지정된 모델과 연동하는 이미지 파일들을 자동으로 생성처리하는 법을 살펴보기로 하겠다.

PHP 로 카카오톡 및 네이버 계정으로 로그인 구현

naver-kakao
Standard

Laravel 로 웹싸이트 개발하기

2014년 11월 역삼동 사무실에서

금번에 진행하는 프로젝트의 회원가입 요구사항 중 하나는 Facebook 이나 Twitter 계정으로 로그인이 아니라, 국내에서 많은 사용자층을 확보한, 카카오톡과 네이버 계정으로 로그인을 이용하는 기능의 추가 이다. 예전에 번역했었던 Laravel 웹 애플리케이션 개발이란 책에서도 사용법이 언급되어 있는 hybridauth 패키지를 이용하면, 이미 구현되어있는 다양한 OAuth/OAuth2 클라이언트들을 쉽게 가져다가 쓸 수 있는데, 이번에 그 패키지를 확장해서, 카카오톡과 네이버 계정으로 로그인 기능을 이용할 수 있는 클래스를 작성했습니다. 필요하신 분은 https://github.com/jinseokoh/additional-providers 에서 다운로드 하기 바랍니다.

kakaonaver

Laravel Application Development Cookbook

laravel-cookbook
Standard

Laravel Application Development Cookbook

One of the nicest things to read a well written cookbook is that the author’s hands-on expertise will rub off on you with minimum effort. This book is not an exception. It contains 11 chapters which cover almost every aspect of making modern websites. You can’t learn all the details you’ll ever need to know by reading a single book but, this could rather be a great starting point. Even though it’s a cookbook with various topics and solutions. The step by step approach begining from installation to more complex tasks makes it easy to read and follow. I’ve learned many great online resources that I wasn’t aware of.

I’m hoping that my next project can leverage some of those. Maybe there’re other aspects of learning and using frameworks like this. Things like how to implement Single Responsibility Principle, and how to refactor your code with intent. That’s not covered with it. Don’t bother! There’re better books for that. This book stands out when you need a seasoned programmer’s code recipes for the things you are working on or planning to do. As a matter of fact, I’m officially translating this book into Korean. So my fellow Korean PHP programmers can also leverage this great framework for their next projects. I could be a big time Laravel evangelist here in Korea then. :) I’d give it 4 out of 5.

Mailers

Standard

These are Jeffrey’s suggested way of organizing Mailer class in L4.

app/mailers/Mailer.php

[php]
<?php namespace SagageMailers;

use Mail;

abstract class Mailer {

public function sendTo($user, $subject, $view, $data = [])
{
Mail::queue($view, $data, function($message) use($user, $subject)
{
$message->to($user->email)
->subject($subject);

});
}

}
[/php]

(tip: always use Mail::queue even if you haven’t deployed any queue implementation.)

app/mailers/UserMailer.php

[php]
<?php namespace SagageMailers;

use User;

class UserMailer extends Mailer {

public function welcome(User $user)
{
$view = ’emails.welcome’;
$data = []; // username, etc.
$subject = ‘Welcome to sagage.com’;
return $this->sendTo($user, $subject, $view, $data);
}

public function cancel(User $user)
{
$view = ’emails.welcome’;
$data = [];
$subject = ‘Sorry to see you go’;
return $this->sendTo($user, $subject, $view, $data);
}

}
[/php]

app/routes.php

[php]
<?php
Route::resource(‘users’, ‘UsersController’);
[/php]

$ php artisan generate:controller UsersController

app/controllers/UsersController.php

[php]
use SagageMailersUserMailer as Mailer;
use User;

class UsersController extends BaseController {
protected $mailer;

public function __construct(Mailer $mailer)
{
$this->mailer = $mailer;
}

public function create()
{

return View::make(‘users.create’);
}

public function store()
{
$user = User::find(1);
$this->mailer->welcome($user);
}
}
[/php]

add the following line in composer.json

[php]
"autoload": {
"classmap": [
:
"app/mailers"
]
}
[/php]

$ composer dump
$ php artisan generate:migration create_users_table –fields=”email:string:unique, password:string”
$ php artisan migrate
$ php artisan tinker

[php]
$user = new User;
$user->email = ‘user@sagage.com’;
$user->password = Hash::make(‘1234’);
$user->save();
dd(User::first()->toArray());
[/php]

deployment tips

Standard

ref) http://laravel.com/docs/configuration

as for the question of how to archive environmental consistency? a simple answer would be using environment variables;

for production DB setup,
app/config/database.php

[php]

‘connections’ => array(

‘sqlite’ => array(
‘driver’ => ‘sqlite’,
‘database’ => __DIR__.’/../database/production.sqlite’,
‘prefix’ => ”,
),

‘mysql’ => array(
‘driver’ => ‘mysql’,
‘host’ => getenv(‘DB_HOST’),
‘database’ => getenv(‘DB_NAME’),
‘username’ => getenv(‘DB_USER’),
‘password’ => getenv(‘DB_PASS’),
‘charset’ => ‘utf8’,
‘collation’ => ‘utf8_unicode_ci’,
‘prefix’ => ”,
),

[/php]

create a folder that matches the environment name you want to correspond to. then those will take precedence. for instance, to overwrite local development environments, edit app/config/development/database.php

[php]

<?php

return [
‘connections’ => array(

‘mysql’ => array(
‘driver’ => ‘mysql’,
‘host’ => ‘localhost’,
‘database’ => ‘project’,
‘username’ => ‘root’,
‘password’ => ‘1234’,
‘charset’ => ‘utf8’,
‘collation’ => ‘utf8_unicode_ci’,
‘prefix’ => ”,
)
)
];

[/php]

So, how do we reference these? you can specify ’em by editing bootstrap/start.php

[php]

<?php

$env = $app->detectEnvironment(array(

‘development’ => array(‘your-machine-name’),

));

[/php]

or,

[php]

<?php

$env = $app->detectEnvironment(function()
{
return getenv(‘APP_ENV’) ?: ‘development’;
});

[/php]

app/config/stripe.php

[/php]
‘production-api-key’
]

[/php]

app/config/development/stripe.php

[/php]
‘test-api-key’
]

[/php]

Laravel 4

Standard

laravel

I’ll continuously write a series of tech tips and found outs related to Laravel 4 framework on my blog esp. for my fellow Korean developers. That’s largely based on Jeffrey Way’s video tutorial. Plus, a bunch of books and blog postings available online. Like it said, you’ve just arrived in the right place, at the right moment if you follow me.

These are a handful of interesting L4 related web-sites I keep an eye on. (I’ll continue updating the list.)