[Tip] Laravel with method specific columns

Laravel의 with method는 relation된 다른 테이블의 값을 함께 가져올 때 사용된다.

다만 이번 프로젝트에서 users 테이블의 연결된 logs의 timestamp 값 (created_at, updated_at)을 가져오고 싶었는데 hidden으로 감추기엔 모두 사용되는 값이였고,
그렇다고 모두 가져오기엔 필요없는 데이터와, 유저에게 표시되면 안되는 부분도 있어서 생성일만 가져오는게 제일인 상황에서 아래와 같이 코드를 짰었다.

// ...
public function history()
{
    // ...
    $logs = $user->logs;
    foreach ($logs as $log) {
        $user->log = [
            'created_at' => $log->created_at,
            'updated_at' => $log->updated_at,
        ];
    }
    // ...
    return $user;
}
// ...

리펙토링을 하는중에 가만 생각해보니 Raw로 쿼리를 짯을 땐 select로 원하는것만 가져오면되는데,
이걸 또 with으로 가져오다보니 select를 따로 하려면 callback을 짜야하는데 간단히 select 정도만 하면되는데 콜백으로 가독성이 더 떨어질까 생각하는 와중에 with에서 callback없이 원하는 column만 가져올 수 있는 방법을 찾앗다.

$user->with('logs:id,created_at,updated_at')->get()

with에 relation method명과 원하는 컬럼 명을 위와같은 포맷으로 전달해 주면 해당 결과만 표시되고 그외의 결과는 표시되지 않는다.

callback보다 보기 깔끔하고, 위에 코드보다 깨끗(?) 하다.

다만 주의해야 하는점이 반드시 ‘id‘값을 포함해야 한다. 예를 들어
$user->with('logs:created_at,updated_at')->get() 와 같이 작성하면 작동되지 않는다.
id 값을 기준으로 가져오기때문에 반드시 logs:id,를 넣어주어야한다.

[laravel] GuLa library

소개

GuL(a)ibrary (이하 구라블러리 ㅎ)는 sir.kr에서 만든 그누보드와 영카트의 테이블의 모델들을 묶어둔 라이브러리입니다.
바로 DB를 바꿀순 없고 api, scheduler등 더 다양한 기능들을 기존 그누보드, 영카트를 유지하면서 laravel로 구현하기 위해 만들어졌습니다.

실은 그누보드로 만들어진 사이트 서로 연동 하는 작업하는데 raw php론 너무 힘들어서 만들엇습니다. ㅎ

특징

구라블러리는 그누보드와 Laravel에서 사용할 DB를 따로 분리하여 운영이 가능합니다.
구라브러리는 Laravel의 relation을 그누보드 DB에서 사용할수 있습니다.
구라브러리는 G5ModelFactory를 통해 모델이 존재하지 않는 경우 runtime상에서 인스턴스 생성이 가능합니다.
구라브러리는 그누보드에서 생성된 g5_write_ 테이블을 G5Member 모델을 통해 자동으로 연결해줍니다,

모두 구랍니다

설치

composer require silnex/gula

.env

GULA_DB_HOST=gnuboard.db.host
GULA_DB_PORT=3306 # (default: 3306)
GULA_DB_DATABASE=gnuboard_db
GULA_DB_USERNAME=gnuboard_db_user
GULA_DB_PASSWORD=gnuboard_db_pass
GULA_CHARSET=utf8mb4 # DB 문자셋 (default: utf8mb4)
GULA_COLLATION=utf8mb4_unicode_ci # DB 문자셋 (default: utf8mb4_unicode_ci)

위처럼 env를 설정하여 그누보드와 Laravel의 DB를 각각 운영하실 수 있습니다.

예제

<?php
// 그누보드
use SilNex\GuLa\Models\Gnu\G5Member;
$g5Member = new G5Member;
$g5Member->all();
$g5Member->where('mb_id', '=', 'admin');

// 영카트
use SilNex\GuLa\Models\Young\G5ShopItem;
$g5ShopItem = new G5ShopItem;
$g5ShopItem->all();

관계-Relationships

Laravel을 쓰면서 가장 마음에 들었엇고 또 가장 많이 사용하는 Relation도 넣었습니다.
다만 그누보드는 외래키가 따로 존재하지않고 mb_id 등 AI를 이용한 검색도 하지 않아 직접(…) 각각의 모델에 넣어줬습니다.

