Skip to main content

49 posts tagged with "php"

View All Tags

Laravel - Log::debug vs logger

· One min read

로그를 찍기 위해서 Log 파사드 또는 logger 헬퍼 함수를 사용할 수 있다.

두 함수의 차이점은 무엇일까?

Log::debug

  • Log 파사드가 정의되어있어야 한다. use Log;
  • NULL 값이 들어올시 NULL이 로그에 출력된다.

logger

  • 헬퍼함수라 Log 파사드 없이 사용 가능하다.
  • NULL 값이 들어올시 아무 내용도 찍히지 않는다.
  • debug 레벨이 기본이라, 다른 레벨의 로그를 찍으려면 logger()->error('에러 로그'); 처럼 함수를 하나 더 호출해야한다.
  • 단, info 레벨 로그는 info(); 헬퍼 함수를 사용할 수 있다.

Laravel 5.5 - 일본으로 메일 보내기

· 3 min read

일본 통신사 중 소프트뱅크와 AU는 UTF8 메일이 깨져서 들어간다. (도코모는 정상적) Laravel 뿐아니라 일본으로 메일을 보내고 싶다면, 이 포스팅의 방식대로 접근하면 된다.

iso-2022-jp

먼저 인코딩을 iso-2022-jp로 바꿔줘야한다.

테스트 결과 JIS 인코딩과 같다. SJIS 인코딩도 있는데 위 인코딩에 몇가지 특수문자가 추가된 형태이다.

AppServiceProvider에 메일 인코딩을 전역으로 설정하자.

app/Providers/AppServiceProvider.php
<?php
...
use Swift;
use Swift_DependencyContainer;
use Swift_Preferences;

class AppServiceProvider extends ServiceProvider
{
...
public function register() {
// laravel의 메일 패키지는 Swift이므로 당황하지말자
Swift::init(function() {
// 헤더를 추가하고
Swift_DependencyContainer::getInstance()
->register('mime.qpheaderencoder')
->asAliasOf('mime.base64headerencoder');
// 인코딩을 변경한다.
Swift_Preferences::getInstance()->setCharset('iso-2022-jp');
});
}
}

7bit

위키를 참조해보면 iso-2022는 문자열을 7bit 또는 8bit로 표현하는 기술이다. 근데, 소프트뱅크 문서에 7bit로 달란다. 메일 폼을 열어서 build시에 인코딩 바이트를 변경하자.

app/Mail/YourMailForm.php
<?php
...
use Swift_Mime_ContentEncoder_PlainContentEncoder;

class YourMailForm extends Mailable
{
...
public function build() {
return $this->subject('ご注文を承りました。')
->view('mail.your_mail_view')
->withSwiftMessage(function($message) {
// 전역으로 설정한 뒤에 이 메소드를 빼보고 테스트를 못 해봤다.
// 다음 분이 빼고 테스트 부탁드려요 : )
$message->setCharset('iso-2022-jp')
// 인코딩 바이트를 바꿔준다.
->setEncoder(new Swift_Mime_ContentEncoder_PlainContentEncoder('7bit'));
});
}
}

mail.view

가장 중요한 것은 Mail View에서도 charset meta tag를 등록해줘야 한다는 것이다. 이 걸 세팅안하고 얼마나 삽질을 해댔는지, 없던 이두근이 생길지경.

resources/view/mail/your_mail_view.blade.php
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=iso-2022-jp">
</head>
<body>
...
</body>
</html>

이렇게 세팅을 하고 메일을 보내면 정상적으로 보내지는 걸 확인할 수 있다.

여담

세 가지만 기억하자.

  • iso-2022-jp
  • 7bit
  • meta[charset="iso-2022-jp"]

Laravel 5.5 - 로그인(Auth) 붙히기

· 11 min read

이전 포스팅에서 데이터를 가져왔으니, 로그인을 구현해보자.

Basic Auth

라라벨에서 제공하는 회원가입, 로그인, 비밀번호 찾기 기능을 사용하고 싶다면 artisan 명령어로 간단하게 시작할 수 있다.

php artisan make:auth

명령어를 실행하면 resources/views/auth 폴더에 뷰가 app/Http/Controllers/Auth에 로직이 생성되고 routes/web.php에 라우팅이 등록된다. DB도 같이 만들고 싶다면 Docs를 따라하자.

다른 테이블 사용

제공된 user 테이블을 안 쓰려면 커스터마이징이 필요하다.

설정 변경

config/auth.php 파일로 이동해 다른 모델을 등록하자.

config/auth.php
<?php
'providers' => [
'users' => [
'driver' => 'eloquent',
// 여기에 사용할 모델을 등록한다.
'model' => App\Models\Member::class,
]
]

config 안의 파일을 변경하면 config:clear를 실행해 혹시 모를 캐시를 날려주자

그리고 app/Models/Member.php로 이동해 내 모델을 라라벨 기본 인증 모듈을 사용할 수 있게 추가해야한다.

app/Models/Member.php
<?
// 라라벨 인증 사용
use Illuminate\Foundation\Auth\User as Authenticatable;
// 비밀번호 변경 메일을 위해 필요한 trait
use Illuminate\Notifications\Notifiable;

// Authenticatable 를 상속받는다.
class Member extends Authenticatable {
// Notifiable trait를 추가한다.
use Notifiable;

protected $guarded = [
'member_id', 'remember_token'
];

protected $hidden = [
'password', 'remember_token',
];
}

인증 필드 변경

Basic Auth는 기본 필드를 email로 잡고 있기 때문에 id 필드를 사용하게 변경해야한다.

app/Http/Controllers/Auth/LoginController.php
<?php
// username 메소드를 추가
public function username() {
// 반환할 필드를 선언한다.
return 'id';
}

회원가입 후 자동로그인

회원가입이 성공하면 세션을 생성해줘야한다.

app/Http/Controllers/Auth/RegisterController.php
<?php
// registered 메소드를 Override
protected function registered(Request $request, $user) {
// 세션 생성
Auth::attempt([
'id' => $request->input('id'),
'password' => $request->input('password')
]);

return response(null, 204);
}

ajax 처리

로그인

로그인을 ajax로 처리해야될 경우 커스터마이징이 필요하다.

app/Http/Controllers/Auth/LoginController.php
<?php
// authenticated 메소드를 Override
protected function authenticated(Request $request, $user) {
// 인증이 된 경우 페이지 전환이 아닌 전환될 페이지를 json으로 반환한다.
if ($request->ajax()) {
return response()->json([
'href' => url()->previous()
]);

} else {
return abort(405);
}
}

비밀번호 찾기

비밀번호 찾기를 ajax로 처리해야될 경우 커스터마이징이 필요하다.

app/Http/Controllers/Auth/ForgotPasswordController.php
<?php
public function sendResetLinkEmail(Request $request) {
$this->validateId($request);
// email이 아닌 아이디로 검색을 해 메일을 찾고 싶다면
$data = Member::where('id', $request->only('id'))
->first(['email']);

$email = isset($data->email) ? $data->email : null;

// 여기서 email이 User Model(Member Model)에 바인딩된다.
// 아래의 sendPasswordResetNotification 메소드에서 $this->email 값이다.
$response = $this->broker()->sendResetLink([
'email' => $email
]);

// 리턴 값을 변경해주자.
return response()->json([
'email' => $email
], $response == Password::RESET_LINK_SENT ? 200 : 500 );
}

routing 예외

