胖胖Model

減重的五個方法

by 尤川豪

阿川先生

Wordcorp一元翻譯 - 工程師

這樣寫一定會過胖

/views
/controllers
/models
    /Article.php
    /User.php
    /Order.php
    /Product.php
    /Category.php
    /Coupon.php
  • 1000/6 = 166.667
  • 2000/6 = 333.333
  • 3000/6 = 500
  • ...
  • 6000/6 = 1000

先談談兩個名詞☺

  • model

  • entity

Model

光在後端就非常含糊,再把前端算進來就根本無法討論 => MVC是一個巨大誤會

Entity

精準的名詞。訂單、商品、文章...etc
=> 常被誤解為MVC中「Model」的全部

 

胖胖Model減重的五個方法

  • Presenter

  • Repository

  • Form

  • Service

  • Package

#1 Presenter

把日期、金額、名稱之類的呈現(presentation)邏輯抽離出來!

這樣很胖

class Article extends Eloquent
{
    public function getDate(){/*...*/}

    // 呈現給台灣地區的時間格式
    public function getTaiwaneseDateTime(){/*...*/}

    // 呈現給歐美地區的時間格式
    public function getWesternDateTime(){/*...*/}

    public function getTaiwaneseDate(){/*...*/}

    public function getWesternDate(){/*...*/}
}

1st 試試Decorator Pattern

class ArticlePresenter
{
    protected $article;

    public function __construct(Article $article)
    {
        $this->article = $article;
    }

    // 呈現給台灣地區的時間格式
    public function getTaiwaneseDateTime(){
        return date('Y-m-d', $this->article->created_at);
    }

    // 呈現給歐美地區的時間格式
    public function getWesternDateTime(){/*...*/}

    public function getTaiwaneseDate(){/*...*/}

    public function getWesternDate(){/*...*/}
}

要在view初始化物件嗎...

@foreach($articles as $article)
    <?php $presenter = new ArticlePresenter($article); ?>
    發文日期:{{ $presenter->getTaiwaneseDate() }}
@endforeach

2nd 讓Entity變聰明

class Article extends Eloquent
{
    public function present()
    {
        return new ArticlePresenter($this);
    }
}

漂亮多了

@foreach($articles as $article)
    發文日期:{{ $article->present()->getTaiwaneseDate() }}
@endforeach

3rd 進一步改善

class Article extends Eloquent
{
	protected $presenterInstance;

	public function present()
	{
		if ( ! $this->presenterInstance)
		{
			$this->presenterInstance = new ArticlePresenter($this);
		}

		return $this->presenterInstance;
	}

} 

4th 用trait

trait PresentableTrait {

	protected $presenterInstance;

	public function present()
	{
                // ...

		if ( ! $this->presenterInstance)
		{
			$this->presenterInstance = new $this->presenter($this);
		}

		return $this->presenterInstance;
	}

} 

Laracasts/Presenter

use Laracasts\Presenter\PresentableTrait;

class Article extends \Eloquent {

    use PresentableTrait;

    protected $presenter = 'ArticlePresenter';

}
use Laracasts\Presenter\Presenter;

class ArticlePresenter extends Presenter {

    // 呈現給台灣地區的時間格式
    public function getTaiwaneseDateTime(){/*...*/}

    // 呈現給歐美地區的時間格式
    public function getWesternDateTime(){/*...*/}

    public function getTaiwaneseDate(){/*...*/}

    public function getWesternDate(){/*...*/}
}

用起來一樣漂亮

@foreach($articles as $article)
    發文日期:{{ $article->present()->getTaiwaneseDate() }}
@endforeach

不要重新發明輪子

#2 Repository

把查詢(query)的邏輯,也就是取得entity的各種方式抽離出來!

難讀的controller

$users = User::where('votes', '>', 100)
                        ->whereGender('W')
                        ->orderBy('created_at')
                        ->get();

還容易duplicate