한땀한땀….
<?php
use SilNex\GuLa\Models\Gnu\G5Member;
$g5Member = new G5Member;

$admin = $g5Member->where('mb_id', '=', 'admin')->first();
$admin->g5Points()->get();

위에 구현을 통해 계정을 가져온 다음 계정의 포인트를 가져오는 식의 활용이 가능합니다.

G5ModelFactory

그누보드는 게시판에 따라 각각 테이블을 만드는데 이때 만들어진 테이블을 아래의 커스텀 모델로 만들어 사용할 수도 있지만, 코드상으로 factory를 통해 생성할 수 있습니다.

<?php
// Anywhere

use SilNex\GuLa\G5ModelFactory;

// ['connection', 'table_name']
$G5WriteFree = new G5ModelFactory(['gula', 'g5_write_free']);
$G5WriteFree->get();

그누보드 게시판 자동연동

구라이브러리는 그누보드에서 생성되는 g5_write_로 시작하는 게시판 테이블을 G5Member 모델을 통해서 자동으로 연동해줍니다.

<?php
use SilNex\GuLa\Models\Gnu\G5Member;

var_dump(class_exists("\SilNex\GuLa\Gnu\G5WriteFree")); // bool(false)

$g5Member = new G5Member;
$admin = $g5Member->where('mb_id', '=', 'admin')->first();
$admin->g5WriteFree()->first(); // g5_write_free의 mb_id가 admin인 게시글을 가져온다.

커스텀 모델

만약 직접 만드신 테이블 혹은 PG사의 테이블 처럼 이후에 추가되는 경우 직접 추가 하실 수 있습니다.

<?php
// App/CustomG5Model.php

namespace App;

use SilNex\GuLa\G5Model;

class CustomG5Table extends G5Model
{
    protected $table = 'custom_g5_table';

    // code here
}

커멘드 (개발중)

아직 개발중이긴 하지만 artisan 커맨드를 통해 쉽게 G5모델을 만들수 있게 개발중입니다.
대략적인 사용방법은 아래와 같습니다.

php artisan g5model:write free # app/G5Models/G5WriteFree.php 생성
php artisan g5model:pg # PG와 관련된 모델 생성


버그/아이디어 제보

직접 프로젝트에 사용하면서 만들다보니 제가 미처 테스트 못하는 부분은 Github 이슈를 통해서 제보해주시면 감사하겠습니다!

[phpunit] Skip Test with message

PHPunit을 사용해 테스트를 하다보면 추후 기능 구현을 위해 남겨 두었다가 개발을 위해 잠시 테스트를 스킵하고 싶은데,
이에 대한 걸 단순히 주석 처리나, ignore시켜서 하기에는 나중에 나 스스로를 못믿기도 하고(…)
테스트 할때마다 이를 표시하고 싶은데, risk 상태로 두자니 찝찝하고 여러모로 어떻게 처리해야할지 모르는 상황에서 markTestSkipped메소드를 찾게 되었다.

Mark Test Skipped

이 메소드는 이름 그대로 해당 test를 넘겨주는 역할을 한다. 다만, 테스트를 아에 건너 뛰는것이 아닌 해당 테스트가 스킵되었고 어떤 내용으로 스킵되었는지를 표시해준다.

// Test codes ...
    public function testSomeThing(): void
    {
        $this->markTestSkipped('this test skipped because i want');
        // ...
    }
// Test codes ...

위처럼 markTestSkipped는 문자열을 인수로 받는다. skip의 s 로 표시된다.

PHPUnit 8.5.4 by Sebastian Bergmann and contributors.

.S                                                  2 / 2 (100%)

Time: 37.1 seconds, Memory: 28.00 MB

OK, but incomplete, skipped, or risky tests!
Tests: 2, Assertions: 1, Skipped: 1.

markTestIncomplete 와의 차이점

또한 phpunit에는 markTestIncomplete메소드가 있어 해당 테스트가 완료되지 않았다는 것을 표기해 줄 수 있다. 위에 상황을 봣을 땐 이 메소드가 더 어울리는 것같지만, markTestSkippedartisan test를 이용할 때 다른 결과를 보여준다.

위처럼 markTestSkipped는 해당 인수는 아래와 같이 테스트 결과와 함께 표시되며, 인자로 전달된 메시지를 출력해준다. 이를 이용해 왜 해당 테스트가 skip되었는지를 명확하게 표시 해 줄 수 있다.