ajax 요청으로 바꿨다면 굳이 필요없는 기본 route는 등록할 필요가 없다. (예를 들면 로그인 페이지) 먼저 routes/web.php에서 Auth::routes(); 를 지워주고 라라벨 route 파일을 열어보자.

vendor/laravel/Illuminate/Routing/Router.php
// 994 라인
public function auth()
{
// Authentication Routes...
$this->get('login', 'Auth\LoginController@showLoginForm')->name('login');
$this->post('login', 'Auth\LoginController@login');
$this->post('logout', 'Auth\LoginController@logout')->name('logout');

// Registration Routes...
$this->get('register', 'Auth\RegisterController@showRegistrationForm')->name('register');
$this->post('register', 'Auth\RegisterController@register');

// Password Reset Routes...
$this->get('password/reset', 'Auth\ForgotPasswordController@showLinkRequestForm')->name('password.request');
$this->post('password/email', 'Auth\ForgotPasswordController@sendResetLinkEmail')->name('password.email');
$this->get('password/reset/{token}', 'Auth\ResetPasswordController@showResetForm')->name('password.reset');
$this->post('password/reset', 'Auth\ResetPasswordController@reset');
}

입맛에 맞게 web.php로 가져와 사용하자.

notification 예외

Noticifation은 사용자에게 빠르게 알림을 보낼 수 있는 기능이지만, 정해져 있는 템플릿을 사용하므로 커스터마이징이 되게 힘들다. 비밀번호 찾기시에 보낼 메일을 정해진 템플릿을 사용할 수 없다면 메소드를 수정하자.

app/Models/Member.php
<?php
// 이 메소드를 override해야한다.
// 첫 파라미터는 비밀번호 인증용 token이 들어온다.
public function sendPasswordResetNotification($token) {
Mail::to($this->email)->send(new PasswordReset($token));
}

Auth

Basic Auth를 사용하는데 건드려야 되는 곳이 많으므로 Auth 모듈만 사용하는게 정신건강에 좋다고 본다. (Bootstrap 기반의 Laravel에 딱 맞는 모양을 입은 프로젝트라면 기본 인증이 좋겠지만)

로그인

먼저 사용할 모델에 Authenticatable 클래스를 상속 받자 그리고 LoginController에서 Auth::attempt() 메소드를 실행하면 끝이다.

app/Http/Controllers/LoginController.php
public function login(Request $request) {
$password = $request->password;

if(Auth::attempt([ 'id' => $request->id, 'password' => $password ])){
return response(null, 200);

}else{
return abort(401);
}
}

not bcrypt

Auth 모듈은 기본으로 bcrypt를 사용해 비밀번호를 암호화하고 비교하는데 다른 암호화 방식을 사용해야하는 경우가 있다. bcrypt를 사용하지 않게 처리해보자.

app/Models/Member.php
// 이 메소드를 override 해야한다.
public function getAuthPassword() {
// bcrypt 비교를 하지 않기 위해 강제로 해시를 생성한다.
return Hash::make($this->password);
}

이제 Auth::attempt() 메소드에 패스워드를 넘길 때 암호화를 해주고 넘기면 된다.

N개의 세션

관리자와 회원은 같은 세션을 사용하면 안 된다. 세션을 분기해보자.

모델 생성

먼저 모델을 하나 만들고 Authenticatable 클래스를 상속받는다.

app/Models/Admin.php
<?php

use Illuminate\Foundation\Auth\User as Authenticatable;

class Admin extends Authenticatable {
// 속성은 테이블에 맞게 알아서 정리하자
}

설정 추가

만든 모델을 Laravel Auth에서 사용한다고 등록을 해줘야한다.

config/auth.php
<?php
return [
'guards' => [
...
// 아래에 등록된 provider를 admin이란 이름의 guard로 사용
'admin' => [
'driver' => 'session',
'provider' => 'admin'
]
],
'providers' => [
...
// 방금 만든 모델을 Auth의 Provider로 등록
'admin' => [
'driver' => 'eloquent',
'model' => App\Models\Admin::class
]
],
];

passowrds 속성은 제공되는 password_resets 기능을 사용할 경우에만 추가해주면 된다.

미들웨어

관리자 세션이 인증된 사람만 관리자 페이지에 접근할 수 있어야한다. php artisan make:middleware Admin 명령어를 실행해 Admin Middleware를 만들자.

app/Http/Middleware/Admin.php
<?php
use Auth;
use Closure;

class Admin {

public function handle($request, Closure $next) {
// admin session이 있는 경우만 request를 진행한다.
if (Auth::guard('admin')->check()) {
return $next($request);
}

// 아닐경우 관리자 로그인 페이지로 넘긴다.
return redirect()->route('admin.login');
}
}

그리고 Http Kernel에 방금 만든 Admin Middleware를 라우팅에서 사용할 수 있게 등록해준다.

app/Http/Kernel.php
<?php
...
protected $routeMiddleware = [
'auth' => \Illuminate\Auth\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,
'can' => \Illuminate\Auth\Middleware\Authorize::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
// admin 이름으로 Admin Middleware를 호출할 수 있게 등록
'admin' => \App\Http\Middleware\Admin::class,
];

새로운 라우팅 파일로 관리하기 위해 RouteServiceProvider에 설정을 추가한다.

app/Providers/RouteServiceProvider.php
<?php
...
public function map() {
...
$this->mapAdminRoutes();
}

protected function mapAdminRoutes() {
// /admin 으로 들어오는 요청에 대해 처리한다.
Route::prefix('admin')
// 이런식으로 한 번에 추가할 수도 있다. 하지만 except 함수를 쓸 수 없는 것 같고,
// 라우팅 페이지에서 미들웨어를 등록해주는 게 더 편했다.
// ->middleware(['web', 'admin'])
->middleware('web')
->namespace($this->namespace)
// routes/admin.php 파일을 관리자 라우팅 파일로 등록한다.
->group(base_path('routes/admin.php'));
}

마지막으로 라우팅 파일에 미들웨어를 넣어준다.

routes/admin.php
<?php
// admin middleware를 사용하고, namespace에 admin.을 추가한다.
Route::group(['middleware' => 'admin', 'as' => 'admin.'], function() {
// 이 라우팅은 route('admin.main') 으로 접근이 가능해진다.
Route::get('/', 'AdminController@index')->name('main');
});

활용

기존 Auth 메소드들에 guard만 추가해주면 쉽게 사용 가능하다.

<?php
Auth::guard('admin')->attepmt();
Auth::guard('admin')->check();
Auth::guard('admin')->user();
Auth::guard('admin')->logout();

로그아웃

Auth::logout() 메소드를 호출하면 된다.

이슈

세션 아이디

라라벨에선 로그인을 할 때 세션아이디를 새로운 값으로 엎어쳐버린다. 따라서 같은 환경에서 접근했는데, 로그인을 하면 다른 사용자가 되는 경우가 생긴다. (로그아웃시에는 그대로다.)

<?php
// 로그인 전에 세션아이디를 가져와
$old_session_id = Session::getId();
// 로그인이 성공하면 DB에서 등록된 세션값을 업데이트하자.

소셜 로그인

laravel/socialite 패키지를 설치하고 아주 쉽게 연동이 가능하다.

세팅

composer require laravel/socialite

기본 socialite 패키지는 facebook, twitter, linkedin, google, github or bitbucket 만 연동이 가능하므로 다른 소셜 로그인을 연동하고 싶을 경우 Socialite Providers 패키지를 사용하면 된다.

Socialite Providers 사이트에는 Naver, Kakao, BattleNet(?) 등의 소셜 연동이 가능한 패키지가 무수히 많다.

