DDD實戰:建立訂單的多種方式

需求

設計一套專案管理軟體,有三種身份的使用者能建立專案。分別是「管理員」、「已註冊顧客」、「未註冊顧客」。

原解法

在Project類別底下,實作三個靜態函式:
* createForAdmin
* createForClient
* createForGuest
分別代表三種建立專案的方法。

在建立時只要呼叫:

$project = Project::createForClient();

即可。

問題

Project類別的函式代表了「一個專案擁有的方法、屬性」,現又加入了靜態函式去代表「關於專案的一切函式」。
在Project類別還小時,這是一個快速又可接受的作法。
但當Project類別肥大起來,這增加了理解的困難(Project代表的含意太過廣泛)。

DDD觀點

負責建立domain objects的任務可以獨立成一個Factory類別。
在建立時只要呼叫:

$factory = new ProjectFactory();
$project = $factory->createForClient();

即可。

DDD實戰:一段訂單建立程序-Part 2

我在DDD實戰:一段訂單建立程序-Part 1進行了一次效果不佳的重構。

最後不盡理想的結果如下:

orderRepository = new OrderRepository();
    }

    private function getNewOrderOrCreate()
    {
        if (Auth::check()){

            $order = $this->orderRepository->getNewOrderByUser(Auth::user());
            if ( !$order ){
                $order = Order::createWithUser(Auth::user());
            }
                        
        } else {
            
            $order = $this->orderRepository->getNewOrderBySesssionId(Session::getId());            
            if ( !$order ){
                $order = Order::createWithSession(Session::getId());
            }

        }
        
        return $order;
    }

    public function getOrder()
    {              
        $order = $this->orderRepository->getNewOrderOrCreate();

        return View::make('client/order', ['order' => $order]);
    }

}

我對controller中的getNewOrderOrCreate函式非常不滿意。
controller中的其他部份,都只是初始化某些類別、把某些參數傳來傳去,謹此而已,唯獨這個函式內含複雜邏輯。真醜。

但是那些判斷logic確實是controller該負責的事情,也不能丟進model,怎麼辦呢?

DDD觀點

DDD中區分了Application Layer、Domain Layer。乍看之下getNewOrderOrCreate不能進Domain Layer,只能硬放在Application Layer變成醜controller。

我在DDD實戰:一段金流程序提到過Service的概念。

我研究了一下發現…

其實Service又可分為Application Service、Domain Service以及Infrastructure Service。

要判斷一個Service隸屬於哪種,常常非常困難(譬如說,您覺得DDD實戰:一段金流程序中的ReceivePaymentNotification算是Application service還是Domain service?)

不過眼前的getNewOrderOrCreate卻非常清楚,應該是屬於Application Layer!

(但這都沒關係。目前我沒打算針對三種Service在程式中加上namespace,也沒打算在檔案結構上分別開資料夾之類的。現階段我就隨便開個Service資料夾,然後管他哪種Service通通丟進去就好了。)

那麼,就讓我來封裝一個Service,接著在Controller使用它吧:

getOrCreate();

        return View::make('client/order', ['order' => $order]);
    }

}
orderRepository = new OrderRepository();
    }
    
    public function getOrCreate()
    {
        if (Auth::check()){

            $order = $this->orderRepository->getNewOrderByUser(Auth::user());
            if ( !$order ){
                $order = Order::createWithUser(Auth::user());
            }
                        
        } else {
            
            $order = $this->orderRepository->getNewOrderBySesssionId(Session::getId());            
            if ( !$order ){
                $order = Order::createWithSession(Session::getId());
            }

        }
        
        return $order;
    }
        
}

終於成功了!終於讓controller內的code看起來非常簡潔、expressive,卻又不影響到Domain Layer!
我還發現,getNewOrderOrCreate光看函式名稱就覺得概念模糊。這種模糊感本身就暗示了抽象化的不足、程式設計想得不夠清楚。
一模一樣的內容,封裝成類別、再重新命名,就成了FetchOrderForClient底下的getOrCreate,真是清楚多了。

DDD實戰:一段訂單建立程序-Part 1

需求

我公司為一電子商務公司,顧客可在網路上下訂單購買商品。
為提供出色顧客消費體驗,故允許顧客可不註冊不登入、直接建立訂單(付款前再要求登入即可)。