Query Scope幫上忙了嗎?

$users = User::popular()->women()->orderBy('created_at')->get();
$users = User::where('votes', '>', 100)->whereGender('W')->orderBy('created_at')->get();

用了Query Scope

沒用Query Scope

這樣很胖

class User extends Eloquent {

    public function scopePopular($query)
    {
        return $query->where('votes', '>', 100);
    }

    public function scopeWomen($query)
    {
        return $query->whereGender('W');
    }

}

Laravel官方提供的Query Scopes...

寫個類別封裝吧

class UserRepository
{
    public function getPopularWomen()
    {
        return User::where('votes', '>', 100)->whereGender('W')->orderBy('created_at')->get();
    }
}

controller簡潔多了

$repository = new UserRepository();

$users = $repository->getPopularWomen();

搭配Automatic Resolution真的是讚

class UserController extends BaseController
{
    protected $users;
    
    public function __construct(UserRepository $repository)
    {
        parent::__construct();
        
        $this->users = $repository;
    }
    
    public function getIndex()
    {
        $women = $this->users->getPopularWomen();
    }
}

BJ4

$users = User::where('votes', '>', 100)->whereGender('W')->orderBy('created_at')->get();

$users = User::popular()->women()->orderBy('created_at')->get();

$users = $this->users->getPopularWomen();

據說可以寫個通用的abstract class...

<?php
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Collection;

abstract class EloquentRepository
{
    protected $model;
    
    public function __construct($model = null)
    {
        $this->model = $model;
    }
    
    public function getById($id)
    {
        return $this->model->find($id);
    }
    
    public function getAll()
    {
        return $this->model->all();
    }

    public function save($data)
    {
        if ($data instanceOf Model) {
            return $this->storeEloquentModel($data);
        }
    }
    
    public function saveMany($collection)
    {
        foreach($collection as $model)
        {
            $this->storeEloquentModel($model);
        }
    }

    public function delete($model)
    {
        return $model->delete();
    }

    protected function storeEloquentModel($model)
    {        
        if ($model->getDirty()) {
            return $model->save();
        } else {
            return $model->touch();
        }
    }

}

有要測試controller再用...

都用Active Record了,
testing避開資料庫可不容易...

#3 Form

把參數驗證(validation)的邏輯(例如字串長度、日期、金額大小)抽離出來!

難讀的controller

$validation = Validator::make(
    array(
        'name' => Input::get( 'name' ),
        'email' => Input::get( 'email' ),
    ),
    array(
        'name' => array( 'required', 'alpha_dash' ),
        'email' => array( 'required', 'email' ),
    )
);
 
if ( $validation->fails() ) {
    $errors = $validation->messages();
}

// ...

還容易duplicate

放進entity如何?

class Ball extends Eloquent
{
    private $rules = array(
        'color' => 'required|alpha|min:3',
        'size'  => 'required',
        // .. more rules here ..
    );

    public function validate($data)
    {
        // make a new validator object
        $v = Validator::make($data, $this->rules);
        // return the result
        return $v->passes();
    }
}
$b = new Ball();

if ($b->validate(Input::all())){
    // success code
}else{
    // failure code
}

到底放哪好?

controller還是entity?

1st 寫個類別封裝吧

class ArticleForm
{
    protected $validationRules = [
        'title' => 'required',
        'content' => 'required',
    ];
    protected $inputData;
    protected $validator;

    public function __construct($input)
    {
        $this->inputData = $input;
    }

    public function isValid()
    {
        $this->validator = Validator::make($this->input, $this->validationRules);
        return $this->validator->passes();
    }
    
    public function getErrors()
    {
        return $this->validator->errors();
    }

}

controller變得苗條又優雅

$form = new ArticleForm( Input::all() );
 
if ( ! $form->isValid() ){
    return Redirect::back()->with( [ 'errors' => $form->getErrors() ] );    
}
 
$article = new Article( Input::only('title', 'content', 'status') );
 