여담

JWT 인증과 같이본다면 Laravel을 사용한 실무에서 필요한 인증은 다 마무리한 셈일 듯

PHP 7.1에서 mcrypt 대체하기

· 3 min read

MCRYPT

  • 암호화 함수인 mcrypt 가 PHP 7.1 버전부터 Deprecated 되었다.
  • 왜 사라졌는지는 링크에 자세하게 나와있다.

기존 소스

  • PHP 구버전에서는 mcryptMCRYPT_RIJNDAEL_128 알고리즘을 통해 AES128 이 구현되어 있을 것이다.
  • MCRYPT_RIJNDAEL_128AES 128동일하다.
  • 타 언어와는 조금 다른데, 호환을 위해선 pkcs5 padding 으로 변경해주는 작업이 필요하다.
  • php mcrypt function 의 기본 패딩은 zeros padding
<?php
// 이런 식이거나
$cipherText = mcrypt_encrypt(MCRYPT_RIJNDAEL_128, $key, pkcs5_pad($plainText), MCRYPT_MODE_CBC, $iv);

// 이런 식일 것
$td = mcrypt_module_open("rijndael-128", "", "cbc", "");
@mcrypt_generic_init($td, $key, $iv);
$cipherText = @mcrypt_generic($td, pkcs5_pad($plainText));
mcrypt_generic_deinit($td);
mcrypt_module_close($td);

function pkcs5_pad($text, $blockSize = 16) {
$pad = $blockSize - (strlen($text) % $blockSize);
return $text . str_repeat(chr($pad), $pad);
}

대안

  • PHP 5.3 부터 사용 가능한 openssl_encrypt 함수를 쓰면 된다.
  • openssl 확장 모듈이 설치되어야한다.

OPENSSL

Encrypt

<?php
$cipherText = openssl_encrypt($plainText, 'AES-128-CBC', $key, OPENSSL_RAW_DATA, $iv);
// decrypt에서 오류가 발생해 PKCS5로 패딩을 맞춰주고 보내야한다면.
$cipherText = openssl_encrypt(pkcs5_pad($plainText), 'AES-128-CBC', $key, OPENSSL_RAW_DATA, $iv);

Decrypt

<?php
$plainText = openssl_decrypt($cipherText, 'AES-128-CBC', $key, OPENSSL_RAW_DATA, $iv);
// unpad가 필요하다면
$plainText = pkcs5_unpad(openssl_decrypt($cipherText, 'AES-128-CBC', $key, OPENSSL_RAW_DATA, $iv));

function pkcs5_unpad($text) {
$pad = ord($text{strlen($text)-1});
if ($pad > strlen($text)) {
return $text;
}
if (!strspn($text, chr($pad), strlen($text) - $pad)) {
return $text;
}
return substr($text, 0, -1 * $pad);
}

PKCS5 vs PKCS7

  • openssl_encrypt의 default padding 은 PKCS7 padding 인데, 어떻게 호환이 되는 것일까?
  • 여기서 해답을 찾았다.

The difference between the PKCS#5 and PKCS#7 padding mechanisms is the block size; PKCS#5 padding is defined for 8-byte block sizes, PKCS#7 padding would work for any block size from 1 to 255 bytes. So fundamentally PKCS#5 padding is a subset of PKCS#7 padding for 8 byte block sizes. so, data encrypted with PKCS#5 is able to decrypt with PKCS#7, but data encrypted with PKCS#7 may not be able to decrypt with PKCS#5.

triple des

  • des-ede3-cbc가 triple des 알고리즘이다.
<?php
// encrypt
openssl_encrypt($text, "des-ede3-cbc", $key, OPENSSL_RAW_DATA, $iv);

// decrypt
openssl_decrypt($cipherText, "des-ede3-cbc", $key, OPENSSL_RAW_DATA, $iv);

패키지

  • 이 모든 걸 커버하는 라이브러리를 사용하자. phpseclib/mcrypt_compat
  • phpseclib 는 laravel/passport 에서도 사용되었다.

여담

카카오페이가 PHP 5 버전만 지원해 문의해봤지만 계획이 없어 이번 기회에 모듈을 다 뜯어봤는데 MCRYPT 만 만져주면 정상적으로 결제가 되었다.

곧 이 짓을 안해도 될 듯하다. KakaoPay 종료

CentOS7 LEMP Stack 설치하기 (HTTP2, PHP7.1, Maria, Letsencrypt)

· 9 min read

LEMP

Linux, Nginx, MariaDB, PHP

Why LEMP instead of LNMP? We go with LEMP due to the pronunciation for Nginx: Engine-X (en-juhn-ecks). Think of how in English, the article an is used instead of a for hour even though it begins with a consonant. The importance is the sound of the first letter rather than its written representation. Besides, LEMP is actually pronounceable and doesn’t sound like reciting the alphabet.

PHP

repo 를 등록하고 설치하는 방법이 있지만 php71 등의 이름으로 설정해야되서 번거롭다. yum 명령어의 --enablerepo 옵션을 사용해 php71 repository 를 가져오자.

## php repo를 가져오기
$ yum -y install http://rpms.famillecollet.com/enterprise/remi-release-7.rpm

$ vi /etc/yum.repos.d/remi-php71.repo

[remi-php71]
name=Remi\'s PHP 7.1 RPM repository for Enterprise Linux 7 - $basearch
#baseurl=http://rpms.remirepo.net/enterprise/7/php71/$basearch/
mirrorlist=http://rpms.remirepo.net/enterprise/7/php71/mirror
## 이 값을 1로 바꿔준다.
enabled=1
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-remi

$ yum -y install php php-fpm php-devel php-curl php-mcrypt php-mysql php-xmlrpc php-mbstring php-zip 등등

$ systemctl start php-fpm
$ systemctl enable php-fpm

Maria

예전 포스팅으로 대체한다. 설치방법은 똑같고 버전만 다르게 해주면 된다. 설치 후 서비스 실행만 systemctl 명령어로 해주자.

Xtrabackup

이제 mysqldump 를 좀 놓아주고 Incremental Backup 을 해주자. 이게 왜 좋은지는 공홈이나 이나 다른 블로그에 자세히 설명되어있다.

## xtrabackup 설치
$ yum -y install xtrabackup

## 증분백업을 위해 전체 백업이 한 번 필요하다.
## root가 아닌 유저일 경우 해당 DB를 백업하기 위해 PROCESS 등 여러 권한을 추가해야한다.
## (로그 메세지로 추가하라고 나온다.)
$ innobackupex --user='root' --password='암호' --databases='DB' --no-timestamp /백업위치

## 증분백업
$ innobackupex --user='root' --password='암호' --incremental --incremental-basedir='/백업위치/xtrabackup_checkpoints' --no-timestamp /증분백업위치

## 증분백업을 전체백업 위에 얹기
$ innobackupex --apply-log --redo-only --incremental-dir='/증분백업위치' /백업위치

## 증분백업 삭제
$ rm -rf /증분백업위치


## 복원
$ systemctl stop mariadb