silnex@localhost:~/test$ artisan test

   WARN  Tests\Feature\MyTest
  ✓ others
  s some thing → this test skipped because i want

  Tests:  1 skipped, 1 passed
  Time:   0.10s
silnex@localhost:~/test$ artisan test

   WARN  Tests\Feature\MyTest
  ✓ others
  i some thing

  Tests:  1 incompleted, 1 passed
  Time:   0.11s

예를 들어 config(‘range’) 의 값이 10 미만일 때 만 skip하고 싶고 왜 스킵되었는 표시해주고 싶을때 사용할수 있다.

[Laravel] Route와 Conroller에서 id를 inject할 때 한 실수

훈련소를 다녀와서 코드를 보고 머리가 돌아가는 속도가 떨어졋다지만 이 정도 일줄은 몰라서 2시간 동안 해멘건데 적어둡니다.

Laravel에선 Route 을 할때 slug를 설정해 컨트롤러에 Model을 넣어줄수 있다. (DI.. 맞나?)
암튼 거기서 실수한 썰을 짦게 남겨 놓습니다.

<?php //web.php
Route::get('/korea/{korea}', 'KoreaController@show');
<?php //KoreaController.php

class KoreaController extender Controller
{
    // ...
    public function show(Korea $korae)
    {
        return $korae->toJson();
    }
    // ...
}

죽어라 /korea/1 로 접근 해도 안되어 미치는 줄 알았는데,
KoreaControllershow메서드를 자세히 보면 ‘korea’가 아니라 ‘korae’ 였다.

즉, Route에서 설정한 {korea}와 Controller에서 입력받는 ‘korae’가 다르기 때문에 ‘Korea’모델을 불러올 수 없었던 거였다.

예ㅖㅖ전에 튜토리얼에서 본 것 같은데 까먹고 있다가 3~4시간 헤메다 겨우 찾았다..

[패치됨] Laravel 7.x – XSS vulnerability

이 포스팅은 laravel 7.0 ~ 7.1.2 까지 버전에 존재하는 취약점 입니다.
사용중이신 버전이 포함되어있다면 laravel 7.1.3이상으로 업데이트 하시기 바랍니다.

Laravel news of XSS vector

laravel-news 에서 7.1.2 미만의 버전에서 XSS 공격 포인트가 있으니 업데이트하라는 소식이 들려와 어느부분이 취약한지 궁금해 해당 부분에 대한 코드를 찾아 봤다.

Github를 의 커밋 로그를 보면 attributesToString 메소드에 $escapeBound 옵션이 추가되었고, sanitizeComponentAttribute란 static 메소드가 추가되었다.

sanitizeComponentAttribute 메소드는 입력된 값이 문자열이거나, object 인경우 __toString 매직 메소드가 존재하면 e()(laravel의 html 이스케이프 함수)로 string을 소독(?) 하는 메소드이다.

근데 x-blade 문법에서 attributes->merge하는 메소드에서는 해당 코드가 적용되지 않아 tag 부분을 이스케이프 할 수 있어 XSS 공격이 가능해진다.

XSS code

<!-- resources/views/welcome.blade.php -->
<x-xss></x-xss>
<!-- resources/views/components/xss.blade.php -->
<div {{ $attributes->merge(['class' => request()->css]) }}>XSS able</div>

URI : http://127.0.0.1:8000/?css="><script>alert(1)</script>

<!-- Result -->
<div class="\"><script>alert(1)</script>">XSS able</div>
XSS in laravel 7.1.2

위와 같이 XSS가 실행되는 것을 볼 수 있다.

Patch

메일 보내고 1시간 만에 패치하고 릴리즈 되었다 ㄷㄷ;

7.1.3 버전에서 패치되었으며[commit] composer update를 하면 자동으로 패치되어진다.

나도 업데이트에서 언급 해줫으면 좋겟다. 나도 찾은건 찾은건데… ㅜ

Laravel 6 to 7 Upgrade

3월 3일 laravel 7이 정식 릴리즈 되면서 기대하던 blade-x, stub, custom casts 등과 같은 기능들이 추가되었다.

지금 열심히 modern php 멤버 분들께서 도큐먼트를 번역중이시니 함께 번역을 하는것은 어떨가 싶다.

이번 글에선 Laravel 6에서 7으로 업데이트할 때 수정되야 하는 사항들을 간략히 정리해보겠다.