原解法

* 將已登入之顧客訂單紀錄顧客id;未登入之顧客訂單紀錄session id。

在MVC架構之下,我個人嚴禁在M內直接操作session,所以我將已登入之顧客訂單建立程序封裝於createWithUser函式,並傳入目前使用者;將未登入之顧客訂單建立程序封裝於createWithSession並傳入當前session id。
以上兩函式屬於Model範疇。
我接著將判斷使用者登入與否、以及判斷顧客是否有「未完成訂單」的code寫在controller,並封裝成getNewOrderOrCreate(拿出顧客之前未完成訂單、讓顧客繼續買東西。不然就建立一筆新訂單。)

inStatus(Order::CREATED_STATUS)->first();
            if ( !$order ){
                $order = Order::createWithUser(Auth::user());
            }        

        } else { // use is not logined

            $order = Order::where('session_id', Session::getId())->inStatus(Order::CREATED_STATUS)->first();
            if ( !$order ){
                $order = Order::createWithSession(Session::getId());
            }

        }
        return $order;        
    }

    public function getOrder()
    {              
        $order = $this->getNewOrderOrCreate();

        return View::make('client/order', ['order' => $order]);
    }

}

問題:
* getNewOrderOrCreate函式負責太多事情,code讀起來十分困難

已經將建立程序封裝在Model了,controller的code還是這麼雜,怎麼辦呢?

DDD觀點

應該定義一個專門負責以各種不同方式、參數取出domain objects的Repository類別。

我決定撰寫一個OrderRepository類別專職處理getNewOrderOrCreate負責的任務,並在controller中使用這個類別:

orderRepository = new OrderRepository();
    }

    public function getOrder()
    {              
        $order = $this->orderRepository->getNewOrderOrCreate();

        return View::make('client/order', ['order' => $order]);
    }

}
inStatus(Order::CREATED_STATUS)->first();
            if ( !$order ){
                $order = Order::createWithUser(Auth::user());
            }        

        } else { // use is not logined

            $order = Order::where('session_id', Session::getId())->inStatus(Order::CREATED_STATUS)->first();
            if ( !$order ){
                $order = Order::createWithSession(Session::getId());
            }

        }
        return $order;        
    }
    
}

controller內的code終於簡潔多了!
如果說DDD的repository對應到MVC,是M中最靠近C的一層的話…
那麼MVC常要求的’skinny controller, fat model’我終於做到了吧!

問題:
* 只是將code由C移出,getNewOrderOrCreate難以理解的問題依然存在
* 現在repository內操作session了…有點像在M內操作session的感覺,這不對吧?不是說了嚴禁嗎?

反省

getNewOrderOrCreate太過含糊不清,應該讓repository內的函式更expressive,光看函式名就得知會取出什麼樣的domain objects。否則,不但不夠reusable、也失去多寫一層repository的意義。

兩點想法:

* 關於session的操作,還是讓它留在controller內吧。

* controller對應到DDD觀點下,要負責application layer大部分的任務。
application layer本來就要負責知道當前提供的應用走到哪一階段(state)。
因此到底要取出未完成訂單、還是建立一筆新訂單,是controller要負責的事。

orderRepository = new OrderRepository();
    }

    private function getNewOrderOrCreate()
    {
        if (Auth::check()){

            $order = $this->orderRepository->getNewOrderByUser(Auth::user());
            if ( !$order ){
                $order = Order::createWithUser(Auth::user());
            }
                        
        } else {
            
            $order = $this->orderRepository->getNewOrderBySesssionId(Session::getId());            
            if ( !$order ){
                $order = Order::createWithSession(Session::getId());
            }

        }
        
        return $order;
    }

    public function getOrder()
    {              
        $order = $this->getNewOrderOrCreate();

        return View::make('client/order', ['order' => $order]);
    }

}
inStatus(Order::CREATED_STATUS)->first();
    }
    
    public function getNewOrderBySesssionId($sessionId)
    {
        return Order::where('session_id', $sessionId)->inStatus(Order::CREATED_STATUS)->first();        
    }
    
}