## 데이터 폴더를 날려야한다. (만약을 대비해 백업)
$ rm -rf /var/lib/mysql/*
$ innobackupex --copy-back /백업위치

## 권한부여
$ chown -R mysql:mysql /var/lib/mysql
$ systemctl start mariadb

Nginx

HTTP2 를 적용하기 위해서는 Nginx 1.9 버전과 Openssl 1.0.2 버전 이상이 필요한데, 기본 repository 에는 이 버전이 적용되어있지 않다. 따라서 검색해보면 컴파일 설치를 해야 된다는 글이 다수다. 모든 패키지는 패키지 매니져로 관리하는게 좋다고 생각하는 나로썬 그냥 넘어갈 수 없다. Nginx 최신 repository 를 관리해주는 나이스한 곳을 찾았다.

$ yum -y install yum-utils
$ yum-config-manager --add-repo https://brouken.com/brouken.repo

## 기존 nginx repo를 사용하지 않는다는 옵션
$ yum-config-manager --save --setopt=epel.exclude=nginx*;
$ yum -y install nginx

$ systemctl start nginx
$ systemctl enable nginx

## nginx 버전 확인
$ Nginx -V
nginx version: nginx/1.13.1
built with OpenSSL 1.0.2k 26 Jan 2017

setopt 가 오류가 날 경우

ngixn.repo 를 만들어주고 연결하자.

$ vi /etc/yum.repos.d/nginx.repo

## vim /etc/yum.repos.d/nginx.repo

[nginx]
name=nginx repo
baseurl=http://nginx.org/packages/mainline/centos/7/$basearch/
gpgcheck=0
enabled=1

## 저장하고 설치 시작

$ yum update
$ yum -y install nginx
$ systemctl start nginx
$ systemctl enable nginx

LetsEncrypt

공짜 SSL 인 LetsEncrypt 의 이름이 certbot 으로 바뀌었다. 공홈을 참조해도 좋다. 인증을 받기위해 먼저 도메인을 따야한다.

$ yum -y install certbot

## 인증
$ certbot certonly --standalone -d example.com -d www.example.com

## 갱신 확인
$ certbot renew --dry-run

## 갱신하면서 nginx 재부팅
## 이 명령어를 적절히 cron에 넣어주자.
$ certbot renew --pre-hook="systemctl stop nginx" --post-hook="systemctl start nginx"

인증이 성공하면 /etc/letsencrypt/live/example.com/ 경로 아래에 키가 떨어질 것이다. 자세한 명령어 옵션은 Docs 참조.

nginx 연동

ssl 을 적용하면서 HTTP2 도 붙혀보자. HTTP2 에서는 bundling 보다 파일을 쪼개서 보내는게 더 효율적이라고 한다. (non-blocking 이니까)

## HTTP
server {
listen 80;
server_name example.com www.example.com;

# certbot --webroot 인증을 받기위한 설정
#location ^~ /.well-known/acme-challenge/ {
# default_type "text/plain";
# root /var/www/letsencrypt;
#}

# 80 접속시 443으로 redirect
location / {
return 301 https://$server_name$request_uri;
}
}

## HTTPS
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name example.com www.example.com;
root /var/nginx/www/public;
index index.php index.html;

# 지저분한 보안 옵션은 추천옵션이니 넣어주자.
ssl on;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
ssl_trusted_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:50m;
ssl_session_tickets off;

#ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_protocols TLSv1.2;
ssl_ciphers EECDH+AESGCM:EECDH+AES;
ssl_ecdh_curve secp384r1;
ssl_prefer_server_ciphers on;

ssl_stapling on;
ssl_stapling_verify on;

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

location ~ \.php {
fastcgi_split_path_info ^(.+\.php)(/.+)$;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param SCRIPT_NAME $fastcgi_script_name;
fastcgi_index index.php;
fastcgi_pass 127.0.0.1:9000;
}

location ~ /\.ht {
deny all;
}

# 캐싱할 데이터가 있다면 추가
location ~* \.(?:jpg|jpeg|gif|png|ico|cur|gz|svg|svgz|mp4|ogg|ogv|webm|htc|woff)$ {
expires 1M;
add_header Cache-Control "public";
}

location ~* \.(?:css|js)$ {
expires 7d;
add_header Cache-Control "public";
}
}

해당 세팅은 Laravel5.4 용이다.

nginx.conf 에는 해당 옵션들도 추가해주자.

nginx.conf
  # CPU 물리 코어에 따라 설정한다.
# 1코어에선 1로 나머지는 auto로 설정하면 된다.
# grep ^processor /proc/cpuinfo | wc -l 로 확인 가능
worker_processes 1;

events {
# 각 worker process에서 한 번에 처리할 수 있는 최대 연결 수
# ulimit -n의 값과 같게 설정하자.
worker_connections 1024;
# I/O event 노티 방식을 epoll로 사용. (poll보다 발전한 방식)
use epoll;
# worker가 한 번에 모든 연결을 수용할 수 있도록 설정
multi_accept on;
}

http {
# access_log 제거
access_log off;
server_tokens off;

# iframe 보안 이슈 DENY
add_header X-Frame-Options DENY;
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";

# PHP 서버라는 header 제거
fastcgi_hide_header X-Powered-By;
# static file 제공 최적화
sendfile on;
# TCP multiple buffer를 individual packet으로 전송하게 변경
tcp_nopush on;
# TCP에서 TCP_CORK 옵션을 활성화
# MTU에서 IP 헤더의 40-60 Byte를 뺀값과 같다는데 뭐라는건지 모르겠다.
tcp_nodelay on;
# keep alive 설정
keepalive_timeout 65;
# keepalive_requests 100000;

# gzip 압축설정
gzip on;
gzip_min_length 1000;
gzip_types application/x-javascript text/css application/javascript text/javascript text/plain text/xml application/json application/vnd.ms-fontobject application/x-font-opentype application/x-font-truetype application/x-font-ttf application/xml font/eot font/opentype font/otf image/svg+xml image/vnd.microsoft.icon;
gzip_disable "MSIE [1-6]\.";
}

적용 후에 HTTP2 확인 확장프로그램 설치 후 이 프로그램에 파란불이 들어오면 성공이다.

파일 백업

소스는 git 으로 관리가 되는데 static 파일은 주기적으로 백업이 필요하다.

file_backup.sh
NOW_DATE=`date`
BACKUP_DATE=`date +"%Y%m%d"`
FILE_DIR=백업할 폴더 경로
BACKUP_DIR=백업된 파일 경로

## gz 압축
tar zcvf ${BACKUP_DIR}/${BACKUP_DATE}.tar.gz ${FILE_DIR}

## 3일 이상된 백업파일은 제거
find ${BACKUP_DIR}/ -mtime +3 -exec rm -f {} \;
## find ${BACKUP_DIR}/ -mtime 3 -delete

여담

다음부터 Ubuntu 쓸까 생각했는데, Nginx 최신 Repo 를 찾은게 컸다. 하지만 빨리 Docker 책 읽자. Nginx, Reverse Proxy, Redis, PHP, Jenkins 를 한 번에 해결해주겠지. 이제 DockerFile 이 잘 읽히긴 하는데, 언제쯤 갈아탈 수 있으려나

Laravel 5.5 - Eloquent ORM 사용하기

· 17 min read

Eloquent ORM을 사용해 데이터를 가져와보자. 이전 포스팅에서 이어진다. (Eloquent ORM 기능만 필요하다면 3.Model 부터 보면 된다)

Migration

DBMS에 상관 없이 테이블을 똑같이 생성하기 위한 기능이다.

생성

artisan 명령어로 간단히 생성할 수 있다. 날짜_테이블 형식의 파일이 database/migrations 밑에 생성된다. 이 파일들의 자동 생성된 이름을 절대 변경하지 말자 내부적으로 언더스코어를 explode해 class를 호출하기 때문에 건들면 고생한다.

## 모델을 생성하면서 같이 생성
## m 옵션을 추가한다.
$ php artisan make:model -m 모델명

## 마이그레이션만 생성
$ php artisan make:migration 마이그레이션명

생성 후에 up 메소드 안에 테이블을 꾸며주고 down 메소드에 테이블을 지울 때 실행할 로직을 구현한다. 컬럼 타입을 지정하는 메소드는 여기를 참조하면 된다.

<?php
public function up() {
Schema::create('table_name', function (Blueprint $table) {
$table->increments('idx');
$table->string('id', 20)->unique();
$table->string('name', 30);
$table->string('email', 100)->unique();
$table->string('password', 250);
$table->tinyInteger('tiny')->nullable();
$table->timestamps();
});
}

// 테이블에 외래키 제약조건이 걸려있을 경우 migration으로 일괄 삭제시에
// 오류가 발생하므로, 외래키 체크 옵션을 비활성화 해줘야될 수 도 있다.
public function down() {
DB::statement('SET FOREIGN_KEY_CHECKS = 0');
Schema::dropIfExists('table_name');
DB::statement('SET FOREIGN_KEY_CHECKS = 1');
}

실행

php artisan migrate 명령어로 실행하면 된다.

  • migrate => 전체 실행
  • migrate:refresh => 다시 실행
  • migrate:rollback => 마지막 migrate 시점으로 돌림
  • migrate:reset => 제거

Tinker

이미 데이터가 들어간 테이블이 있어 migrate --path로 한 파일만을 실행해야 하는데, tinker를 사용하면 더 쉽게 해당 마이그레이션을 실행시킬 수 있다. Tinker는 커맨드로 라라벨 쉘(실행환경)으로 들어간다고 생각하면 된다.

## tinker
$ php artisan tinker

## shell로 접속된다.
Psy Shell v0.8.5 (PHP 7.0.7 ??cli) by Justin Hileman
New version is available (current: v0.8.5, latest: v0.8.8)
>>>
## blueprint 의존성을 추가해주고 (Blueprint는 up 메소드에 DI로 들어가 있기에 읽지 못한다.)
$ use Illuminate\Database\Schema\Blueprint;
## 새로 실행하려 했던 스키마를 실행시켜주기만 하면 된다.
$ Schema::create('new_table', function (Blueprint $table) {
$table->increments('idx');
$table->string('id', 20)->unique();
$table->timestamps();
});

DB에 해당 테이블이 생성된 걸 확인할 수 있다.

Seed

Seed는 테이블에 필수 데이터 또는 더미 데이터를 심어주는 과정이다. 테스트에 필요한 데이터를 넣어주는데 아주 효과적이다.

생성

php artisan make:seeder 시더명

database/seeds 아래에 파일이 생성된다. 파일을 열어보면 run() 메소드 하나가 있는데, 여기에 시드파일이 호출될 때 실행할 로직을 구현해주면 된다.

database/seeds/BoardSeed.php
<?php
public function run() {
// 모델로 생성하기
Board::create([
'title' => '공지',
'author' => '관리자',
'content' => '공지입니다'
]);
// DB 파사드로 생성하기
DB::table('board')->insert([
'title' => '공지',
'author' => '관리자',
'content' => '공지입니다'
]);
}

Faker

랜덤한 테스트용 데이터가 많이 필요하다면, 가짜(더미) 데이터를 생성해주는 라이브러리를 사용할 수 있다. 링크를 따라가보면 정말 엄청난 종류의 랜덤 데이터를 생성할 수 있음에 놀랄 것이다.

faker를 사용하려면 ModelFactory를 먼저 정의해야한다. 상품 모델을 가져와 가짜 데이터 타입을 정의해보자.

database/factories/ModelFactory.php
<?php
$factory->define(App\Models\Product::class, function(Faker\Generator $faker){
return [
// 메소드는 위의 라이브러리를 참고하자.
// 상품명을 중복되지 않게 city(도시명)으로 가져온다.
'name' => $faker->unique()->city,
// 색상 헥스코드를 가져온다.
'color' => $faker->safeColorName,
// 1000~50000원 사이의 가격을 가져온다.
'price' => $faker->numberBetween(1000, 50000),
// lorempixel의 랜덤 이미지를 가져온다.
'thumbnail' => $faker->imageUrl(200, 100),
'detail_image' => $faker->imageUrl(300, 600),
'qty' => $faker->numberBetween(1, 1000),
'status' => 1,
'owner' => $faker->name,
'sorting' => $faker->numberBetween(1, 9999)
];
});

실행

위에서 선언한 faker 모델을 seed에서 호출해보자.

database/seeds/ProductSeed.php
<?php
public function run() {
// 두번째 인자로 실행시킬 횟수를 넣어주면 된다.
factory(App\Models\Product::class, 100)->create();
}
php artisan db:seed

테이블에 상품 더미데이터가 100개 생성된 것을 확인할 수 있다.

Model

모델은 하나의 테이블의 타입을 정해 놓은 것이라 보면 된다. 데이터를 가져오거나 넣을 때 모델에 정의된 형태로 가져온다.

기본 구조

아래 구조 정도만 알아두면 된다. 더 자세한건 Eloquent Model Class를 확인해보자.

app/Models/Board.php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;

class Board extends Model {
// 임의의 생성 필드가 있다면 설정해준다. (기본값 created_at)
const CREATED_AT = 'created_dt';
// 사용하지 않을 경우 Null로 초기화해주자. (기본값 updated_at)
const UPDATED_AT = null;
// 둘 다 사용하지 않을 경우 timestamps만 꺼주면 된다. (기본값 true)
public $timestamps = false;
// PK가 auto increment가 아닐경우 false로 바꿔준다. (기본값 true)
public $incrementing = false;

// PK 값을 변경한다. (기본값 id)
protected $primaryKey = 'idx';
// 테이블 명을 변경한다. (기본값 모델명의 복수형)
protected $table = 'board';

// dump insert 및 dump update가 안되는 필드를 설정한다.
protected $guarded = [
'idx'
];

// guarded와 fillable 중 하나만 있으면 된다. (써보니 guarded가 편하더라.)
// dump insert 및 dump update가 가능한 필드를 설정한다.
//protected $fillable = [
// 'title', 'content'
//];

// 노출시키지 않을 필드가 있을 경우
protected $hidden = [
'password'
];
}

Relationship

1:1

hasOne과 belongsTo로 연결한다.

1:n

hasMany와 belongsTo로 연결한다.

n:m

belongsToMany로 연결한다. 처음에 제일 감이 안왔던 Relationship이지만 코드를 보면 이해하기가 쉽다. 주문(Order) - 주문 상품(OrderProduct) - 상품(Product) 테이블이 있다고 가정한다. 각각 시작 - 링크(피벗) - 종료 테이블이다.

<?php App/Models/Order.php

public function product(){
// 마지막으로 연결될 모델명, 링크 테이블 명, 시작->링크를 연결시킬 인덱스, 링크->종료를 연결시킬 인덱스
return $this->belongsToMany('App\Models\Product', 'order_product', 'order_id', 'product_id')
// 링크 테이블의 컬럼을 가져와야할 때
->withPivot(['price', 'quantity']);
}

이렇게 정의하면 뷰에서 order->productforeach로 돌려 product->product_id처럼 접근할 수 있고 product->pivot->price로 pivot 테이블의 데이터도 가져올 수 있다.

1:1:1

이런 관계가 있을 경우에 라라벨에서 관계메소드를 직접 지원하지는 않는다. 억지로 사용하려면, hasOne을 두 번 사용해 연결해야하는데, Select Query를 두 번 날려야 된다는 소리다. (참을 수 없다)

BelongsToThrough trait 패키지를 통해 깔끔하게 해결할 수 있다. 메소드에서 belongsToMany처럼 커스텀 외래키를 사용해야할 경우엔 5번째 파라미터로 [ 클래스 => 키 ] 형식으로 넘겨주면 된다. (여기의 소스를 참고하자)

이슈

ORM의 Join 방식

한 방 쿼리가 불가능하다. 오직 PK를 통해 Select된 데이터들을 다시 PHP 단에서 합쳐서 보여준다. 한 방이 필요할 때는 Database에서 View를 이용하거나 DB 파사드를 써야한다.

Composite Key

라라벨 Eloquent Model에서 복합키를 사용할 수는 없다. 편법으로 trait를 추가할 수 있는데 find 메소드도 overriding 해야 된다.

Timestamps

created_at과 updated_at은 timestamps 형식이라 date_format과 같은 쿼리함수 대신 whereDate 메소드로 연산을 실행해야한다. 물론 DB에 데이터가 들어가기 전 datetime으로 해당 컬럼을 변경해주면 된다. (timestamps의 유효기간은 2035년까지므로.)

Group By

GroupBy를 이용한 쿼리 사용시에 Syntax error or access violation: 1055 'table.column' isn't in GROUP BY라는 오류 메세지가 보이며 실행이 안 되는 경우가 있다. 해당 컬럼으로 GroupBy를 하지 않았는데도 발생한다.

database config의 strict 모드 중 ONLY_FULL_GROUP_BY 모드가 활성화 되어있어서 그런데 이 모드만 제외를 시켜주면 된다.

config/database.php
<?php
'connections' => [
'mysql' => [
// strict 속성을 false로 바꿔준다.
'strict' => false,
// modes 속성을 추가해 기본 strict 옵션을 다시 추가한다.
'modes' => [
'STRICT_TRANS_TABLES'
'NO_ZERO_IN_DATE'
'NO_ZERO_DATE'
'ERROR_FOR_DIVISION_BY_ZERO'
'NO_AUTO_CREATE_USER'
'NO_ENGINE_SUBSTITUTION'
]
],

변경 후 config:cache로 config 파일들을 다시 캐싱해주자.

연동

만든 모델을 선언만 하면 Eloquent Model 메소드Query Builder 메소드를 사용할 수 있다.

CRUD는 다음과 같다.

YourController.php
<?php

use App\Model;

public function test($id) {
// PK로 데이터 1개 반환
Model::find($id);
Model::where('idx', $id)->first();

// 전체 데이터 반환
Model::all();
// 조건 데이터 반환
Model::where()->get();

// Insert
$model = new Model;
$model->column = '추가 값';
$model->column2 = '추가 값2';
$model->save();

// Mass Insert
Model::create([
'column' => '추가 값',
'column2' => '추가 값2'
]);

// Bulk Insert
Model::insert([[
'column' => '추가 값',
'column2' => '추가 값2'
], [
'column' => '추가 값',
'column2' => '추가 값2'
]]);

// Update
$model = Model::find($id);
$model->column = '변경 값';
$model->column2 = '변경 값2';
$model->save();

// Mass Update
Model::where()->update([
'column' => '변경 값',
'column2' => '변경 값2'
]);

// Delete
Model::destory($id);
Model::find($id)->delete();
Model::where()->delete();
}

Collection과 Model의 차이

Model에서 쓸 수 있는 메소드와 Collection에서 쓸 수 있는 메소드가 다르다. Model Class에서 정의하는 관계 메소드들은 Model에서만 사용 가능하다.

  • Collection: Array<Model> 즉 모델의 집합이다.
  • Model: 테이블에서 하나의 행이라고 생각하자.

Paging

모델에서 paginate 메소드를 사용하면, querystring에 page 변수가 붙어 페이징이 된다. (예를들면 /boards?page=1) 라라벨의 기본 페이징은 Bootstrap의 Class를 사용한다.

<?php
// 이렇게 모델을 가지고 왔다면
Model::where()->get();

// get을 paginate로 바꿔주기만 하면 된다. (뿌려지는 list item의 기본값은 15이다.)
Model::where()->paginate(10);

// next와 prev 버튼이 없는 pagination을 구현하고 싶다면
Model::where()->simplePaginate(10);

페이징 뷰를 꾸미고 싶다면 아래 명령어를 실행해 resources/views/vendor/pagination에 view가 생기고 default.blade.php를 수정하면 된다.

php artisan vendor:publish --tag=laravel-pagination

ajax pagination

실무에선 Paging 호출을 GET보단 AJAX를 쓰는게 깔끔한데 List View와 ListItem View, Controller 세부분을 모두 변경해줘야한다. 아래 소스는 jQuery를 사용하고 있다고 가정한다. (Frontend Framework를 같이 사용하고 있다면 더 깔끔하게 처리 될 수 있을듯)

Controller

YourController.php
<?php
public function list(Request $request) {
// ajax 요청일 경우에 listitem 뷰 반환
if ($request->ajax()) {
return view('listitem', [
'data' => Model::where()->paginate(10);
]);
// get 요청일 경우에 list 뷰 반환
} else {
return view('list', [
'data' => Model::where()->paginate(10);
])
}
}

List view

list.blade.php
<div id="list">
@include('listitem')
</div>
<script>
var paging_listener = function () {
// 페이지 버튼에 click listener 등록
$('.pagination a').click(function (e) {
// a href 로 페이지가 이동하는걸 방지한다.
e.preventDefault();
get_list($(this).attr('href'));
});
};

var get_list = function (url) {
$.get(url)
.then(function(html) {
$('#list').html(html);
// 페이지가 다시 그려졌으므로 listener를 다시 등록한다.
paging_listener();
});
};

$(function () {
paging_listener();
});
</script>

ListItem view

listitem.blade.php
<ul>
@foreach($data as $item)
<li>{{ $item->name }}</li>
@endforeach
</ul>
// pagination html이 반환되어 자동으로 보여진다.
{{ $data->links() }}

multiple pagination

한 페이지에 여러 리스트가 있는 경우가 종종 있다. 먼저 paginate 메소드를 살펴보자.

<?php
/**
* Paginate the given query into a simple paginator.
*
* @param int $perPage
* @param array $columns
* @param string $pageName
* @param int|null $page
* @return \Illuminate\Contracts\Pagination\LengthAwarePaginator
*/
public function paginate($perPage = 15, $columns = ['*'], $pageName = 'page', $page = null)
  • perPage: 한 페이지에 표시될 Item 수
  • columns: 가져올 컬럼명의 배열
  • pageName: page 변수명 (예를들면 /boards?page=1 에서 page 변수명을 변경 가능)
  • page: 가져올 페이지

