2024년 7월 8일 월요일

Laravel : Laravel8 + Vuejs2 + Clean Architecture

Laravel : Laravel8 + Vuejs2 + Clean Architecture

Laravel8 + Vuejs2 + Clean Architecture

1. 먼저 간단히 php 에서 전형적이고 심플한 형태로 제작해 보자.

라라벨 type hine 로 자동으로 객체주입이 가능하다. 놀랍다.

Entities - User.php
Repositories - UserRepository.php
UseCases -GetUserUseCase.php
폴더를 만들자.

각각의 파일들은 다음과 같을 것이다.

// app/Entities/User.php
namespace App\Entities;

class User {
    public $id;
    public $name;
    public $email;
    public function __construct($id, $name, $email) {
        $this->id = $id;
        $this->name = $name;
        $this->email = $email;
    }
}
// app/Repositories/UserRepository.php
namespace App\Repositories;

use App\Entities\User;

class UserRepository {
    public function find($id) {
        // 데이터베이스에서 사용자 찾기 로직
        return new User(1,"dxninja","dxninja@email.com");
    }

    public function save(User $user) {
        // 데이터베이스에 사용자 저장 로직
    }
}

// app/UseCases/GetUser.php
namespace App\GetUserUseCase;

use App\Repositories\UserRepository;

class GetUserUseCase {
    protected $userRepository;

    public function __construct(UserRepository $userRepository) {
        $this->userRepository = $userRepository;
    }

    public function execute($id) {
        return $this->userRepository->find($id);
    }
}

이제, Controller 에서 사용해보자

$user = $this->getUser->execute("userid");

Koin보다 간단하다. 역시나 막 사용하는 php !!

2. Service Provider 로 좀더 유연하고 클린하게 만들어보자.

위의 1에서는 간단하게 의존성 주입이 되었지만 직접개체를 주입하는 방식이라 유연하지 못했다. 좀더 클린하고 결합도를 낮추고 인터페이스 분리를 하기 위해서 인터페이스를 주입하여 객체의 변경과 확장에도 대응하도록 하자.

IUserRepository와 구현체를 만들자.

namespace App\Repositories;

use App\Entities\User;

interface IUserRepository {
    public function find($id): ?User;
    public function save(User $user): void;
}


namespace App\Repositories;

use App\Entities\User;

class UserRepository implements IUserRepository {
    public function find($id): ?User {
        // 데이터베이스에서 사용자 찾기 로직
        return new User(1, "dxninja", "dxninja@email.com");
    }

    public function save(User $user): void {
        // 데이터베이스에 사용자 저장 로직
    }
}

IUseCase와 구현체를 만들자.


namespace App\UseCases;

interface IUseCase {
    public function execute($id);
}


namespace App\UseCases;

use App\Repositories\IUserRepository;

class GetUserUseCase implements IUseCase {
    protected $userRepository;

    public function __construct(IUserRepository $userRepository) {
        $this->userRepository = $userRepository;
    }

    public function execute($id) {
        return $this->userRepository->find($id);
    }
}

Repository와 UseCase의 서비스 프로바이더 만들자.


namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use App\Repositories\IUserRepository;
use App\Repositories\UserRepository;

class RepositoryServiceProvider extends ServiceProvider {
    public function register() {
        $this->app->singleton(IUserRepository::class, UserRepository::class);
    }

    또는 defer가능하도록.

     public function register() {
        $this->app->singleton(IUseCase::class, function ($app) {
            return new GetUserUseCase($app->make(IUserRepository::class));
        });
    }

    public function provides() {
        return [IUseCase::class];
    }
}

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use App\UseCases\IUseCase;
use App\UseCases\GetUserUseCase;
use App\Repositories\IUserRepository;

class UseCaseServiceProvider extends ServiceProvider {
    public function register() {
        $this->app->singleton(IUseCase::class, function ($app) {
            return new GetUserUseCase($app->make(IUserRepository::class));
        });
    }

    또는 defer가능하도록

    public function register() {
        $this->app->singleton(IUserRepository::class, UserRepository::class);
    }

    public function provides() {
        return [IUserRepository::class];
    }
}

서비스 프로바이더를 라라벨에 등록하자.


// config/app.php

'providers' => [
    // Other Service Providers

    App\Providers\RepositoryServiceProvider::class,
    App\Providers\UseCaseServiceProvider::class,
],

Controller에서 IUseCase를 주입받도록 하자.

namespace App\Http\Controllers;

use App\UseCases\IUseCase;
use Illuminate\Http\Request;

class UserController extends Controller {
    protected $getUser;

    public function __construct(IUseCase $getUser) {
        $this->getUser = $getUser;
    }

    public function show($id) {
        $user = $this->getUser->execute($id);
        return view('user.show', compact('user'));
    }
}

라우터가 아래처럼 되어있다고 치자.


use App\Http\Controllers\UserController;

Route::get('/users/{id}', [UserController::class, 'show']);

내친김에 테스트 코드도 만들자

namespace Tests\Unit\UseCases;

use Tests\TestCase;
use App\UseCases\GetUserUseCase;
use App\Repositories\IUserRepository;
use App\Entities\User;
use Mockery;