問題:
* controller又有點變回一開始那個鳥樣了
* 雖然repository讓controller內的code更可讀一點點了…,但反省半天到最後只有這麼一點benefit,而且repository內的函式在其他地方未必真的會用到,這reusability也許不必要
* 綜合以上兩點,我可不可以說這根本是over design、over engineering?

結論

如您所見,在我的範例之中,增加一層repository,當下得到的benefit並不大。有些人會認為跟最一開始的code根本沒差多少。

根據Martin Fowler的介紹,當query logic龐大起來時,多加上Repository這層專門負責處理這些query logic,不但可以減少duplicate code,還可以讓呼叫方呼叫時更清楚自己在呼叫什麼。

Repository在DDD中被認為是Domain Layer最重要的組成之一。
何時引進這層,就看當下需求了。

2015-01-04 更新

我越看那個controller越覺得很不爽。
研究了一下,發現DDD當然有指引這類問題的方向。
我缺少的是Application Service。
參見:DDD實戰:一段訂單建立程序-Part 2

DDD實戰:一段金流程序

最近開始研究DDD(Domain Driven Design),發現它確實是組織大型軟體架構出色又實際的方法。
可惜網路上只找得到一堆理論與說明,實際的範例code很少,我只好自己摸索了。
我會陸續將成果分享出來,希望對需要的人有所幫助。


需求

我公司串接由A公司提供之金流服務(第三方支付),A公司在確認收到顧客付款(信用卡、ATM轉帳)之後,會以HTTP POST通知我公司某設定好之URL。
由於可能有粗心大意之顧客,針對單筆訂單送出多筆交易、重複付款,因此需要在發現重複付款之時通知A公司放棄之前相關授權交易。

原解法

我公司有代表交易之trades資料表與代表訂單之orders資料表。
商業邏輯如下:
* 使用A公司SDK計算與驗證參數
* 從trades找到對應之交易並設為「已授權」
* 從orders找到對應之訂單並設為「已付款」

在MVC架構之下,我將其撰寫於某專責於金流交易之controller:

fetchParams();

        // 驗證失敗,參數並非A公司加密傳來
        if (!$params) {
            return '0|Fail';        
        }
         
        // 更新交易狀態
        $t = Trade::where('trade_no', $params['trade_no'])->first();
        $t->recieved_datetime = date('c');            
        $t->status = 1;
        $t->save();
        
        // 重複付款了,發送API通知A公司取消前幾筆交易
        if ($t->order->status == ORDER::PAID_STATUS){
            // blah blah
            // ...
            return '1|OK';            
        }
        
        // 更新訂單狀態
        $t->order->status = ORDER::PAID_STATUS;
        $t->order->save();
        
        return '1|OK';            
    }

}

問題:
* code可讀性不夠高
* 此controller action一次處理多件事情、分工不佳

反省

也許可將這串程序封裝成Trade類別底下的一個動作?


class SomeController extends BaseController {

    public function postPaymentReceive(){
        // A公司提供之SDK
        $sdk = new SDK();
        $params = $sdk->fetchParams();

        // 驗證失敗,參數並非A公司加密傳來
        if (!$params) {
            return '0|Fail';        
        }
        
        // 授權此筆交易
        // 內含交易狀態更新、訂單狀態更新、重複扣款通知
        $t = Trade::where('trade_no', $params['trade_no'])->first();
        $t->authenticate($params);
                
        return '1|OK';            
    }

}

問題:
* 只是將code由controller移進model,所有問題都依舊存在

在MVC架構之下,只追求’thin controller, fat model’會很容易將軟體架構只改善到這個階段。

DDD觀點

一個「交易」實體本身不會「授權」自己,應該由「某個程序」去進行「授權」一個「交易」實體的動作。
也就是說這行code不合理:

$t->authenticate();

它將原本Trade類別的意義由「一筆交易擁有的屬性、行為」擴大去包含了「會對一筆交易施加的行為」。
就算改寫為靜態函式依然擴大了原Trade類別的意義。
Trade類別的意義過廣會導致可讀性降低、不好理解、難維護。

DDD中關於一個service的判斷如下:
* 涉及到domain中的其他object
* stateless
* 涉及一個domain,通常不屬於一個entity或value object

我決定撰寫一個service類別專職處理這段接收通知的金流程序,並在controller中使用這個類別:


class SomeController extends BaseController {