composer.json

  1. “php” 의 버전을  “^7.2.5”,
  2. “laravel/framework” 의 버전을 “^7.0″으로
  3. “nunomaduro/collision” 의 버전을 “^4.1″로
  4. “phpunit/phpunit” 의 버전을 “^8.5″로
  5. “facade/ignition” 의 버전을 “^2.0″으로

app/Exceptions/Handler.php

위에서 부터 순서대로
# 6.x
7.x

변경 사항이다.

<?php
# use Exception;
use Throwable;
// ...
# public function report(Exception $exception)
public function report(Throwable $exception)
// ...
# public function render($request, Exception $exception)
public function render($request, Throwable $exception)
// ...

위와같이 composer.json과 Handler.php 파일을 수정했다면
compsoer update를 통해 laravel 7으로 넘어가도록 하자!

[Laravel 7] Zttp

원문: laravel-news

Zttp is coming to Laravel 7

Adam Wathan가 만든 Zttp(Guzzle wapper) 패키지는 Laravel 7에서 새로운 Http 패키지로 선보여 집니다.

이 기능을 추가하기위한 pull request를 보면 Guzzle를 사용할 때보다 약 90%정도 더 좋은 구문을 제공합니다.

새로운 Http에 기본적인 사용 방법에 대한 예시입니다.

use Illuminate\Support\Facades\Http;

$response = Http::post('url', [
    'name' => 'Taylor',
]);

echo $response['foo'];

$response->body()
$response->json()
$response->status()
$response->ok()
$response->successful() (>= 200 &amp;&amp; < 300)
$response->serverError()
$response->clientError()

Taylor의 PR내용을 보면, 이 패키지는 완전 새로운 클라이언트가 아닌 Guzzle 윗 레이어에서 UX/DX를 좀더 펼리하게 해주는 역할을 한다고 합니다.

이 패키지는 복잡하고 많은 내용이 추가되는것이 아닙니다. 그리고 만약 더 많은 것들이 필요하다면 Guzzle를 직접 사용할 수 있습니다.

새로운 HTTP 패키지에 대한 내용을 알고 싶으면 pull request를 확인해보세요.


새로운 Http 패키지 분석 (?)

Taylor의 PR를 보면 “Guzzle의 모든 기능을 이 API를 통해 제공하려는게 아니다.”라고 적혀있는걸 보면 말 그대로 TEST나 간단한 Http request정도만 지원하고 나머지는 Guzzle를 직접 적으로 사용하라는것같다.

더 많은 예시

새로운 Http 패키지의 기본적인 content-typeapplication/json이라고 한다.
하지만 아래와 같은 코드로 쉽게 form-urlencoded요청을 보낼 수 있다.

$response = Http::asForm()->post('url', [
    'name' => 'Taylor',
]);

그리고 multi-part타입으로 파일을 보낼 수도 있다.

$response = Http::attach('name', 'stream/contents', 'filename.txt')->post('url');

해더 / 인증정보 (Authentication)

해더는 withHeaders 메소드로 보낼 수 있다.

$response = Http::withHeaders(['X-Foo' => 'bar'])->post('url', [
    'name' => 'Taylor',
]);

인증 / bearer 과 같은 토큰 정보는 withToken메소드로 보낼 수 있다.

$response = Http::withToken('token')->post('url', [
    'name' => 'Taylor',
]);

Basic Authenication은 withBasicAuth메소드로 보낼 수 있다.

$response = Http::withBasicAuth('username', 'password')->post('url', [
    'name' => 'Taylor',
]);

에러

ZTTP와 같이 기본적으로 서버, 클라이언트 에러를 출력하지 않는다고 한다.
하지만 만약 HTTP응답이 실패했을때 $response->throw()를 통해 예외 처리를 할 수 있다.

$response = Http::post(...);

if (! $response->successful()) {
    $response->throw();
}

Testing / Faking

Http::fake()를 지원하며, 기본적으로 비여있는 200응답을 한다.

Http::fake();

$response = Http::post('url');

Http::fake()는 인자로 Array를 받으며, 패턴이 일치하는 경우는 Http::response를 통해 응답을 만들어 낼 수 있으며, 만약 일치되는 패턴이 없다면, 실제로 실행된다.

Http::fake([
    'github.com/*' => Http::response([1, 2, 3], 200, ['Headers']),
]);

만약 모든 패턴에 대해서 실행되는것을 막고 싶다면 아래와 같이 작성할 수 있다.

Http::fake([
    'github.com/*' => Http::response([1, 2, 3], 200, ['Headers']),
    '*' => Http::response('', 200),
]);