Controller

Controller에서 paginate 메소드를 활용해보자.

YourController.php
<?php
public function list(Request $request) {
if ($request->ajax()) {
// list1_page 값이 들어올 경우
if ($request->has('list1_page')) {
// list1_item 뷰에 바인딩해준다.
return view('list1_item', [
'data1' => $this->get_list1()
]);
} else if ($request->has('list2_page')) {
return view('list2_item', [
'data2' => $this->get_list2()
]);
}
}

// 메인 요청
$data1 = $this->get_list1();
$data2 = $this->get_list2();
return view('list', [
'data1' => $data1,
'data2' => $data2
]);
}

protected function get_list1() {
return Model::where()->orderBy()->paginate(5, ['*'], 'list1_page');
}

protected function get_list2() {
return Model::where()->paginate(10, ['*'], 'list2_page');
}

List view

위의 script에서 조금만 수정해주면 된다. (list item view는 위와 동일한 코드의 반복이므로 생략)

list.blade.php
<div id="list1">
@include('list1_item')
</div>
<div id="list2">
@include('list2_item')
</div>

<script>
var paging_listener = function () {
$('.pagination a').click(function (e) {
e.preventDefault();
var page = $(this).attr('href').split('?')[1];
// page.split('=')[0]은 페이지 변수명을 가져온다.
var paging_type = page.split('=')[0] === 'list1_page'
? 'list1'
: 'list2';
get_list($(this).attr('href'), paging_type);
});
};