$article->save();

看得出哪邊容易duplicate嗎?

class ArticleForm
{
    protected $validationRules = [
        'title' => 'required',
        'content' => 'required',
    ];
    protected $inputData;
    protected $validator;

    public function __construct($input)
    {
        $this->inputData = $input;
    }

    public function isValid()
    {
        $this->validator = Validator::make($this->input, $this->validationRules);
        return $this->validator->passes();
    }
    
    public function getErrors()
    {
        return $this->validator->errors();
    }

}

2nd 來繼承嘍

class FormModel
{
    protected $validationRules;
    protected $inputData;
    protected $validator;

    public function __construct($input)
    {
        $this->inputData = $input;
    }

    public function isValid()
    {
        $this->validator = Validator::make($this->input, $this->validationRules);
        return $this->validator->passes();
    }
    
    public function getErrors()
    {
        return $this->validator->errors();
    }

}

v( ̄︶ ̄)y

class ArticleForm extends FormModel
{
    protected $validationRules = [
        'title' => 'required',
        'content' => 'required',
    ];
}

面對多種表單也OK

    protected $happyArticleValidationRules;
    protected $angryArticleValidationRules;
    protected $funnyArticleValidationRules;

    public function isValidHappy()
    {
        $this->validator = Validator::make($this->input, $this->happyArticleValidationRules);
        return $this->validator->passes();
    }
    
    public function isValidAngry()
    {
        $this->validator = Validator::make($this->input, $this->angryArticleValidationRules);
        return $this->validator->passes();
    } 
   
    public function isValidFunny()
    {
        $this->validator = Validator::make($this->input, $this->funnyArticleValidationRules);
        return $this->validator->passes();
    }

實作參考

#4 Service

把施加在多種entity上

或是

複雜的商業行為

抽離出來!

來創業,做一個創新線上訂閱音樂服務!

月租費就收400元好了

客戶下訂單(付月租費)

  1. 信用卡扣款

  2. 建立訂單(存進資料庫)

  3. 發送確認email

寫在controller

class CheckoutController extends Controller
{
    public function postSubmitOrder()
    {
        // 1. 利用Stripe從信用卡扣款        
        \Stripe\Stripe::setApiKey("sk_test_BQokikJOvBiI2HlWgH4olfQ2");

        \Stripe\Charge::create(array(
            "amount" => 400,
            "currency" => "usd",
            "source" => array(
                // 這是一個簡化的範例。請別把信用卡資料存在資料庫。
                "number" => $user->creditCard->number,
                "exp_month" => $user->creditCard->expMonth,
                "exp_year" => $user->creditCard->expYear,
                "cvc" => $user->creditCard->cvc
            ),
        ));    
        
        // 2. 建立訂單
        $order = new Order();
        
        $order->price = 400;

        $order->user_id = Auth::user()->id;
        
        $order->save();
        
        // 3. 發送確認Email
        Mail::send('emails.confirmation', [], function($message) use ($user)
        {
            $message->to( $user->email )
                              ->subject( '謝謝您的訂購。' );
        });
                
    }
}

永遠透過這個controller結帳?

那這樣寫OK

執行長:來改變商業模式吧!

  • 推廣成本太高,鼓勵藝人幫我們推廣好了!

    分一半利潤給他們!
     

  • 手機App也寫好了,要支援付款功能~
     

  • 在瀏覽專輯頁面支援Ajax付款好了~

讓客戶在結帳頁之外也能付款...

  • 用iframe做plugin放在藝人官網接受付款
  • 手機App發送request結帳
  • 在瀏覽專輯頁面一鍵AJAX付款
  • ...執行長的其他神奇點子...etc
// 1. 利用Stripe從信用卡扣款        
\Stripe\Stripe::setApiKey("sk_test_BQokikJOvBiI2HlWgH4olfQ2");