연속적인 응답은 Http::sequentce를 이용해 지정된 순서에 맞게 응답이 출력된다.

Http::fake([
    '*' => Http::sequence([Http::response('foo', 200), Http::response('bar', 200)]),
]);

마지막으로 Http::fake()는 클로저를 지원한다.

Http::fake(function ($request) {
    return Http::response('foo', 200);
});

잡-설

잘 모르는 영어와 개발 코드를 보니 복잡하네요..
새로운 패키지가 들어오는건 좋지만 아직도 배울게 한참많은데
배워야 될것들이 계속 늘어나는걸 보면,, ㅜㅜ

[Laravel 7] Custom mutators

TL; TR

Laravel 7.x에선 모델의 Property인 $casts에 커스텀된 클래스를 전달해줄 수 있다.[링크]

기존의 Mutators

Laravel 7.0의 공개 일자는 2020년 2월로 현재 얼마 남지 않은 상황에 laravel.com/doc/master에 차근차근 새로운 기능들에 대한 문서들도 업데이트 되는 것 같다.

이번에 getVarNameAttribute()setVarNameAttribute($value) 를 사용하면서 mutator에 대한 커스텀은 안될까 하여 찾아보다가 master문서에 custom mutators 항목이 추가된 것을 발견하고 곧 7.0이 릴리즈 될테니 테스트겸 시도해보았다.

Laravel 7 dev 버전에 대한 설치는
laravel new laravel7 --dev 또는
composer create-project --prefer-dist laravel/laravel:7.x-dev laravel7 으로 설치 할 수 있다.

$ cd laravel7
$ php artisan --version
Laravel Framework 7.x-dev

위와같이 artisan 명령어로 버전을 확인할 수 있다.
과거엔 welcome 템플릿에 laravel 7.0 (DEV) 🚀 라고 적혀있엇는데 아마 곧 공개를 앞으도 바뀐것같다.

++ User 테이블로 Custom mutator를 테스트 해보려고 하는데 laravel/ui 가 설치되지 않는다. 찾아보니 laravel7.x 부턴 laravel/ui에 login, register 등의 컨트롤러가 포함되어 배포되기에 laravel/ui 2.x를 설치해 줘야한다.
composer requrie laravel/ui:2.x-dev --dev

기존에 mutators의 경우 array, datetime 등의 일부만 작동을 했고 만약 커스텀하게 짜기위해선 아래와 같이 해당 모델에 get, set~~Attribute 메소드를 짜줘야했다.

// App/User

public function getNameAttribute()
{
    return "my name is {$this->name}";
}

public function setNameAttribute($value)
{
    $this->name = ucfirst($value);
}

하지만 여러 테이블에서 사용될 때 반복되고 모델에도 만약 수정이 필요할 때 모델별로 다 수정해줘야하는 번거로움이 생긴다 (trait를 써도되지만 일단넘어가자..)

이번 Laravel 7.x 에선 Illuminate\Contracts\Database\Eloquent\CastsAttributes 클래스를 사용해서 datetime, array와 같인 $casts 프로퍼티에 지정할수있다.

App\Casts\Json

<?php

namespace App\Casts;

use Illuminate\Contracts\Database\Eloquent\CastsAttributes;

class Json implements CastsAttributes
{
    /**
     * Cast the given value.
     *
     * @param  \Illuminate\Database\Eloquent\Model  $model
     * @param  string  $key
     * @param  mixed  $value
     * @param  array  $attributes
     * @return array
     */
    public function get($model, $key, $value, $attributes)
    {
        return json_decode($value, true);
    }

    /**
     * Prepare the given value for storage.
     *
     * @param  \Illuminate\Database\Eloquent\Model  $model
     * @param  string  $key
     * @param  array  $value
     * @param  array  $attributes
     * @return string
     */
    public function set($model, $key, $value, $attributes)
    {
        return json_encode($value);
    }
}

App\User

<?php

namespace App;

use App\Casts\Json;
use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    /**
     * The attributes that should be cast to native types.
     *
     * @var array
     */
    protected $casts = [
        'options' => Json::class,
    ];
}

위와 같이 CastsAttributes 클래스를 받고 해당 클래스를 $casts로 전달해주면 자동으로 맵핑해준다.

지금은 casts를 따로 생성하는 artisan명령어는 없는것같지만, laravel 7이 릴리즈되면 추가되지 않을까싶다.