var get_list = function (url, target) {
$.get(url)
.then(function(html) {
$(`#${target}`).html(html);
paging_listener();
});
};
</script>

여담

이제 DB에서 데이터를 가져오는 것까지 끝났다. 다음 포스팅에서는 User Login을 구현해보자.

composer zlib_decode와 content-length mismatch 오류

· 3 min read

오류

컴포저로 패키지를 다운 받은 후에 갑자기 오류가 나면서 다른 설치조차 안되는 경우가 있다. 오류 메세지는 아래 두 경우로 나타난다.

zlib_decode

json 파일 디코딩이 실패했다는 오류이다.

Failed to decode response: zlib_decode(): data error
Retrying with degraded mode, check https://getcomposer.org/doc/articles/troubleshooting.md#degraded-mode for more info

[ErrorException]
zlib_decode(): data error

위의 링크를 따라가 보면 해결책으로 다음과 같이 제시가 되는데 ESET antivirus는 아직 국내에서 쓰는 사람을 못 봤고, IPv6을 막아도 그대로일 것이다.

  • If you are using ESET antivirus, go in "Advanced Settings" and disable "HTTP-scanner" under "web access protection"
  • If you are using IPv6, try disabling it. If that solves your issues, get in touch with your ISP or server host, the problem is not at the Packagist level but in the routing rules between you and Packagist (i.e. the internet at large). The best way to get these fixed is raise awareness to the network engineers that have the power to fix it. Take a look at the next section for IPv6 workarounds.
  • If none of the above helped, please report the error.