\Stripe\Charge::create(array(
    "amount" => 400,
    "currency" => "usd",
    "source" => array(
        // 這是一個簡化的範例。請別把信用卡資料存在資料庫。
        "number" => $user->creditCard->number,
        "exp_month" => $user->creditCard->expMonth,
        "exp_year" => $user->creditCard->expYear,
        "cvc" => $user->creditCard->cvc
    ),
));    

// 2. 建立訂單
$order = new Order();

$order->price = 400;

$order->user_id = Auth::user()->id;

$order->save();

// 3. 發送確認Email
Mail::send('emails.confirmation', [], function($message) use ($user)
{
    $message->to( $user->email )
                      ->subject( '謝謝您的訂購。' );
});
// 1. 利用Stripe從信用卡扣款        
\Stripe\Stripe::setApiKey("sk_test_BQokikJOvBiI2HlWgH4olfQ2");

\Stripe\Charge::create(array(
    "amount" => 400,
    "currency" => "usd",
    "source" => array(
        // 這是一個簡化的範例。請別把信用卡資料存在資料庫。
        "number" => $user->creditCard->number,
        "exp_month" => $user->creditCard->expMonth,
        "exp_year" => $user->creditCard->expYear,
        "cvc" => $user->creditCard->cvc
    ),
));    

// 2. 建立訂單
$order = new Order();

$order->price = 400;

$order->user_id = Auth::user()->id;

$order->save();

// 3. 發送確認Email
Mail::send('emails.confirmation', [], function($message) use ($user)
{
    $message->to( $user->email )
                      ->subject( '謝謝您的訂購。' );
});
// 1. 利用Stripe從信用卡扣款        
\Stripe\Stripe::setApiKey("sk_test_BQokikJOvBiI2HlWgH4olfQ2");

\Stripe\Charge::create(array(
    "amount" => 400,
    "currency" => "usd",
    "source" => array(
        // 這是一個簡化的範例。請別把信用卡資料存在資料庫。
        "number" => $user->creditCard->number,
        "exp_month" => $user->creditCard->expMonth,
        "exp_year" => $user->creditCard->expYear,
        "cvc" => $user->creditCard->cvc
    ),
));    

// 2. 建立訂單
$order = new Order();

$order->price = 400;

$order->user_id = Auth::user()->id;

$order->save();

// 3. 發送確認Email
Mail::send('emails.confirmation', [], function($message) use ($user)
{
    $message->to( $user->email )
                      ->subject( '謝謝您的訂購。' );
});
// 1. 利用Stripe從信用卡扣款        
\Stripe\Stripe::setApiKey("sk_test_BQokikJOvBiI2HlWgH4olfQ2");

\Stripe\Charge::create(array(
    "amount" => 400,
    "currency" => "usd",
    "source" => array(
        // 這是一個簡化的範例。請別把信用卡資料存在資料庫。
        "number" => $user->creditCard->number,
        "exp_month" => $user->creditCard->expMonth,
        "exp_year" => $user->creditCard->expYear,
        "cvc" => $user->creditCard->cvc
    ),
));    

// 2. 建立訂單
$order = new Order();

$order->price = 400;

$order->user_id = Auth::user()->id;

$order->save();

// 3. 發送確認Email
Mail::send('emails.confirmation', [], function($message) use ($user)
{
    $message->to( $user->email )
                      ->subject( '謝謝您的訂購。' );
});

Duplicate Code

Duplicate code只是問題之一

  • controller可讀性早已很差
    - 可以視為business logic洩漏
  • 整段business logic無法獨立測試

放進Entity好了!

把這段執行順序視為
business logic的一種

工程師A:這屬於User的行為!