class GetUserUseCaseTest extends TestCase {
    public function testExecute() {
        $userRepositoryMock = Mockery::mock(IUserRepository::class);
        $userRepositoryMock->shouldReceive('find')
            ->with(1)
            ->andReturn(new User(1, 'dxninja', 'dxninja@email.com'));

        $getUserUseCase = new GetUserUseCase($userRepositoryMock);
        $user = $getUserUseCase->execute(1);

        $this->assertInstanceOf(User::class, $user);
        $this->assertEquals(1, $user->id);
        $this->assertEquals('dxninja', $user->name);
        $this->assertEquals('dxninja@email.com', $user->email);
    }

    protected function tearDown(): void {
        Mockery::close();
        parent::tearDown();
    }
}



namespace Tests\Feature\Http\Controllers;

use Tests\TestCase;
use App\Http\Controllers\UserController;
use App\UseCases\IUseCase;
use App\Entities\User;
use Mockery;

class UserControllerTest extends TestCase {
    public function testShow() {
        $user = new User(1, 'dxninja', 'dxninja@email.com');
        
        $useCaseMock = Mockery::mock(IUseCase::class);
        $useCaseMock->shouldReceive('execute')
            ->with(1)
            ->andReturn($user);

        $controller = new UserController($useCaseMock);
        $response = $controller->show(1);

        $this->assertEquals('user.show', $response->name());
        $this->assertEquals($user, $response->getData()['user']);
    }

    protected function tearDown(): void {
        Mockery::close();
        parent::tearDown();
    }
}

namespace Tests\Feature\Api;

use Tests\TestCase;
use App\Entities\User;
use Mockery;
use App\UseCases\IUseCase;

class UserApiTest extends TestCase {
    public function testGetUser() {
        $user = new User(1, 'dxninja', 'dxninja@email.com');

        $useCaseMock = Mockery::mock(IUseCase::class);
        $useCaseMock->shouldReceive('execute')
            ->with(1)
            ->andReturn($user);

        $this->app->instance(IUseCase::class, $useCaseMock);

        $response = $this->getJson('/api/users/1');

        $response->assertStatus(200)
                 ->assertJson([
                     'id' => $user->id,
                     'name' => $user->name,
                     'email' => $user->email,
                 ]);
    }

    protected function tearDown(): void {
        Mockery::close();
        parent::tearDown();
    }
}

3. Vuejs에서도 구현하자. (개선중)

src/
├── components/
│   └── UserComponent.vue
├── entities/
│   └── User.js
├── repositories/
│   ├── IUserRepository.js
│   └── UserRepository.js
├── usecases/
│   └── GetUserUseCase.js
├── services/
│   └── container.js
├── views/
│   └── UserView.vue
└── main.js

// container.js inversify 

import { Container } from "inversify";
import "reflect-metadata";
import IUserRepository from '../repositories/IUserRepository';
import UserRepository from '../repositories/UserRepository';
import GetUserUseCase from '../usecases/GetUserUseCase';

const container = new Container();
container.bind(IUserRepository).to(UserRepository);
container.bind(GetUserUseCase).toSelf();

export default container;

// Entity

export default class User {
    constructor(id, name, email) {
        this.id = id;
        this.name = name;
        this.email = email;
    }
}



// Repository

export default class IUserRepository {
    find(id) {
        throw new Error("Method not implemented.");
    }
}

import IUserRepository from './IUserRepository';
import User from '../entities/User';

export default class UserRepository extends IUserRepository {
    find(id) {
        // 여기서 실제 데이터베이스 호출을 구현합니다.
        return new User(1, "dxninja", "dxninja@email.com");
    }
}

// UseCase

export default class GetUserUseCase {
    constructor(userRepository) {
        this.userRepository = userRepository;
    }

    execute(id) {
        return this.userRepository.find(id);
    }
}

// Presenter

<template>
  <div>
    <p>User ID: {{ user.id }}</p>
    <p>User Name: {{ user.name }}</p>
    <p>User Email: {{ user.email }}</p>
  </div>
</template>

<script>
import { inject } from 'inversify-vue';
import GetUserUseCase from '../usecases/GetUserUseCase';

export default {
  name: 'UserComponent',
  data() {
    return {
      user: {}
    };
  },
  async created() {
    const getUserUseCase = inject(GetUserUseCase);
    this.user = await getUserUseCase.execute(1);
  }
};
</script>


// View

<template>
  <div>
    <UserComponent />
  </div>
</template>

<script>
import UserComponent from '../components/UserComponent.vue';

export default {
  name: 'UserView',
  components: {
    UserComponent
  }
};
</script>


// main.js

import Vue from 'vue';
import App from './App.vue';
import container from './services/container';
import { Provider } from 'inversify-vue';

Vue.config.productionTip = false;

new Vue({
  render: h => h(App),
  provide: {
    container
  }
}).$mount('#app');



// testcode


import 'reflect-metadata';
import { Container } from 'inversify';
import IUserRepository from '../../src/repositories/IUserRepository';
import UserRepository from '../../src/repositories/UserRepository';
import GetUserUseCase from '../../src/usecases/GetUserUseCase';
import User from '../../src/entities/User';

test('GetUserUseCase executes correctly', () => {
  const container = new Container();
  container.bind(IUserRepository).to(UserRepository);
  container.bind(GetUserUseCase).toSelf();

  const getUserUseCase = container.get(GetUserUseCase);
  const user = getUserUseCase.execute(1);

  expect(user).toBeInstanceOf(User);
  expect(user.id).toBe(1);
  expect(user.name).toBe("dxninja");
  expect(user.email).toBe("dxninja@email.com");
});



0 comments:

댓글 쓰기