    public function postPaymentReceive(){
        // A公司提供之SDK
        $sdk = new SDK();
        $params = $sdk->fetchParams();

        $service = new ReceivePaymentNotification();

        return $service->handle($params);        
    }

}
processParameters($params);

        return '1|OK';
    }
    
    public function processParameters($params)
    {        
        $t = Trade::where('trade_no', $params['trade_no'])->first();

        $this->processTransaction($t, $params);        

        $this->processOrder($t->order);        
    }

    public function processTransaction($t, $params)
    {
        $t->recieved_datetime = date('c');            
        $t->status = 1;
        $t->save();
    }
    
    public function processOrder($o)
    {

        // 重複付款了,發送API通知A公司取消前幾筆交易
        if ($o->status == ORDER::PAID_STATUS){
            // blah blah
            // ...
            return '1|OK';            
        }
        
        // 更新訂單狀態
        $o->status = ORDER::PAID_STATUS;
        $o->save();
    }
    
}

幾乎只是將原本的code剪下、貼上而已,卻花了我整個下午。

您覺得可讀性、可維護性有改善嗎?

期待您的寶貴意見,歡迎在下方給我feedback。

軟體架構:Domain Driven Design

從事軟體開發,特別是從Web領域開始的developers,一定因為各框架普遍提供MVC架構的關係而大致了解MVC的功能區分。

然而,當專案大了起來,很快就會發現如何讓MVC分工,比想像中困難:

* 某些code到底該放在C還是M?

* 修改V內的某些code會直接影響到business logic的運作與安全…這不應該吧?

很顯然地,對軟體架構的知識只擁有MVC是不夠的。

我舉一個更殘酷的例子。我在Reddit曾經和國外的工程師討論MVC的問題:

What exactly is ‘model’ in MVC?

最高分的回答如下,謝謝Nephand

I would say think of the ‘model’ more as your application – it’s not a single thing. The ‘view’ and ‘controller’ are just a channel of communication into your application’s behaviours.
Another word to replace ‘model’ might be ‘domain’ – see Domain Driven Design. Your application domain might need to deal with emails, or SMS but such things aren’t specific to your domain – generally speaking – so they’re services that your model is aware of (even if it’s just dispatching an event that your services listen for). They’re channels of communication into and out of your domain.
Your web interface talks to the application domain through a view and controller. Yet imagine you want to offer an API – this has no need of the View [simplistically put]. Also, your application might talk to the outside world through an SMS Service, or an Email Service and so on.
Personally, I like Hexagonal architecture as a way of conceptualising this.
Your ‘model’/’domain’/’core application’ then might have, for example, a persistence/database layer which creates a ‘seam’ between your application’s logic and the actual data storage format. This can be great, it translates out complex objects into a simple data format (SQL Rows…Mongo Documents…Redis)
You mention the idea of ‘one model’ and ‘one table’ – and you’re right to say this part is confusing. The ‘model == table’ approach has plenty of benefits, in that it’s pretty simple and easy to follow – but it can be limiting.
Having a ‘seam’ between your model and the data store becomes useful as it grows more complex. Within your ‘model’ you might have one ‘god object’ that serves as a root for multiple sub-objects. Each part can encapsulate behaviours that its containing object doesn’t need to know about – but all of that complexity still constitutes just one table row.
Domain Driven Design (as a primary voice but not the only one) differentiates Entity and Value Object [the classic Value Object being Money ].
The Entity might represent what you’re thinking of as a ‘row’ in your table (in the most general sense) – it has something that makes you want to be able to track its lifespan and differentiate it from others inside your application (e.g. a unique primary key ‘ID’).
Quick and dirty example: A User. A User would/could be an Entity, with a unique identifier. A User might have an email address. The concept of an ‘Email Address’ is not unique to a User, and so it might be a good candidate for being a Value Object that can be used by anything in your application domain/model. So when you later need to give another Entity (say a Company) an email address, you can re-use that same value object and all of the behaviours you might want for all EmailAddresses in your domain.
Lets say your business requires a hatred of hotmail users, and will never consider @hotmail.com a valid email address. You can then encapsulate such a rule into your EmailAddress object’s constructor [other approaches are preferred perhaps but I did say quick and dirty].
Whether you use EmailAddress in the context of a User or a Company, it will always throw an Exception / fail elegantly.
This is not [typically] intended to validate user input, but to ensure that your model/domain is meeting your hotmail hatred requirement by not allowing an EmailAddress value object for “*@hotmail.com” to be created. Whether that is passed in from a web form, over an API, or parsed in from an SMS interface shouldn’t matter to your core logic – you’re just saying ‘never allow this to happen even if it makes it past input validation somehow’.
This comes back to your concern about having a complex model separated into different models and in turn tables. That is where different strategies for mapping your ‘model’ to your data store come in. The Data Mapper is arguably the most flexible – and in turn potentially complicated – to implement. Patterns of Enterprise Application Architecture in combination with the Evans book Domain Driven Design can go a long way to help you understand these ideas, even if you water them down to ‘keep it simple [stupid]’.
Basically your User table (VALUES user_id, email_address) would be updated with the $user->id and $user->emailAddress->getAddress() values to store. A Company would look similar, $company->id, $company->emailAddress->getAddress().
The great part about all of this is that you can start to break up and refactor your god objects into smaller parts, that can be persisted separately or not. While your god object might still serve as a ‘root’ object, you will hopefully have been able to split its behaviours off into smaller chunks that can be tested easily, persisted easily, and even re-used where needed by other parts of your model.
This is all well and good, but don’t over-engineer things. If it’s just a string value, then just let it be a string value. Until you arrive at a behaviour requirement that makes it useful to implement an abstraction then it’s probably just bloat.
Obligatory warning This is a massive over-simplification of some more interesting concepts than I’d be able to explain – both due to lack of experience and time. Apologies to the knowledgeable if it’s a bad explanation – and to everyone else if it’s especially rambling, I don’t have time to proof read.
Quick Links for getting to grips with the model: – Unbreakable Domain Models – good quick intro. – Domain Driven Design – Patterns of Enterprise Application Architecture – Command Query Request Separation