class User extends Eloquent
{
    public function checkout()
    {
        // 1. 利用Stripe從信用卡扣款        
        \Stripe\Stripe::setApiKey("sk_test_BQokikJOvBiI2HlWgH4olfQ2");
        
        \Stripe\Charge::create(array(
            "amount" => 400,
            "currency" => "usd",
            "source" => array(
                // 這是一個簡化的範例。請別把信用卡資料存在資料庫。
                "number" => $this->creditCard->number,
                "exp_month" => $this->creditCard->expMonth,
                "exp_year" => $this->creditCard->expYear,
                "cvc" => $this->creditCard->cvc
            ),
        ));    
        
        // 2. 建立訂單
        $order = new Order();
        
        $order->price = 400;
        
        $order->user_id = $this->id;
        
        $order->save();
        
        // 3. 發送確認Email
        Mail::send('emails.confirmation', [], function($message) use ($user)
        {
            $message->to( $user->email )
                              ->subject( '謝謝您的訂購。' );
        });

    }
}
// 原本的Checkout controller

$user->checkout();
// 瀏覽專輯頁面一鍵AJAX付款的對應controller

$user->checkout();
// 用iframe做plugin讓藝人能放進官網的controller

$user->checkout();
// 處理手機App結帳的controller

$user->checkout();

工程師A:是否變得優雅許多!

工程師B:這屬於Order的行為!

class Order extends Eloquent
{
    public function checkout()
    {
        // 1. 利用Stripe從信用卡扣款        
        \Stripe\Stripe::setApiKey("sk_test_BQokikJOvBiI2HlWgH4olfQ2");

        $user = User::find($this->user_id);
        
        \Stripe\Charge::create(array(
            "amount" => 400,
            "currency" => "usd",
            "source" => array(
                // 這是一個簡化的範例。請別把信用卡資料存在資料庫。
                "number" => $user->creditCard->number,
                "exp_month" => $user->creditCard->expMonth,
                "exp_year" => $user->creditCard->expYear,
                "cvc" => $user->creditCard->cvc
            ),
        ));    
        
        // 2. 發送確認Email
        Mail::send('emails.confirmation', [], function($message) use ($user)
        {
            $message->to( $user->email )
                              ->subject( '謝謝您的訂購。' );
        });
    }

}
// 原本的Checkout controller

$order = new Order(
    Auth::user()->id, 
    400
);

$order->checkout();
// 瀏覽專輯頁面一鍵AJAX付款的對應controller

$order = new Order(
    Auth::user()->id, 
    400
);

$order->checkout();
// 用iframe做plugin讓藝人能放進官網的controller

$order = new Order(
    Auth::user()->id, 
    400
);

$order->checkout();
// 處理手機App結帳的controller

$order = new Order(
    Auth::user()->id, 
    400
);

$order->checkout();

工程師B:這才叫做優雅!

工程師A:活人才有行為!不該放在訂單!

工程師B:堅持放在訂單!
有更新orders資料表,沒更新users資料表!

工程師C:它不屬於誰,它就是它!

class BillService
{
    public function checkout(User $user)
    {
        // 1. 利用Stripe從信用卡扣款        
        \Stripe\Stripe::setApiKey("sk_test_BQokikJOvBiI2HlWgH4olfQ2");
        
        \Stripe\Charge::create(array(
            "amount" => 400,
            "currency" => "usd",
            "source" => array(
                // 這是一個簡化的範例。請別把信用卡資料存在資料庫。
                "number" => $user->creditCard->number,
                "exp_month" => $user->creditCard->expMonth,
                "exp_year" => $user->creditCard->expYear,
                "cvc" => $user->creditCard->cvc
            ),
        ));    
        
        // 2. 建立訂單
        $order = new Order();
        
        $order->price = 400;
        
        $order->user_id = $user->id;
        
        $order->save();
        
        // 3. 發送確認Email
        Mail::send('emails.confirmation', [], function($message) use ($user)
        {
            $message->to( $user->email )
                              ->subject( '謝謝您的訂購。' );
        });   
    }
}
// 原本的Checkout controller

$service = new BillService();

$service->checkout(
    Auth::user()
);
// 瀏覽專輯頁面一鍵AJAX付款的對應controller