content-length mismatch

composer의 패키지 list를 가지고 있는 json파일이 다 안받아졌다는 내용의 오류이다.

[Composer\Downloader\TransportException]
Content-Length mismatch

해결

구글링 해보면 clear-cache를 한 뒤에 composer -vvv 명령어를 사용해 디버깅 로그를 확인해보라는 내용이 주류인데 별 도움은 안 된다. http 연결이아닌 https로 packagist에 붙어보라는 것도 해결되지 않았다.

packagist repository 변경

packagist는 유럽에 있어서 한국에서 유럽까지 갔다가 다시 돌아오는 구조라, packagist 일본 미러를 이용하면 이 문제가 해결된다.

## 이 명령어로 global config에 repos.packagist 를 추가한다.
$ composer config -g repos.packagist composer https://packagist.jp

## 설정이 되었는지 확인한다..
$ composer config -gl|grep repo

## 아래 설정이 보이면 추가가 된 것이다.
[repositories.packagist.org.type] composer
[repositories.packagist.org.url] https://packagist.jp

캐시 제거

composer global clear-cache
composer clear-cache

이제 다시 패키지를 설치하면 설치가 될 것이다.

다운로드 속도 향상

병렬 다운로드를 가능하게 하는 hirak/prestissimo 패키지를 설치하자. 288s -> 26s 가 되는 마법이 일어난다고 한다.

composer global require hirak/prestissimo

Laravel 5.5 - 시작하기

· 11 min read

왜 라라벨인가?

image from hexo

1등이 된지가 14년 7월이다. 이 글은 17년 글이다. 다른 장점이 궁금하다면 다른 포스트나 stackoverflow에서 해결해줄 것 같다.

이 글을 읽어주시는 분들은 Modern PHP에 대해 이해하고 있다고 생각했기 떄문에 문법적인 것들은 설명하지 않을 것이다. 더군다나 라라벨을 도입하면서 삽질한 내용을 정리하고 있기에, 입문용은 절대 아니다. 기본적으로 아래와 같은 PHP 개념을 알고 있어야한다.

  • PSR
  • Composer
  • Class
  • Namespace
  • Closure
  • Trait
  • PDO

추가적으로 이런 걸 사용해 봤다면 더 쉬울 듯하다.

  • Monolog
  • 다른 MVC 프레임워크
  • Lodash

설치

PHP

PHP 7 이상을 설치해주고, 아래 Extension은 웬만하면 열어주는 것들이니 크게 신경쓰지 말자.

  • PHP >= 7.0.0
  • OpenSSL PHP Extension
  • PDO PHP Extension
  • Mbstring PHP Extension
  • Tokenizer PHP Extension
  • XML PHP Extension

Composer

Composer 홈페이지에서 다운받고 전역설정만 해주면 된다.

Laravel Cli

Laravel 명령어를 사용할 수 있게 composer로 전역 설치하자.

composer global require "laravel/installer"

프로젝트 생성

초기 생성

laravel new 프로젝트명

위 명령어를 실행하면, 알아서 composer 패키지까지 설치된다.

clone 했을 때

환경 설정 파일과 프로젝트 키가 없으므로 적절하게 세팅해줘야한다.

## 패키지 의존성 설치
$ composer install

## .env 파일 복사 (설정파일 생성)
## post-root-package-install scripts로 들어가 있긴한데, 복사가 안 되는 경우
$ cp .env.example .env

## 프로젝트 키 생성
$ php artisan key:generate

실행

프로젝트를 실행해보자.

php artisan serve

이제 localhost:8000으로 라라벨 프로젝트에 접근이 가능하다.

php artisan 명령어는 laravel에서 사용하는 커맨드 쉘이다. php artisan로 명령어 리스트가 나오고 php artisan help 명령어 로 해당 명령어의 옵션을 볼 수 있다.

프로젝트 구조

라라벨 한글 메뉴얼이나 공식 메뉴얼을 보면 되는데 마치 사전을 펴놓았는데 어떤 단어를 찾는지 까먹은 느낌이 들 것이다.

모든 프레임워크는 Model, View, Controller, Routing만 알면 끝이라는 것을 잠시 기억 속에서 꺼내보고 딱 3가지 경로만 기억하자.

  • Route: routes/
  • Controller: app/Http/Controllers/
  • View : resources/views/

모델은 어딨어? 라고 생각할 것 같은데 Model을 완벽하게 사용하려면 ORM을 써야하고 DB를 ORM에 맞게 정규화해야하며 이 작업은 처음 진행하기에 고통스럽다. 다음 포스트에 진행해보자.

Route

모든 웹 프레임워크의 기본은 Routing이다. routes 폴더에 기본으로 api, channels, console, web 파일이 보이는데 그 중 2가지만 알면 된다.

  • api.php: Token 인증이 필요한 라우터로 /api/{route} 로 접근이 가능하다.
  • web.php: 기본적인 Route이다. GET이 아닌 다른 메소드는 CSRF Token이 있어야만 호출이 가능하다.

Controller

Routing 요청을 처리하는 로직이 들어있는 부분이다.

View

Controller에서 처리된 데이터를 플랫폼에 보여주는 부분이다.

프로젝트 세팅

App 설정

env 파일

.env 파일에서 APP_NAME과 DB로 시작하는 설정을 바꿔준다.

app 파일