為了回答我關於MVC的問題,他引用了大量的專有名詞…我完全看不懂,但大致知道是關於Domain Driven Design的東西。看起來這些概念已經是國外討論軟體架構的必備知識了。

而這些,在台灣很少聽人提到。


關於DDD其實我早有耳聞。Rails之父DHH在

The five programming books that meant most to me

曾經提到一本關於Domain Driven Design的書:由Eric Evans撰寫的Domain-Driven Design: Tackling Complexity in the Heart of Software

我翻過之後實在是..很厚重,再加上又是原文,唸不下去。

今天在網路上找到此書由其他作家精簡過之後,接著翻譯為簡體中文的版本

领域驱动设计精简版

我還沒有心得可以分享,因為我還沒有消化完。

但是目前翻閱了三分之一,確實非常精彩。

開卷有益啊朋友,有空翻一翻吧。

Composer進階原理:PHP命名空間與PSR-0

上次的Composer設計原理與基本用法說明了PHP套件管理的歷史與社群提出的解決之道,本篇文章接著談類別管理的進階議題。

當類別名稱一樣…

當專案大了起來,有時候會有類別名稱重複的問題。
假設今天要撰寫一個論壇模組,提供討論區與留言板功能。
你一定很想將討論區的文章與留言板的文章都命名類別為Article:

// BoardArticle.php

// ForumArticle.php

當然了,這麼做會得到一個結果:

Fatal error: Cannot redeclare class Article

這種問題有一個簡單的解決辦法,就是加上前綴字。
類別分別命名為ForumArticle與BoardArticle就可以了。

Q1: 等一下!這個解法好陽春!我看到至少4個問題:
1. 類別名稱容易變得冗長。
2. 有些類別一開始你以為不會跟人重複,結果之後真的重複了。難道永遠替類別加前綴字?
3. 類別名稱寫Article俐落多了!文章就是概念上的文章,不要逼我告訴你是討論區還是留言板!如果專案用到兩種留言板模組,分別由以前的兩個前輩寫好,難道還要逼我把作者名稱寫進去?

class TonyBoardArticle{
  //...
}
class JackBoardArticle{
  //...
}


4. 如果我在打造框架(framework)呢?幾乎會把所有常見名詞用過一次(像是Request、Loader、Response、Controller、Model等類別)!難道前面全部前綴?
看看Codeigniter的原始碼,全部用CI_當作前綴。超醜的。