$service = new BillService();

$service->checkout(
    Auth::user()
);
// 用iframe做plugin讓藝人能放進官網的controller

$service = new BillService();

$service->checkout(
    Auth::user()
);
// 處理手機App結帳的controller

$service = new BillService();

$service->checkout(
    Auth::user()
);

工程師C:兩位別吵了!都沒擁有它!

多個entity爭執不下、
行為本身又很複雜的時候,
獨立成service。

除了最簡單的封裝之外...

#5 Package

把其他公司也能使用、概念上獨立於當前專案的程式碼抽離出來!

The secret to building large apps is never build large apps.

Break your applications into small pieces.

Then, assemble those testable, bite-sized pieces into your big application.

  • 有可能分享給其他公司使用嗎?

新功能不要急著加進entity或是service裡面

常見情境

  • 用法瑣碎的第三方SDK
  • 想寫在controller內,但實際上可以包起來

範例一:Google Drive API

class Document extends Eloquent
{

    public function addPermission($value, $type, $role, $withLink = false) 
    {
        $googleClient = new Google_Client();
        
        $googleClient->setClientId(Config::get('google_drive.clientId'));
        
        $googleClient->setClientSecret(Config::get('google_drive.clientSecret'));
        $googleClient->setRedirectUri(Config::get('google_drive.redirectUri'));
        $googleClient->setAccessType('offline');
        $googleClient->setScopes(array('https://www.googleapis.com/auth/drive'));            
        $googleClient->setApprovalPrompt('force');
        
        $googleService = new Google_Service_Drive($this->googleClient);
        $googleClient->setAccessToken(/*...*/);        
        
        $newPermission = new Google_Service_Drive_Permission();
        $newPermission->setValue($value);
        $newPermission->setType($type);
        $newPermission->setRole($role);
        $newPermission->setWithLink($withLink);
        $googleService->permissions->insert($this->fileId, $newPermission);        
    }

}

獨立成service

<?php
class GoogleDriveService{
    
    protected $googleClient;
    protected $googleService;
    
    public function __construct(){
        $this->googleClient = new Google_Client();

        $this->googleClient->setClientId(Config::get('google_drive.clientId'));
        
        $this->googleClient->setClientSecret(Config::get('google_drive.clientSecret'));
        $this->googleClient->setRedirectUri(Config::get('google_drive.redirectUri'));
        $this->googleClient->setAccessType('offline');
        $this->googleClient->setScopes(array('https://www.googleapis.com/auth/drive'));            
        $this->googleClient->setApprovalPrompt('force');
        
        $this->googleService = new Google_Service_Drive($this->googleClient);
        $this->googleClient->setAccessToken(TokenServiceProider::getAccessToken());        
    }
    public function insertPermission( $fileId, $value, $type, $role, $withLink = false) {
        $newPermission = new Google_Service_Drive_Permission();
        $newPermission->setValue($value);
        $newPermission->setType($type);
        $newPermission->setRole($role);
        $newPermission->setWithLink($withLink);
        $this->googleService->permissions->insert($fileId, $newPermission);
    }
}
class Document extends Eloquent
{
    public function addPermission($value, $type, $role, $withLink = false) 
    {
        $service = new GoogleDriveService();

        $service->insertPermission($this->fileId, $value, $type, $role, $withLink = false) ;
    }

}

獨立成serivce?

<?php
class GoogleDriveService{
    
    protected $googleClient;
    protected $googleService;
    