config/app.php
<?php
return [
// 시간대 세팅을 한다.
'timezone' => 'Asia/Seoul',
// 언어 설정을 한다.
// fallback_locale은 언어팩이있어야 하므로 아래에서 진행할 laravel-lang 설치 후 바꿔주자.
'locale' => 'ko',
]

database 파일

config/database.php
<?php
return [
'connections' => [
'mysql' => [
// mysql일 경우 utf8로 설정해준다 (maria는 utf8mb4)
// DB 설정과 다르게 Model 이용시 charset 및 collation이 지정되어 들어가기에 꼭 설정해줘야한다.
'charset' => 'utf8',
'collation' => 'utf8_general_ci',
// 내 테이블의 접두사가 필요하다면 여기서 설정한다.
// 예를들어 test_member, test_product 이런식의 테이블 명명규칙이라면 아래처럼 주면된다.
'prefix' => 'test_',
// engine 옵션은 my.ini에서 만져주자..
'engine' => null,
],
];

config 내의 파일과 .env파일을 건드릴 경우 laravel 환경을 다시 캐싱해주거나 아예 삭제해야 적용이 된다. php artisan config:cache 명령어 또는 php artisan config:clear 명령어를 실행해주자.

Debug Bar

정말 멋진 디버깅 툴이다. 간단히 설치 후에 .env 파일의 APP_DEBUG 옵션이 true이면 자동으로 View 하단에 생성된다. 아래와 같은 데이터를 확인할 수 있다.

  • 로그 내역
  • 뷰 데이터 바인딩 확인
  • 오류
  • 라우팅
  • 쿼리 실행
  • 메일 발송 데이터
  • 실행 시간
  • Request

설치

composer require barryvdh/laravel-debugbar

laravel-lang

기본 오류 메세지들의 localization 패키지이다.

설치

composer require caouecs/laravel-lang:~3.0

적용

vendor/caouecs/laravel-lang/src/{국가} 폴더를 resources/lang/{국가} 로 복사하고 config/app.phpfallback_locale{국가}로 바꿔주면 된다.

## 귀찮으니 git bash에서 날려봅시다.
$ cp -r ./vendor/caouecs/laravel-lang/src/ko ./resources/lang/ko

라우팅

routes/web.php
<?php
// url 변수
Route::get('/your_url/{id}', function($id) {
return $id;
});

Route::get('/your_url/{id?}', function($id = null) {
return $id;
});

컨트롤러

생성

artisan 명령어로 간단히 생성할 수 있다.

## make:controller 폴더명/컨트롤러명
$ php artisan make:controller TestController

## 리소스 메소드를 같이 생성
$ php artisan make:controller TestController --resource

연결

routes/web.php
<?php
// 컨트롤러명@메소드명 으로 바로 연결시킬 수 있다.
Route::get('/url/{id}', 'TestController@get');
Route::post('/url', 'TestContoller@post');

// CRUD가 명확한 컨트롤러인 경우 Resource를 사용하는 게 편할 수 있다.
Route::resource('tests', 'TestController');
// only 또는 except로 원하는 resource만 가져갈 수 있다.
Route::resource('tests', 'TestController', [
'only' => [
'index', 'show'
]
]);
app/Http/Controllers/TestController.php
<?php
use Illuminate\Http\Request;

class TestController extends Controller{

public function get($id){

// 뷰를 호출
return view('test.detail', [
'data' => $data
]);
}

public function post(Request $request, $id){
// Request 데이터를 제어할 수 있다.
$input = $request->all();
$name = $request->input('name');

$data = [];

// json body와 201 http code를 반환
return response()->json([
'data' => $data
], 201);
}
}

Resource

리소스 사용시라면 아래와 같은 메소드로 구성되어야한다. 게시판 컨트롤러라면 아래와 같은 구성일 것이다.

메소드경로액션라우트.메소드
GET/boards게시판 메인boards.index
GET/boards/create게시글 생성페이지boards.create
POST/boards게시글 저장boards.store
GET/boards/{id}게시글 상세페이지boards.show
GET/boards/{id}/edit게시글 수정페이지boards.edit
PUT/PATCH/boards/{id}게시글 수정로직boards.update
DELETE/boards/{id}게시글 삭제boards.destroy

블레이드 템플릿을 사용하며 홈페이지 설명이 꽤나 자세하다. 당장 알아야 할건 아래 두 개 정도 뿐이다. 나머지는 그때 그때 문서를 참조하자.

resources/views/test/detail.blade.php
// 바인딩한 데이터를 {{ }} 구문으로 바로 접근할 수 있다.
{{ $data->name }}님의 정보입니다.
resources/views/test/create.blade.php
// post로 전송시
<form method="post" url="/url">
// post로 데이터를 전송시 csrf 토큰이 필요하다.
// 이 헬퍼함수를 호출하면 자동으로 토큰 값이 들어간 input hidden 필드가 생성된다.
{{ csrf_field() }}
이름: <input type="text" name="name" vavlue="">
</form>

// 기타 메소드로 전송시
<form method="post" url="/url">
{{ crsf_field() }}
// PUT, PATCH, DELETE 와 같은 메소드로 전송시 form 태그에서는 지원을 해주지 않으므로 method spoofing이 필요하다.
// 이 헬퍼함수를 호출하면 해결된다.
{{ method_field('PUT')}}
</form>

여담

이제 라라벨을 이용해 데이터를 서버에 보내고 뷰를 구성할 수 있게 되었다. 다음 포스팅에서는 DB 연동과 ORM 모델을 사용해 데이터를 가져오고 페이징 처리를 해보자.

PHP에서 HTML tag 사이의 문자 가져오기

· 2 min read

PHP 로 페이지를 크롤링한 후 HTML tag 안의 데이터가 필요할 때 함수 하나로 해결할 수 있다.

소스

<?php
/**
* [splitBetweenStr 텍스트 사이의 문자열을 배열로 반환]
* @param [string] $str [전체 문자열]
* @param [string] $startWord [찾을 시작 문자열]
* @param [string] $endWord [찾을 종료 문자열]
* @return [array] [텍스트 사이의 문자열 배열]
*
* ex) splitBetweenStr('<br>hi</br><b>test</b><br>graceful_light</br>', '<br>', '</br>')
* => ['hi', 'graceful_light']
*/
function splitBetweenStr($str, $startWord, $endWord) {
for ($i=0, $len=strlen($str); $i<$len; $i++) {
$target = substr($str,$i);
$prevStartIdx = strpos($target, $startWord);
$startIdx = $prevStartIdx + strlen($startWord);
$endIdx = strpos(substr($target, $startIdx), $endWord);

if ($prevStartIdx===false || $endIdx===false) {
break;
} else {
$betweenStrings[] = substr($target, $startIdx, $endIdx);
$i += $startIdx + $endIdx + strlen($endWord) - 1;
}
}

return $betweenStrings;
}

여담

물론 다른 크롤러 라이브러리를 사용하는게 속편하다. 호스팅 환경에서는 유용하게 사용할 수 있다.

cURL error 60 SSL certificate problem

· One min read

window localhost php 에서 cURL 함수 사용시 cURL error 60: SSL certificate problem...으로 시작하는 오류가 발생할 때가 있다.

image from hexo

해결

키 다운로드

root certificate bundle을 다운로드 받아 php 하위 경로로 넣어준다.

등록

php.ini에서 키를 추가한다.

php.ini
[curl]
; A default value for the CURLOPT_CAINFO option. This is required to be an
; absolute path.
curl.cainfo = D:/php7/extras/ssl/cacert.pem

참조