命名空間(namespace)登場

於是PHP從5.3版之後支援了命名空間(namespace)。
所以可以用Article替類別定義了:

// BoardArticle.php

// ForumArticle.php

使用類別時只要加上命名空間即可:

//index.php

如果當前的php檔只用到其中一個Article類別,可以指定當前的php檔只用哪個命名空間+類別的組合:


如此一來,當php找不到Article類別時,便會去使用use關鍵字宣告的組合。
當然了,就算用use指定過,原本的宣告方式還是可以用的。(如最下方兩行所示)

當東西多了起來...

OK,可以繼續完成我們的論壇模組了!
討論區跟留言板有各自的文章,再來還需要「推文」:



使用剛剛學到的命名空間去載入他們:


果然是漂亮的各種命名阿!


Q2: include有好多行!上次的Composer設計原理與基本用法提到了Composer可以解決這種問題,當引入命名空間之後,Composer也能發揮作用嗎?

是的。

Composer登場

跟上次初學Composer一樣,建立一個composer.json檔:

{
    "autoload": {
        "files": [
            "ForumArticle.php",
            "ForumComment.php",
            "BoardArticle.php",
            "BoardComment.php"
        ]
    }
}

注意,上次我們用"classmap"指定資料夾、把資料夾內檔案全掃一次,這次我們用"files"分別設定各個檔案。

再來,在terminal輸入

composer install

執行完畢之後,跟上次一樣,只要載入一個檔案:


就可以使用各個類別囉!


Q3: 等一下!看起來跟沒有命名空間的時候差不多啊!一樣是把php檔自動require進去而已?

對啊,你最上面的原始寫法,也只是手動載入好幾個檔案,在載入的時候本來就沒有特別之處:


載入php檔就只是載入,跟命名空間是兩回事。


Q4: 還是不太對啊!上次我用classmap一次把好幾個資料夾內容掃過,這次我用files分別指定每個檔案幹嘛?Composer不是厲害在能指定資料夾去自動掃過?

......你說的沒錯。
開個my_lib資料夾,把4個php檔都丟進去吧。composer.json這樣寫就好了:

{
    "autoload": {
        "classmap": [
            "my_lib"
        ]
    }
}

再來,在terminal輸入

composer install

這樣就搞定了!
其實我只是想示範,除了用classmap設定資料夾之外,也可以用files直接指定檔案。

Q5: OK,原諒你。不過,我必須說,我今天什麼都沒學到。最後還是在composer.json寫classmap而已,跟上次一模一樣。

是的...我剛說了,載入php檔就只是載入,跟命名空間是兩回事。
我今天介紹的namespace功能是PHP本身提供的。而Composer只是協助你載入的工具、當然不可能改變程式語言本身。
Composer只是幫助你少打那一串require而已。


Q6: ㄟ等等...有個地方我覺得很醜。我們現在的my_lib資料夾裏面有4個檔案,檔名很醜:

"ForumArticle.php"
"ForumComment.php"
"BoardArticle.php"
"BoardComment.php"


類別名稱本身都是俐落的Article跟Comment了,檔案名稱還是有前綴字。但也不可能有兩個Article.php、兩個Comment.php。你有沒有辦法解決這個美感問題?

這個問題簡單,把那個my_lib資料夾刪掉,開一個Forum資料夾、一個Board資料夾。本來的ForumArticle.php改成Article.php丟進Forum資料夾、本來的BoardArticle.php改成Article.php丟進Board資料夾。composer.json改寫成這樣:

{
    "autoload": {
        "classmap": [
            "Board",
            "Forum"
        ]
    }
}

再來,在terminal輸入

composer install

這招不錯吧!檔案名稱就是類別名稱,乾淨俐落!
而且資料夾的名字本身就是namespace的名稱!
以後都這樣做啦!一用到命名空間就開個同名資料夾把檔案丟進去!

Q7: 這招我覺得還好耶...。本來檔案都放在my_lib,我在composer.json只要填my_lib一行就好,現在變成要填兩行。要是我這個論壇模組有一大堆類別、用了一大堆命名空間呢?那我classmap底下不就要填入好幾行?那我寧可全部丟進my_lib,只填my_lib一行!

