実践Laravel
yuuki takezawa (ytake)
istyle inc.
profile
• 竹澤 有貴 / ytake
• 株式会社アイスタイル
• twitter https://twitter.com/ex_takezawa
• facebook https://www.facebook.com/yuuki.takezawa
• github https://github.com/ytake
author
Agenda
• About Dependency Injection
• 堅実なアプリケーションにするBest Practice
Dependency Injection
Interface vs Concrete
class Process
{
protected $ls;
public function __construct()
{
$this->ls = new Ls();
}
public function execute()
{
}
}
class Ls
{
public function invoke(): string
{
return 'ls';
}
}
このProcessクラスは、
lsに依存
他のコマンドを実装する場合、
OO Processクラスをたくさん
作成しなければならない
Lsクラスの実行メソッドはinvoke他のクラ
スの実行メソッドは違うかもしれない
Interface vs Concrete
class Command
{
public function process()
{
(new Process())->execute();
(new HogeProcess())->handle();
(new FugaProcess())->dispatch();
}
} クラスによって利用方法が異なる
各コマンドによる利用方法の違いを
知っていなければ利用できない
Interface vs Concrete
interface ProcessCommandInterface
{
/**
* @return string
*/
public function command(): string;
}
コマンドを提供
このインターフェースを実装するクラスは、
このメソッドを実装しなければならない
Interface vs Concrete
class Ls implements ProcessCommandInterface
{
public function command(): string
{
return 'ls';
}
}
Interface vs Concrete
class Process
{
protected $command;
public function __construct(
ProcessCommandInterface $command
) {
$this->command = $command;
}
public function execute()
{
$this->command->command();
}
}
このインターフェースを
実装したクラスであればなんでも良い
このメソッドは必ず存在する
Interface vs Concrete
class Command
{
protected $commandClass = [
Ls::class,
Rm::class,
];
public function process()
{
foreach($this->commandClass as $class) {
(new Process(new $class()))->execute();
}
}
}
一括で操作可能
外部から渡されるクラスによって
振る舞いが変わる
設定値として外から指定することも可
Laravel Service Container
• 外から実行クラスを指定する機能を元に設計
• インターフェースと具象クラスの指定は
Service Providerに記載
• フレームワークの機能はこの手法で拡張
class Hoge
{
public $int = 1;
}
class HogeFugaPiyo
{
protected $hoge;
public function __construct(Hoge $hoge)
{
$this->hoge = $hoge;
}
}
Laravelのプロジェクト内のクラスであれば
どのクラスのコンストラクタに記述しても自動的
にインスタンスが!
型宣言をすると、
リフレクションを使って自動的に解決
インスタンスの生成方法を指定
// Hogeと書いてあったらHogeをください
$this->app->bind(Hoge::class, function () {
return new Hoge;
});
$this->app->bind(Hoge::class);
// singletonでください
$this->app->singleton(Hoge::class);
インスタンスの生成方法を指定
引数が必要な場合
class Fuga
{
private $arg;
public function __construct(int $arg)
{
$this->arg = $arg;
}
}
型宣言ではない場合
解決できないのでしょうか?
$this->app->bind(Fuga::class, function () {
return new Fuga(1);
});
無名関数を利用することで、
クラスが解決されるまで、
Fugaは生成されません
インスタンスの生成方法を指定
混合
class Piyo
{
private $hoge;
private $fuga;
public function __construct(Hoge $hoge, Fuga $fuga)
{
$this->hoge = $hoge;
$this->fuga = $fuga;
}
}
複数のクラスがある場合
$this->app->bind(Hoge::class, function () {
return new Hoge;
});
$this->app->bind(Fuga::class, function () {
return new Fuga(1);
});
// 場合によってはこんなのも
$this->app->bind(
Fuga::class,
function (Application $app) {
return new Fuga($app->make(‘nanika’));
});
bindメソッドの場合は、
フレームワーク本体が引数
クラス、またはサービス名を指定
インスタンス生成時に
特定の何かをしたい
class HogeFugaPiyo
{
private $name = ‘’;
public function __construct(Hoge $hoge)
{
$this->hoge = $hoge;
}
public function setName(string $name)
{
$this->name = $name;
}
}
$this->app->extend(
HogeFugaPiyo::class,
function (HogeFugaPiyo $hoge) {
$hoge->setName(“hoge”);
return $hoge;
});
* 他にもあります
インスタンス生成時に
特定の処理を実行させられます
特定のクラスを継承したクラスの
インスタンスが生成された時に
なにかしたい
abstract class AbstractHoge
{
private $name = ‘abstract’;
public function setName(string $name)
{
$this->name = $name;
}
}
class Hoge extends AbstractHoge
{
}
$this->app->resolving(
AbstractHoge::class,
function (AbstractHoge $abstractHoge) {
$abstractHoge->setName(‘aaaaa’);
return $abstractHoge;
});
FormRequestはこの手法でValidationを
実行します
疎結合にしたい
// constructorにinterfaceを書きましょう
$this->app->bind(HogeInterface::class, Hoge::class);
何かの条件で返却される
インスタンスを変更したい
use Illuminate¥Support¥Manager;
class StringManager extends Manager
{
protected $driver = ‘hoge’;
public function getDefaultDriver()
{
return $this->driver;
}
protected function createHogeDriver()
{
return ‘hoge dayo’;
}
protected function createFugaDriver()
{
return ‘fuga dayo’;
}
}
// constructorでManagerクラス 型宣言
$this->app->bind(
StringManager::class,
function(Application $app) {
return new StringManager($app);
});
ちょっとだけ上級者向けですが、
オブジェクト指向のデザインパターンにも
ある解決手法です
protected $manager;
public function __construct(StringManager $manager)
{
$this->manager = $manager;
}
public function __invoke($id)
{
$manager = $this->manager->driver(‘hoge’);
if($id === 1) {
$manager = $this->manager->driver(‘fuga’);
}
dd($manager);
}
Facade, helper
class HogeFugaPiyo
{
public function invoke()
{
¥App::make(‘aaa’);
app(‘bbb’);
}
}
依存解決はできますが、
このクラスそのものが何に依存しているかわか
りますか?
class HogeFugaPiyo
{
public function __construct(Application $app)
{
$this->app = $app;
}
}
ファサードやヘルパー関数は、
実態に置き換えるとこうなります
堅実なアプリケーションへの道
要件を正しく理解する
• このアプリケーションはだれのためのものか
• 価値はなにか
• 目的はなにか
サービス要件・システム要件
• ユーザーは自分のTODOを加えることができる
• 自分のTODO一覧を見ることができる
• TODO一覧は自分以外は見ることができない
• TODOリストはデータベースで管理する
• キャッシュストレージにmemcahcedを使う
サービス要件に合わせたクラスを考える
• 登場する主要なものを型として定義
-> 配列は可変のため保証されない
• チームで開発する場合は、便利さよりも
仕様の理解と、コードがサービス理解を促進すること
• 一つでなんでもできるメソッドは、複雑
データベースの戻り値を堅実に
use App¥Item;
final class IndexController extends Controller
{
public function __invoke()
{
foreach(Item::all() as $item) {
$item->item_id;
}
}
}
データベースのカラムとして
返ってきますが、
アクセス可能なプロパティは、
テーブル構造を知っている必要があります
データベースの戻り値を堅実に
trait QueryPrepared
{
/** @var string */
protected $fetchStyleClass = '';
public function fetchClass(string $class): self
{
$this->fetchStyleClass = $class;
return $this;
}
protected function prepared(PDOStatement $statement): PDOStatement
{
$statement->setFetchMode($this->fetchMode);
if (!empty($this->fetchStyleClass)) {
$statement->setFetchMode(¥PDO::FETCH_CLASS, $this->fetchStyleClass);
}
return $statement;
}
}
PDOの機能を使いこなす!
データベースの戻り値を堅実に
final class SqliteConnection extends ¥Illuminate¥Database¥SQLiteConnection
{
use QueryPrepared;
}
拡張してメソッドを上書き
データベースの戻り値を堅実に
public function register()
{
/** @var DatabaseManager $dbManager */
$dbManager = $this->app['db'];
$dbManager->extend('sqlite', function (array $config, $name) {
$connectior = new SQLiteConnector;
return new SqliteConnection($connectior->connect($config));
});
}
DatabaseManagerはdbという
サービス名で登録されている
driver名を上書きする
sqlite使用時に拡張したクラスを
利用するように変更する
単純な拡張なので影響は全くありません
データベースの戻り値を堅実に
class ItemMapper
{
private $item_id;
private $item_name;
public function getId(): string
{
return $this->item_id;
}
public function getName(): string
{
return $this->item_name;
}
}
カラム名のプロパティは
PDOが自動的にマッピング
このクラスからどうやって値に
アクセスできるかが一目瞭然
データベースの戻り値を堅実に
public function __invoke(DatabaseManager $db)
{
$connection = $db->connection();
$connection->fetchClass(ItemMapper::class);
dd($db->connection()->table('items')->get());
}
データベースの戻り値を堅実に
Collection {#180 ▼
#items: array:2 [▼
0 => ItemMapper {#182 ▼
-item_id: 1
-item_name: "item1"
-created_at: "2017-10-03 20:47:04"
}
1 => ItemMapper {#183 ▼
-item_id: 2
-item_name: "item2"
-created_at: "2017-10-03 20:47:14"
}
]
}
任意のオブジェクトとして返却
PDOの機能なので簡単!
リポジトリを抽象レイヤとして扱い堅実に
use App¥Item;
final class IndexController extends Controller
{
public function __invoke(
HogeRepository $repository
) {
$repository->all();
}
}
データベースへのアクセスを抽象化したもの
ただし返却は前述の通りカラム
リポジトリを抽象レイヤとして扱い堅実に
class HogeRepository
{
protected $item;
public function __construct(Item $item)
{
$this->item = $item;
}
public function all()
{
$result = [];
foreach($this->item->all() as $row) {
$result[] = new ItemMapper($row->item_id);
}
return $result;
}
}
Collection生成のように扱う
yieldしても良いでしょう
ビジネスロジックはクラスで表現する
class PaymentService
{
private $repository;
public function __construct(Repository $repository)
{
$this->repository = $repository;
}
public function pay(int $money)
{
$this->repository->add($money);
}
public function quantity(int $itemID)
{
// aaa
}
}
購入系ビジネスロジック?
依存が増えて複雑になりそう!
ここに処理がまとまってるから書こう!
あっという間に巨大クラス
ビジネスロジックはクラスで表現する
class MakePurchase
{
private $repository;
public function __construct(Repository $repository)
{
$this->repository = $repository;
}
public function run(int $money)
{
$this->repository->add($money);
}
}
役割をクラス名で表現
機能は一つのみ
ビジネスロジックはクラスで表現する
• バグがあっても小さい修正
• 機能拡張は比較的容易
• テストのしやすさ
• フレームワークの機能が最小限
依存関係は正しい?
class SampleController extends Controller
{
public function apply($id)
{
$validator = ¥Validator::make(¥Input::all(), ['content' => 'required']);
if ($validator->fails()) {
return redirect()->route('hoge')->withInput()->withErrors($validator);
}
try {
$result = User::find($id);
$requests = ¥Input::all();
$entry = Entry::create($requests);
¥Log::info('added entry', $requests);
return view('form')->with('user', $entry);
} catch (¥Exception $e) {
$session = ¥Session::get('from_values');
...
}
}
}
依存関係は正しい?
/**
* @param User $userModel
* @param Entry $entryModel
* @param FactoryContract $validator
* @param Request $request
* @param SessionManager $session
* @param LoggerInterface $log
* @param Redirector $redirect
*/
public function __construct(
User $userModel,
Entry $entryModel,
FactoryContract $validator,
Request $request,
SessionManager $session,
LoggerInterface $log,
Redirector $redirect
) {
$this->userModel = $userModel;
$this->entryModel = $entryModel;
$this->validator = $validator;
$this->request = $request;
$this->session = $session;
$this->log = $log;
$this->redirect = $redirect;
}
依存関係を正しく書くとこうなります
依存関係は正しい?
• Facade・ヘルパー関数は便利
• 便利だからといってあちこちで使うと・・・
複雑度が増します
• 便利なものは便利に使う
• なんでもできるクラスを作らない
ビジネスロジックか、システム都合か
class MailRegistration
{
public function run(Auth $auth)
{
app('log')->info('start!');
//---
try {
$auth->getAddress();
// なにか処理
} catch(¥RuntimeException $e) {
app('log')->info('error!');
}
app('log')->info('end!');
}
}
メール登録に認証クラスは必要か?
ビジネスロジックにログを取ってください
という要件はあるか?
フレームワーク依存
細分化
class MailRegistration
{
public function run(User $user)
{
$user->getAddress();
// なんたらかんたら
throw new RuntimeException('メール登録に失敗しました');
}
}
純粋なビジネスロジックのクラスに
フレームワークと結合する
class AppMailRegistration
{
protected $mailRegistration;
protected $logger;
public function __construct(
MailRegistration $mailRegistration,
LoggerInterface $logger
) {
$this->mailRegistration = $mailRegistration;
$this->logger = $logger;
}
public function run(User $user)
{
$this->logger->info('start');
$this->mailRegistration->run($user);
$this->logger->info('end!');
}
}
ビジネスロジックと結合
ビジネスロジックか、システム都合か
• フレームワークの機能はビジネスロジックではない!
• 細分化することでバージョンアップ、
または他のフレームワークへの乗り換えも簡単
• 不具合修正の範囲をとにかく小さく
• ユニットテスト!
best practice
バリデーションはどこで?
• 小さなアプリケーションならコントローラや、
データベースに入れる直前でもいいでしょう
• 少し大きなアプリケーションではFormRequestで
コントローラに到達する前に
• ミドルウェアでも良いですが、
何かを挟み込む処理で利用するものと思った方が吉
Queueってどう運用すれば良いの?
• supervisorでdaemonにしましょう
• console開きっぱなしはやめましょう
• 自作プロセス監視は自信があれば
本番稼働時の注意
• debugbarはrequire-devでインストールすること
内部の処理をダンプするため非常に遅い
• cookieの名前はデフォルトから必ず変えましょう!
• 本当にEloquentで大丈夫?
SQL知らなくて大丈夫?
でもLaravel遅いんでしょう?
• 普通のアプリケーションであれば1秒以内に応答
• 多くのボトルネックはDB設計
• 実は意外と気付かないフロントエンドのボトルネック
• バッチ処理?
バッチ処理が得意な言語で実装するなど、
Laravelだけでなんでもやろうとしないことも重要
Now Hiring!
@cosme及び関連会社における
各種サイトのシステムの開発・保守・運用と、
新サービスなど全社横断的なプロジェクトを支える
システム基盤の開発・保守・運用をお任せいたします。
@cosmeを知らなくても大丈夫!

Best practice laravel