    public function __construct(){
        $this->googleClient = new Google_Client();

        $this->googleClient->setClientId(Config::get('google_drive.clientId'));
        
        $this->googleClient->setClientSecret(Config::get('google_drive.clientSecret'));
        $this->googleClient->setRedirectUri(Config::get('google_drive.redirectUri'));
        $this->googleClient->setAccessType('offline');
        $this->googleClient->setScopes(array('https://www.googleapis.com/auth/drive'));            
        $this->googleClient->setApprovalPrompt('force');
        
        $this->googleService = new Google_Service_Drive($this->googleClient);
        $this->googleClient->setAccessToken(TokenServiceProider::getAccessToken());        
    }
    public function insertPermission( $fileId, $value, $type, $role, $withLink = false) {
        $newPermission = new Google_Service_Drive_Permission();
        $newPermission->setValue($value);
        $newPermission->setType($type);
        $newPermission->setRole($role);
        $newPermission->setWithLink($withLink);
        $this->googleService->permissions->insert($fileId, $newPermission);
    }
}

獨立成package

<?php namespace Howtomakeaturn\GoogleDriveKing;

use Google_Clinet;
use Google_Service_Drive;

class GoogleDriveKing{

    protected $googleClient;
    protected $googleService;
    
    public function __construct(){
        $this->googleClient = new Google_Client();

        $this->googleClient->setClientId(Config::get('google_drive.clientId'));
        
        $this->googleClient->setClientSecret(Config::get('google_drive.clientSecret'));
        $this->googleClient->setRedirectUri(Config::get('google_drive.redirectUri'));
        $this->googleClient->setAccessType('offline');
        $this->googleClient->setScopes(array('https://www.googleapis.com/auth/drive'));            
        $this->googleClient->setApprovalPrompt('force');
        
        $this->googleService = new Google_Service_Drive($this->googleClient);
        $this->googleClient->setAccessToken(TokenServiceProider::getAccessToken());        
    }
    // ...
}

範例二:把資料庫轉成csv備份

<?php

class BackupController extends Controller
{
    public function postDownload()
    {
        $results = DB::select('select * from users');

        // ...
    }
}

獨立成Package

<?php
namespace Howtomakeaturn\CSVDumper;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\File;

class CSVDumper{
    
    protected $tableName;
    protected $doctrineSchemaManager;
    protected $queryBuilder;
    
    public function __construct($tableName)
    {
        $this->tableName = $tableName;
        $this->doctrineSchemaManager = Schema::getConnection()->getDoctrineSchemaManager();
        $this->queryBuilder = DB::table($tableName);
    }

    // ...
}
<?php

class BackupController extends Controller
{
    public function postDownload()
    {
        // ...
        foreach ($tableNames as $name) {
            $dumper = new Howtomakeaturn\CSVDumper\CSVDumper($name);

            $result = $dumper->dumpAndStoreTable(storage_path() . '/database-backup');
        }
        // ...
    }
}

所以,怎麼改善這個?

/views
/controllers
/models
    /Article.php
    /User.php
    /Order.php
    /Product.php
    /Category.php
    /Coupon.php

方法一:照Domain分

/views
/controllers
/MyApp
    /Article
        /Article.php
        /Presenter.php
        /Repository.php
        /Form.php
    /User
        /User.php
        /Presenter.php
        /Repository.php
        /Form.php
    /Order
    // ...
    /Service
        /FirstService.php
        /SecondService.php
/GithubName
    /CoolPackageOne
    /CoolPackageTwo
    /CoolPackageThree

方法二:照功能分

/views
/controllers
/MyApp
    /Entities
        /Article.php
        /User.php
    /Presenters
        /Article.php
        /User.php
    /Repositories
        /Article.php
        /User.php
    /Forms
        /Article.php
        /User.php
    // ...
    /Service
        /FirstService.php
        /SecondService.php
/GithubName
    /CoolPackageOne
    /CoolPackageTwo
    /CoolPackageThree

方法三:

請您自由發揮創意:)

Factory, Search, Utility, Observer, Listener...etc

延伸閱讀...

Active Record?

最後
一個祕密...
一個建議...

胖胖Model減重的五個方法

By howtomakeaturn

胖胖Model減重的五個方法

隨著軟體專案不斷增加新功能,你是否隱約感到不安?覺得Model越來越胖、開發速度越來越慢?

  • 15,353