唔,這樣說也是有道理。那重新建立my_lib資料夾,把Board跟Forum資料夾丟進my_lib資料夾。composer.json改回這樣寫:

{
    "autoload": {
        "classmap": [
            "my_lib"
        ]
    }
}

再來,在terminal輸入

composer install

classmap不只是告訴Composer去載入哪幾個資料夾內的檔案,還會把資料夾內的資料夾也全部掃過一次。
怎麼樣,Composer夠神吧。


Q8: 原來classmap底下會遞迴掃描下去...。我決定了,我的Forum跟Board都是在提供線上討論功能,我決定替我這個模組命名為Discussion。我要在my_lib底下開Discussion資料夾,然後把原本的Forum跟Board資料夾都丟進去。你覺得這個想法如何?

還不錯。一個Discussion資料夾就是你的整個Discussion模組。
提供了討論區、留言板功能的Discussion模組,我喜歡。


Q9: 好像忘了什麼...。啊,剛剛說命名空間跟檔案結構符合會最漂亮。那我要把那4個檔案的namespace改成這樣:



剛剛說了,載入檔案就只是載入檔案,跟命名空間無關。
現在檔案結構沒變,所以我應該不用重新輸入composer install。
讓我試試...。
靠!怎麼噴error了!你騙我?

Fatal error: Class 'Discussion\Forum\Comment' not found 

呃...,我前面的說法確實有點誤導。
PHP自動載入的基本函式長這樣:

void __autoload ( string $class )

如你所見,PHP至少需要Composer提供資訊指出$class該去哪個檔案找。
namespace改變之後,PHP會找不到對應的$class在哪。
所以還是輸入一下composer install吧!Composer會把需要的資訊整理好的。


Q10: OKOK,我知道了,我駕馭這一切了。我覺得這個Dicsussion模組真的超屌的,不但命名空間漂亮,連檔案結構都漂亮。我要把這個Discussion資料夾整個丟給我朋友,他們公司最近需要論壇模組。
讓我打電話給他...。
「什麼?你們已經做好半個論壇模組了?你只需要我模組的其中幾個功能?你們的模組也是放在Discussion資料夾?」
糟糕,資料夾名稱重複了!所以我的模組拿給別人還是有不相容的可能,怎麼辦?

沒有錯..還記得你Q1提到的第3個狀況嗎?確實有把作者名稱加進去的必要!
別怕,我教你。你開一個Tony資料夾,把整個Discussion資料夾丟進去。
接著所有檔案namespace改成像這樣:


要用的時候就這樣喔:


是變得有點長啦。
但這下搞定了吧!作者名稱再撞到的話,就改個獨特的名稱就是了!

終於。讓我們談談PSR-0

你一定常聽到PSR-0對吧!
PSR-0是PHP跨框架相容性統一標準組織訂出來的自動載入慣例。

來談談PSR-0幾個最重要的要求吧!

* 命名空間加上類別名稱一定要長這樣:

\\(\)*

* 前面一定要是作者名稱
* 中間可以有任意層次的命名空間、最後是類別名稱。
* 中間任意層次的命名空間直接對應到檔案結構。

發現了嗎?在剛剛Q1~Q10的過程中,其實你已經把PSR-0學完了,連設計原理都一起搞懂了。

懂這些之後,你也可以做好自己的模組、發佈到Packagist給全世界透過composer下載、使用了!

最後,如果遵守psr-0的話,composer.json可以這樣寫:

{
    "autoload": {
        "psr-0": {
            "Tony\\Discussion\\": "my_lib/"
        }        
    }
}

注意,雙引號、兩次反斜線並沒有特別意思,只是json規定的格式。

跟classmap一樣都可以完成任務。兩者其實是有差別的...,我們下次再談。

結語

Composer工具以及PHP-FIG組織的出現,讓一直以來散落各地的PHP社群開始有集中的趨勢。
換句話說,各社群終於能共享彼此的library了。
然而,如你所見,psr-0不但導致檔案結構容易變得深層,還要求檔案結構必須配合程式碼,這也招致了不少批評
除此之外,composer autoload內的classmap跟psr-0到底如何分工?效能差異又為何?這些問題也都還在爭論與驗證當中。

不過,PSR-0在各框架已被廣泛支援,因此建議你還是需要有所瞭解。

