31 Dec 2019
|
PHP
TDD
Laravel
coderstape
Coder’s tape의 Test Driven Laravel 강의 를 듣고 정리한 포스팅 입니다.
1. Update 테스트 코드 구현
데이터를 업데이트 시키는 기본 코드 작성
/** @test */
public function a_book_can_be_updated()
{
$this->withoutExceptionHandling();
$this->post('/books', [
'title' => 'cool book title',
'author' => 'harry'
]);
$response = $this->patch('/books', [
'title' => 'New Title',
'author' => 'Victor',
]);
$this->assertEquals('New Title', Book::first()->title);
$this->assertEquals('harry', Book::first()->author);
}
테스트를 동작시키면, 라우트에 대하여 PATCH가 지원되지않는다는 에러 발생. 따라서 라우트를 추가시킴.
//Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException : The PATCH method is not supported for this route. Supported methods: POST.
Route::patch('/books', 'BooksController@update');
컨트롤러에 update 메소드 기본 코드 작성.
public function update()
{
$data = request()->validate([
'title'=>'required',
'author'=> 'required',
]);
}
하지만, 현재 코드로도 여전히 에러가 발생한다. 업데이트 로직을 구현하기 위해서는 어떤 데이터에 대하여 업데이트를 할 건지를 명시해줘야한다. 이때 데이터의 id값을 사용.
/** @test */
public function a_book_can_be_updated()
{
$this->withoutExceptionHandling();
$this->post('/books', [
'title' => 'cool book title',
'author' => 'harry'
]);
// book 클래스 인스턴스 추가
$book = Book::first();
//id값을 variable routing으로 사용
$response = $this->patch('/books/' . $book->id, [
'title' => 'New Title',
'author' => 'Victor',
]);
라우트와 컨트롤러를 적절히 수정시킴.
Route::patch('/books/{book}', 'BooksController@update');
public function update(Book $book) // route model binding
{
$data = request()->validate([
'title'=>'required',
'author'=> 'required',
]);
$book->update($data);
}
2. Create & Update 테스트 코드 및 로직 구현
결과적으로, 데이터를 생성하고 업데이트하는 로직을 성공적으로 아래와 같이 구현 완료
- 데이터 생성
- 제목, 작가명 유효성 검증
- 데이터 업데이트
// BooksController.php
namespace App\Http\Controllers;
use App\Book;
use Illuminate\Http\Request;
class BooksController extends Controller
{
public function store()
{
$data = request()->validate([
'title'=>'required',
'author'=> 'required',
]);
Book::create($data);
}
public function update(Book $book)
{
$data = request()->validate([
'title'=>'required',
'author'=> 'required',
]);
$book->update($data);
}
}
//web.php
Route::post('/books', 'BooksController@store');
Route::patch('/books/{book}', 'BooksController@update');
// BookManagementTest.php
namespace Tests\Feature;
use App\Book;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class BookReservationTest extends TestCase
{
use refreshDatabase;
/** @test */
public function a_book_can_be_added_to_the_library()
{
$this->withoutExceptionHandling();
$response = $this->post('/books', [
'title' => 'cool book title',
'author' => 'harry'
]);
$response->assertOk();
$this->assertCount(1, Book::all());
}
/** @test */
public function a_title_is_required()
{
// $this->withoutExceptionHandling();
$response = $this->post('/books', [
'title' => '',
'author' => 'harry'
]);
$response->assertSessionHasErrors('title');
}
/** @test */
public function an_author_is_required()
{
$response = $this->post('/books', [
'title' => 'cool book title',
'author' => ''
]);
$response->assertSessionHasErrors('author');
}
/** @test */
public function a_book_can_be_updated()
{
$this->withoutExceptionHandling();
$this->post('/books', [
'title' => 'cool book title',
'author' => 'harry'
]);
$book = Book::first();
$response = $this->patch('/books/' . $book->id, [
'title' => 'New Title',
'author' => 'victor',
]);
$this->assertEquals('New Title', Book::first()->title);
$this->assertEquals('victor', Book::first()->author);
}
}
3. 리팩토링
유효성 검증하는 코드가 반복적으로 사용되고 있음. 이 코드를 extract 시켜 별도의 메소드로 구현
$data = request()->validate([
'title'=>'required',
'author'=> 'required',
]);
public function store()
{
$data = $this->validateRequest();
Book::create($data);
}
public function update(Book $book)
{
$data = $this->validateRequest();
$book->update($data);
}
public function validateRequest()
{
return request()->validate([
'title' => 'required',
'author' => 'required',
]);
}
$data를 인라인으로 변경하는 것도 가능
// before
$data = $this->validateRequest();
$book->update($data);
// after
$book->update($this->validateRequest());
리팩토링 최종 완성된 코드는 다음과 같음.
class BooksController extends Controller
{
public function store()
{
Book::create($this->validateRequest());
}
public function update(Book $book)
{
$book->update($this->validateRequest());
}
public function validateRequest()
{
return request()->validate([
'title' => 'required',
'author' => 'required',
]);
}
}
30 Dec 2019
|
PHP
TDD
Laravel
coderstape
Coder’s tape의 Test Driven Laravel 강의 를 듣고 정리한 포스팅 입니다.
1. PHPUnit Setup
.env에서 데이터베이스를 sqlite로 설정
sqlite 데이터베이스 생성
touch database/database.sqlite
2. Create(store) 테스트 코드 구현
Tests/Feature/BookReservationTest.php 생성 후, 데이터를 추가하는 기본코드 작성
php artisan make:test bookReservationTest
class BookReservationTest extends TestCase
{
/** @test */
public function a_book_can_be_added_to_the_library()
{
$response = $this->post('/books', [
'title' => 'cool book title',
'author' => 'harry'
]);
$response->assertOk();
$this->assertCount(1, book::all());
}
}
withoutExceptionHandling
는 phpuit test 결과로 어떠한 에러가 발생했는지를 알려줌.
$this->withoutExceptionHandling();
라우터 및 컨트롤러 생성
Route::post('/book', 'BooksController@store');
php artisan make:controller BooksController
데이터를 생성하여 저장시키기 위해, 컨트롤러 내 store 메소드생성
class BooksController extends Controller
{
public function store()
{
}
}
모델 생성 & 마이그레이션 적용. 이후, BookReservationTest.php 및 BooksController 에 모델 Book을 import해줌.
php artisan make:model Book -m
또한, use refreshDatabase;
를 테스트 파일 내부에서 import시킴. 이 코드는 테스트를 동작시킬 때 마다 데이터베이스를 초기화 시키는 역할을 함.
use App\Book;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class BookReservationTest extends TestCase
{
use refreshDatabase;
모델에 mass assignment 설정
class Book extends Model
{
protected $guarded = [];
}
마이그레이션에 title, author 추가
public function up()
{
Schema::create('books', function (Blueprint $table) {
$table->bigIncrements('id');
$table->string('title');
$table->string('author');
$table->timestamps();
});
}
3. 유효성 검증 테스트 코드 작성
제목 유효성 검증에 대한 테스트케이스 작성
/** @test */
public function a_title_is_required()
{
$this->withoutExceptionHandling();
$response = $this->post('/books', [
'title' => '',
'author' => 'harry'
]);
$response->assertSessionHasErrors('title');
}
컨트롤러에 유효성 검증에 대한 코드 추가
class BooksController extends Controller
{
public function store()
{
$data = request()->validate([
'title'=>'required',
'author'=> 'required'
]);
Book::create($data);
}
}
모든 프로세스가 완료 되었으나, withoutExceptionHandling()
로 에러메세지가 반환되므로 이 부분은 주석처리
/** @test */
public function a_title_is_required()
{
//$this->withoutExceptionHandling();
$response = $this->post('/books', [
'title' => '',
'author' => 'harry'
]);
$response->assertSessionHasErrors('title');
}
마찬가지로, 작가명이 누락된 테스트케이스도 작성 가능
/** @test */
public function an_author_is_required()
{
// $this->withoutExceptionHandling();
$response = $this->post('/books', [
'title' => 'cool book title',
'author' => ''
]);
$response->assertSessionHasErrors('author');
}
29 Dec 2019
|
Laracast
PHP
Laracasts - The PHP Practitioner 강의를 듣고 정리한 포스팅 입니다.
0. GET STARTED
기본 코드는 다음과 같음
class Post
{
public $title;
public $author;
public $published;
public function __construct($title, $author, $published)
{
$this->title = $title;
$this->author = $author;
$this->published = $published;
}
}
$posts = [
new Post('My first post', 'harry', true),
new Post('My second post', 'harry', true),
new Post('My third post', 'mat', true),
new Post('My fourth post', 'simon', false),
];
PHP에서 array를 활용할 수 있는 메서드들이 매우 많음. 그 중 대표적인 것이 아래.
1. array-filter
filter down the array - array_filter(array, closure function)
$unpublishedPosts = array_filter($posts, function ($post) {
// return $post -> published === false;
return ! $post -> published;
});
$publishedPosts = array_filter($posts, function ($post) {
return $post -> published;
});
var_dump($unpublishedPosts);
var_dump($publishedPosts);
2. array_map
map over the array(change the status). It is useful when you need to modify what gets refunded
array_map(closure function, array)
// map over the array -> change the status - array_map(closure, array)
// when you need to modify what gets returned.
$modified = array_map(function($post) {
$post->published = true;
return $post;
}, $posts);
var_dump($modified);
// 응용 1. (array) -> return each element as array ( this case, obj -> array)
$modified = array_map(function($post){
return (array) $post;
}, $posts);
var_dump($modified);
// 응용 2.
$modified = array_map(function($post){
return ['title' => $post->title];
}, $posts);
var_dump($modified);
// 응용 3. map 메서드는 foreach 로도 표현 가능
foreach ($posts as $post) {
$post->published = true;
}
var_dump($posts);
3. array_column
extract specific column from the array(object)
array_column(array, column)
// 특정 컬럼만을 추출할 때 유용. 오브젝트, 배열 모두에 사용 가능
$titles = array_column($posts, 'title');
// 응용) 3번째 인자로, key 값에 해당하는 컬럼을 지정할 수 있음.
// 'my first post' : "harry"
$titles = array_column($posts, 'author', 'title');
var_dump($titles);
29 Dec 2019
|
Laracast
PHP
Laracasts - The PHP Practitioner 강의를 듣고 정리한 포스팅 입니다.
1. Template 상속
views/partial : 재사용가능한 template(HTML)을 추출하여 partial 폴더에서 관리할 수 있음.
<?php require('partials/nav.php); ?>
- DJango에서 템플릿 확장/상속(base template)과 같은 개념임.
<body>
<?php require('partials/nav.php'); ?>
<nav>
<ul>
<li><a href="/about">About Page</a></li>
<li><a href="/about/culture">About our Culture</a></li>
<li><a href="/contact">Contact Page</a></li>
</ul>
</nav>
마찬가지로, partial의 개념을 이용하여 head, footer의 재사용되는 코드를 따로 빼서 새로운 php 파일로 관리한 후 그 파일을 불러오는 방식으로 어플리케이션 구성을 다듬을 수 있다.
- partials 폴더 내 head.php, footer.php 생성
- head.php내 포함된 nav.php 경로는 상대경로이므로 수정
- CSS는 public/css/style.css를 새로 만들어 관리
// head.php
<!<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
<link rel="stylesheet" type="text/css" href="/public/css/style.css">
</head>
<body>
<?php require('nav.php'); ?>
// footer.php
</body>
</html>
// about.view.php
<?php require('partials/head.php') ?>
<h1>About Us</h1>
<?php require('partials/footer.php') ?>
29 Dec 2019
|
Laracast
PHP
Laracasts - The PHP Practitioner 강의를 듣고 정리한 포스팅 입니다.
1. Router
어플리케이션의 url을 관장하는 역할(Django의 urls.py). 어플리케이션의 구조를 아래와 같이 좀 더 구체화시킴
- Views: Template의 역할. 브라우저에 보여지는 부분
- Controllers: router에 따라 연결된 controller를 호출하며 각 controller는 정해진 로직에 따라 views와 연결.
- Core: Database, boostrap, 추가적인 클래스 정의
routes.php: root directory에 생성. 연습을 위해 “about”, “about-culture”, “contact”, “index” 페이지에 대한 views, controller, routes를 형성함.
1-1. Controllers & views의 예시
// controllers/about.php
<?php
require 'views/about.view.php';
//views/about.php
<body>
<h1>About Us</h1>
</body>
2. Routes.php
<?php
$router->define([
'' => 'controllers/index.php',
'about' => 'controllers/about.php',
'about/culture' => 'controllers/about-culture.php',
'contact' => 'controllers/contact.php'
]);
//아래와 같이 다양한 방법으로도 표현 가능
//$router->define('', 'controllers/index.php');
//$router->define('about', 'controllers/index.php');
// Router::define('', 'controllers/index.php');
//$router->define('', 'controllers/index.php');
3. core/bootstrap.php
router 관련 로직을 core/Router.php
내 router
클래스에서 구현 & bootstrap.php
와 연결
Request.php
: 서버 내 요청된 URI
를 trim
하는 클래스가 구현된 파일
<?php
$app = [];
$app['config'] = require 'config.php';
require 'core/Router.php';
require 'core/Request.php';
require 'core/database/Connection.php';
require 'core/database/QueryBuilder.php';
$app['database'] = new QueryBuilder(
Connection::make($app['config']['database'])
);
4. core/Router.php
define
: $routes
를 프로퍼티에 저장하는 메서드
load
: $file
은 routes.php
의미함. routes.php
의 로직에 따라 ($router->define([])
) 의 routing값들이 $routes
에 저장됨.
load
는 스태틱 메서드이므로 인스턴스를 생성하지 않음. 따라서, new static
또는 new self
를 사용해야함.
- 인스턴스를 생성하지 않으므로
$this
는 사용이 불가능함. 따라서, new static
이 저장된 $router
을 return 함.
direct
: 들어온 URI가 실제로 존재하는지 유무를 프로퍼티와 대조하여 검증함 (array_key_exists
) . 존재할 경우, 대응되는 URI를 반환시킴. 존재하지 않을 경우 Throw
<?php
class router
{
protected $routes = [];
public static function load($file) // $file: routes.php
{
// static method는 인스턴스를 생성하지 않는 global method.
// 인스턴스 생성을 위해서는 new static (or new self)
$router = new static;
require $file;
// return $this; static method는 인스턴스를 생성하지 않으므로 $this는 사용이 불가능 함.
return $router;
}
public function define($routes)
{
$this->routes = $routes;
}
public function direct($uri)
{
// about/culture
// 들어온 uri와 매칭되는 key값이 있는지를 routes.php에서 검
if (array_key_exists($uri, $this->routes)) {
return $this->routes[$uri];
}
// 존재하지 않을 경우 Throw
throw new Exception('No route defined for this uri');
}
5. Request.php
<?php
class request
{
public static function uri()
{
return trim($_SERVER['REQUEST_URI'],'/');
}
}
6. index.php
bootstrap.php
를 호출함으로써, router
, request
클래스 등 router에 필요한 모든 로직을 불러올 수 있음.
<?php
require 'core/bootstrap.php';
// option 1.
$router = new Router; // Router 클래스는 bootstrap과 연결되어 있음.
require 'routes.php'; // routes (uri list)를 불러오며 $route(route 인스턴스)의 프러퍼티에 경로 저장.
$uri = trim($_SERVER['REQUEST_URI'],'/'); // URI 경로 trim
require $router->direct($uri); // Router 클래스의 메서드: routes 존재 여부 검사후 redirect.
//die(var_dump($app));
// option 2.
$router = Router::load('routes.php');
require $router-> direct($uri);
// option 3.
require Router::load('routes.php')
->direct(Request::uri());