最後...

現在已經出現psr-0的替代方案,稱為psr-4

PSR-0從2014-10-21開始被註明為不建議使用。
至於PSR-4..我們下次再談。

Laravel:20分鐘完成Facebook登入功能

要實作Facebook的OAuth 2.0登入流程,最基本的方法是先瞭解OAuth 2.0協定內容,接著到Facebook官網下載程式語言的SDK、註冊應用程式,然後照著官方文件實作。

但如果使用Laravel的話,直接使用社群提供的package就可以囉。本文介紹artdarek/oauth-4-laravel的使用。

首先要下載這個套件。在指令列輸入:

composer require 'artdarek/oauth-4-laravel:dev-master'

自動下載完畢之後,在’app/config/app.php’內加入兩個value:

'providers' => array(
    // ...

    'Artdarek\OAuth\OAuthServiceProvider'
)

'aliases' => array(
    // ...

    'OAuth' => 'Artdarek\OAuth\Facade\OAuth',
)

Laravel就知道去哪找這個套件了。

然後在’app/config/’底下建立’oauth-4-laravel.php’設定檔:

 'Session', 

    /**
     * Consumers
     */
    'consumers' => array(

        /**
         * Facebook
         */
        'Facebook' => array(
            'client_id'     => '',
            'client_secret' => '',
            'scope'         => array(),
        ),      

    )

);

其中的client_id跟client_secret需要跟Facebook取得,請到Facebook Developers註冊一個app:
上面的Apps => Add a New App => Website => 輸入app名稱 => Create New Facebook App ID => 搞定。
把id 跟 secret複製貼上到這邊。
scopte留空陣列,只會跟臉書拿到基本公開資料。如果想要email的話,就在陣列內加入’email’。
關於權限請參考官網:Permissions with Facebook Login

然後在’app/routes.php’加入你希望的route:
(假設你有個負責會員資料的controller稱為MemberController)

Route::get('/auth/facebook', 'MemberController@loginWithFacebook');

在你希望提供facebook登入的頁面加入:

    Login with Facebook

然後到你的MemberController加入這段程式碼:

public function loginWithFacebook() {

    // get data from input
    $code = Input::get( 'code' );

    // get fb service
    $fb = OAuth::consumer( 'Facebook' );

    // check if code is valid

    // if code is provided get user data and sign in
    if ( !empty( $code ) ) {

        // This was a callback request from facebook, get the token
        $token = $fb->requestAccessToken( $code );

        // Send a request with it
        $result = json_decode( $fb->request( '/me' ), true );

        $message = 'Your unique facebook user id is: ' . $result['id'] . ' and your name is ' . $result['name'];
        echo $message. "
"; //Var_dump //display whole array(). dd($result); } // if not ask for permission first else { // get fb authorization $url = $fb->getAuthorizationUri(); // return to facebook login url return Redirect::to( (string)$url ); } }

大功告成!按下你剛設定的’Login with Facebook’按鈕,你會看到Faecbok要求授權、之後會在你的網站上顯示從Facebook撈到的個資。如何使用這些個資就看您打算如何應用囉。

Q&A

Q: 雖然很神奇,但這從頭到尾黑箱作業。背後到底發生什麼事我都不知道,要我如何敢照做?

問得好。其實,會這麼神奇是因為…

套件管理使用了Composer,所以只要輸入composer require就會自動下載完畢。不放心的話請到Composer官網逛逛,或是閱讀我之前寫的Composer設計原理與基本用法

除此之外,一般來說載好第3方套件之後,通常你至少需要初始化這個第3方類別、接著設定幾個值之類的:

$coolLibrary = new ThirdPartyLibrary();
$coolLibrary->setParameter($paramater)

但是Laravel提供Service Provider功能,讓package的初始化在Service Provider內完成,然後Laravel又提供Facades功能,讓你可以使用類別靜態函式去操作某個實體。
結果就是你只需要寫這行:

 $fb = OAuth::consumer( 'Facebook' );

你就可以用這$fb變數去進行各種操作…驗證、發送API request、等等。
當然這種靜態函式包裝某個實體的作法,有些人不喜歡、認為這反OOP。參見:讓你少打很多字:Facades。

by 阿川先生