Tại sao Laravel ServiceProvider gọi phương thức $this->app->rebinding() "không tồn tại"?
Em có một thắc mắc về phần container rebinding như thế này:
- Class Illuminate\Container\Container có phương thức rebinding trong khi interface Illuminate\Contracts\Container\Container không khai báo phương thức này.
- Interface Illuminate\Contracts\Foundation\Application kế thừa interface Illuminate\Contracts\Container\Container nhưng cũng không hề khai báo phương thức rebinding.
- Class Illuminate\Foundation\Application kế thừa class Illuminate\Container\Container.
-
Dựa theo 3 điều trên thì em thấy rằng phương thức rebinding chỉ có thể gọi ở một thể hiện của Illuminate\Container\Container hoặc Illuminate\Foundation\Application
-
Trong khi đó, Class Illuminate/Support/ServiceProvider có thuộc tính "app" là một thể hiện của interface Illuminate\Contracts\Foundation\Application.
-
Em biết rằng trong quá trình run-time, Laravel sẽ inject một thể hiện của Illuminate\Foundation\Application (cái mà có phương thức rebinding) nhưng xét trên sự khai báo của class Illuminate/Support/ServiceProvider thì việc gọi phương thức rebinding strong các ServiceProvider khác thì thật vô lý (theo ý hiểu của em). Mọi người có thể xem hai class sau:
Em vẫn không hiểu lý do tại sao Laravel lại làm như vậy mà không chịu bổ sung một số phương thức public hữu ích vào interfaces.
Laravel là framework em yêu thích nhất và thực tế nó vẫn hoạt động rất "ngon" với core như trên. Nhưng xét cho cùng, theo quan điểm cá nhân, em thấy cách code như trên thật vô lý.
Mong anh/chị giải đáp giúp em về việc sử dụng trên.
3 CÂU TRẢ LỜI
Bạn gửi mail hỏi Taylor Otwell xem sao
...
Mình đùa chút thôi theo mình thì những method signature trong Illuminate\Container\Container
có thể được coi là public API của Laravel Service Container. Và những phương thức đó được khuyên dùng trong thực tế (do đó chúng được đề cập đến trong documentation). Những phương thức trong interface (contract) trên có thể giải quyết được hầu hết các use case khi làm ứng dụng. Mình cũng đảm bảo chẳng mấy ai sử dụng rebinding
trong dự án thực tế cả.
Nhiệm vụ chính của container vẫn là binding và resolving và interface trên vẫn đảm bảo được điều đó.
PHP là một duck typed language thay vì strong typed như trong một số ngôn ngữ khác như Java, .NET. Với các ngôn ngữ dạng strong typed, method signature trong interface thường phải định nghĩa cả kiểu cho parameters và kiểu trả về --> đảm bảo type safety. Trong PHP thì không như vậy thường sẽ ko có kiểu xác định. Do đó với PHP, các phương thức của một object sẽ xác định cách object đó có thể sử dụng thay vì phải phụ thuộc vào sự kế thừa từ một class khác hoặc một thể hiện cụ thể của interface. Tất nhiên PHP vẫn có thể là một strong typed nếu muốn
@vinhnguyen Trên trang tài liệu chính thống của mình, Laravel không hướng dẫn cách dùng phương thức này. Nhưng em đọc bài viết của anh https://viblo.asia/p/laravel-service-container-in-depth-tips-to-customize-your-application-RQqKLnqNl7z#_ioc--di-3 nên em mới biết về nó và cách sử dụng. Em hiểu các nguyên tắc mà anh đề cập. Em có nói "Laravel là framework em yêu thích nhất và thực tế nó vẫn hoạt động rất "ngon" với core như trên". Em chỉ tò mò một chút về lý do mà Laravel đã làm. Em thấy nó thật khó kiểm soát.
Ông Taylor bận lắm, không có thời gian giải thích mấy thắc mắc "vớ vẩn" của em đâu. Và vì Laravel vẫn chạy ngon đó thôi.
Em cảm ơn.
@quynh001 Mình cũng không đồng tình với cách làm của Laravel.
Type hinting là contract mà lại sử dụng specific method trong concrete class thì rõ ràng là không hợp lý.
Theo mình contract sinh ra là để có thể custom, thay đổi implement dễ dàng và chỉ cần refer đến contract. Chẳng lẽ đã có contract rồi lại phải tìm trong concrete class xem có method nào khác để implement?
Simplicity
When all of Laravel's services are neatly defined within simple interfaces, it is very easy to determine the functionality offered by a given service. The contracts serve as succinct documentation to the framework's features.
In addition, when you depend on simple interfaces, your code is easier to understand and maintain. Rather than tracking down which methods are available to you within a large, complicated class, you can refer to a simple, clean interface.
Cũng có nhiều issue về vấn đề này nhưng có issue được fix, có issue thì không, và vẫn chưa có câu trả lời nào xác đáng cả
- https://github.com/laravel/framework/issues/18361
- https://github.com/laravel/framework/issues/18792
- https://github.com/laravel/framework/issues/20472
- https://github.com/laravel/framework/issues/11200
Trong pull này ông GrahamCampbell có trả lời:
I understand what you are saying that this is technically incorrect without the method on the interface. I understand that just fine, but, this is not the only place where this occurs, and Taylor intentionally missed out some methods.
Có vẻ như có quá nhiều chỗ đã dùng theo cách này, giờ nếu thêm method vào interface có thể gây ra breaking changes nên họ quyết định là won't fix
Còn đoạn này thì mình không hiểu lắm: Taylor cố tình bỏ qua một số method??
Taylor intentionally missed out some methods
Có khi bạn hỏi ông Taylor phát cho a e thông suốt được không
@pht Cảm ơn anh đã hiểu câu hỏi của em và nêu ra những ví dụ khác liên quan để thấy vấn đề rõ ràng hơn.
Nhìn chung, em nghĩ đi dùng "hàng" của người khác thì phải chấp nhận làm theo hướng dẫn sử dụng của họ.
Thôi thì code cũ chạy ngon thì kệ nó, mình viết code mới thì tạo contract mới thôi. Không lại gặp trường hợp vào đọc container, rồi viết một class khác implements đúng interface, quay ra thấy "method not exist" thì mệt lắm.
Theo anh mọi chuyện khá là đơn giản, chứ không quá phức tạp như em nghĩ đâu
Em có thể tham khảo file public/index.php
để thấy biến $app
được tạo ra như thế nào
$app = require_once __DIR__.'/../bootstrap/app.php';
cụ thể hơn, trong file bootstrap/app.php
thì
$app = new Illuminate\Foundation\Application(
realpath(__DIR__.'/../')
);
Như vậy thì $app
là một instance của class Illuminate\Foundation\Application
, và sẽ luôn luôn là như vậy, không hề có chuyện $app
được resolve ra từ một class khác có implement Illuminate\Contracts\Container\Container
đâu em. (Đương nhiên rồi, khi mà $app
chính là service container, nó không thể tự bind
hay resolve
chính nó (^^;))
Thế nên việc trong class Illuminate\Foundation\Application
có những hàm mà trong interface Illuminate\Contracts\Container\Container
không có thì theo anh nghĩ cũng không phải là vấn đề gì cả đâu
Nó chỉ trở thành vấn đề, khi mà như em nói, ta type hint một interface, nhưng lại sử dụng hàm không có trong interface đó thôi Ví dụ như
interface A
{
function a () {}
}
class B
{
function b (A $a)
{
$a->c(); // $a gọi đến hàm không có trong interface A
}
}
@thangtd90 Typehint chính là vấn đề mà em gặp phải, mà nguồn Laravel đã quá "linh động"
Kì thực, nếu chúng ta chỉ sử dụng concrete mặc định mà không thay đổi thì chẳng có vấn đề gì cả.
- Nhưng đây là constructor của ServiceContainer
/**
* Create a new service provider instance.
*
* @param \Illuminate\Contracts\Foundation\Application $app
* @return void
*/
public function __construct($app)
{
$this->app = $app;
}
- Laravel đã typehint interface Application nhưng lại sử dụng method từ concrete mặc định của nó. Em không thích cách làm như thế. Đây chỉ là ví đụ nhỏ em thấy. Còn nhiều trường hợp typehint khác mà ông Taylor vẫn chấp nhận điều tương tự. Đó là lý do của tác giả, em chỉ thắc mắc chút thôi.
Cụ thể, vấn đề của em gặp phải như sau:
-
Khi viết một package, chắc chắn không nên sử dụng integration services từ package khác mà nên dùng dependencies, ngay cả khi package chỉ phục vụ cho một framework duy nhất. Em nghĩ tốt nhất là như vậy.
-
Trong package của mình, em cần sử dụng type-hint Session interface và cần sử dụng method flash.
-
Em tìm hiểu thì thấy lluminate\Contracts\Session\Session thì không có method flash. Flash chỉ có trong concrete mặc định của nó là Illuminate\Session\Store.
-
Như vậy, hoặc thay vì type-hint interface, em phải type-hint concrete hoặc em cần tự khai báo một interface mới có method flash.
-
Mà em thì chọn cách type-hint interface, nó thuận lợi cho việc mở rộng và dễ test hơn.
@quynh001 Đoạn trên thì cũng không hẳn là type hint, mà chỉ là comment cho kiểu của biến thôi (^^;) nhưng đúng là nên để tên class Illuminate\Foundation\Application
ở đây thì hơn =))
Khi viết một package, chắc chắn không nên sử dụng integration services từ package khác mà nên dùng dependencies, ngay cả khi package chỉ phục vụ cho một framework duy nhất. Em nghĩ tốt nhất là như vậy.
Câu này thì anh không hiểu lắm (^^;)
Em tìm hiểu thì thấy lluminate\Contracts\Session\Session thì không có method flash. Flash chỉ có trong concrete mặc định của nó là Illuminate\Session\Store.
Cái này thì đúng là có vấn đề (^^;), một là em không type hint interface nữa, gọi helper của laravel thôi :v Type hint interface thì không nên gọi hàm không có trong interface đó
Mà em thì chọn cách type-hint interface, nó thuận lợi cho việc mở rộng và dễ test hơn.
Anh cũng nghĩ vậy, type hint thì nên là interface
@thangtd90 Trong trường hợp này, integration services mà em nói chính là helpers và facades mà Laravel đã viết đó anh. :v Em thích phong cách code rõ ràng hơn.
@thangtd90 Laravel khai báo một danh sách các core aliases trong Illuminate\Foundation\Application
'session.store' => [\Illuminate\Session\Store::class, \Illuminate\Contracts\Session\Session::class],
Dựa theo khai báo này, khi muốn làm việc với Session mà muốn type-hint thay vì sử dụng helpers hay Facade thì rõ ràng em nên type-hint interface lluminate\Contracts\Session\Session. Mà interface này thì không có method flash.
@quynh001 anh nghĩ contracts (interfaces) có mục đích chính là giải quyết vấn đề tightly coupled trong logic của mình, nhất là với các component dạng driver-based. Nó sẽ thuận tiện khi em viết unit test chẳng hạn. Việc các interface không có đầy đủ các phương thức cũng có thể coi là một điểm trừ nhưng theo anh thì nó giống như trade-offs giữa sự đơn giản và tính đầy đủ ấy. Thôi thì không thích thì mình tự customize lại
@quynh001 Cảm ơn anh đã phân tích rất hay và chi tiết. Em không coi đây là điểm yếu vì Laravel vẫn quá "pro" và vì em không giám chắc rằng mình viết được concrete nào "hay ho" hơn concrete mặc định của nó.
Giống như anh nói "trade-offs giữa sự đơn giản và tính đầy đủ ấy" và phân tích.
PHP là một duck typed language thay vì strong typed như trong một số ngôn ngữ khác như Java, .NET. Với các ngôn ngữ dạng strong typed, method signature trong interface thường phải định nghĩa cả kiểu cho parameters và kiểu trả về --> đảm bảo type safety. Trong PHP thì không như vậy thường sẽ ko có kiểu xác định. Do đó với PHP, các phương thức của một object sẽ xác định cách object đó có thể sử dụng thay vì phải phụ thuộc vào sự kế thừa từ một class khác hoặc một thể hiện cụ thể của interface. Tất nhiên PHP vẫn có thể là một strong typed nếu muốn
Em chỉ không thích phong cách code như vậy. Với em, khi đã viết interface và type-hint interface, chỉ nên gọi những phương thức đã được định nghĩa trên interface đó.
@thangtd90 @vinhnguyen Hai anh phân tích đầy đủ và chi tiết quá. Em không thể accept hai câu trả lời được sao?
@quynh001 Mình thật sự vẫn chưa hiểu được câu hỏi của bạn.
Thứ 1: Đúng là chỉ có thể gọi rebinding qua các instance của Illuminate\Foundation\Application
và Illuminate\Container\Container
vì chỉ 2 class này có method đấy. Và Illuminate\Foundation\Application
rõ ràng là extends từ Illuminate\Container\Container
mà Container lại có rebinding nên đương nhiên gọi được.
Thứ 2: Và trong ServerProvider gọi các method bind
, rebinding
là thông qua instance của Application
mà bạn nhỉ? Nếu trong ServiceProvider bạn gọi $this->rebinding
thì chắc chắn không được rồi vì nó làm gì có nhỉ?
Thứ 3: Là bạn đang bị nhầm lẫn một chút thì phải, ServiceProvider không phải là một instance của Application.
Sr bạn cái này mình đọc nhầm. Bạn nói đúng, ServiceProvider chứa app là instance của Application.
Thứ 4: Interface chỉ là một cái contract để diễn đạt rằng các instance của interface đấy sẽ thực hiện được những hành động gì (thông qua các method trong interface). Các lớp khác nhau có thể tương tác với chúng thông qua interface mà không cần biết triển khai các method ở mỗi class ra sao. Và chỉ các method mô tả đặc trưng của class như vậy thì bỏ vào interface. Còn các method râu ria phục vụ các việc khác thì không nên để vào để giữ các contract được sạch đẹp và thể hiện đúng mô tả đặc trưng của nó.
Trên đây là ý kiến của mình, mời bạn tham khảo.
@huukimit Cảm ơn phản hồi từ bạn. Như tôi đã nói, trong quá trình run-time, Laravel inject một instance của \Illuminate\Foundation\Application vào ServiceContainer nên tôi hiểu tại sao lại gọi được $this->app->rebinding() như vậy.
Tôi đã sửa lại tiêu đề để phù hợp hơn. Tôi sẽ cố gắng giải thích lại rõ hơn. ServiceContainer khai báo thuộc tính "app" là một implementation của Illuminate\Contracts\Foundation\Application (interface này không có phương thức rebinding). Như vậy, chỉ xét riêng trong bối cảnh class này, tôi nghĩ chỉ nên gọi những phương thức trên "app" mà đã được khai báo trong interface đó.
rebinding, refresh là đặc trưng riêng mà Laravel Container định nghĩa, những sự kiện này tôi thấy có thể áp dụng như resolving và afterResolving vậy. Do đó, việc bổ sung nó vào interface \Illuminate\Contracts\Container\Container là phù hợp theo quan điểm của tôi.
@quynh001 À mình hiểu rồi, tức là bạn muốn hỏi rằng tại sao không đặt rebinding
vào trong interface Container chứ không phải là Tại sao Laravel ServiceProvider gọi phương thức $this->app->rebinding() "không tồn tại"? Vì rõ ràng method này tồn tại mà
@huukimit Có lẽ do cách trình bày của tôi khó hiểu quá.
Tôi nói lại nhé, phương thức rebinding chỉ tồn tại trên instance của Illuminate\Container\Container hoặc Illuminate\Foundation\Application. Nhưng trong bối cảnh của class ServiceContainer đặt ngoài "project Laravel". Bạn chú ý nhé, tôi nhấn mạnh vào việc "Đặt ngoài project Laravel". Nghĩa là nếu bạn nhìn vào một class ServiceContainer một cách riêng lẻ, chỉ có class này thôi, bạn sẽ thấy rằng app là một implementation của interface Illuminate\Contracts\Foundation\Application (nơi không có phương thức nào tên là rebinding cả). Bạn hiểu chứ?
@huukimit Tôi có một ví dụ như sau. Giả sử, tôi không thích dùng Illuminate\Foundation\Application mặc đinh của Laravel.
Tôi khai báo một class như sau.
// Chỉ implements interface \Illuminate\Contracts\Foundation\Application thôi nhé, không có rebinding.
class MyApplication implements \Illuminate\Contracts\Foundation\Application
{
// code
}
Đây là constrcutor của ServiceProvider, chú ý biến app.
/**
* Create a new service provider instance.
*
* @param \Illuminate\Contracts\Foundation\Application $app
* @return void
*/
public function __construct($app)
{
$this->app = $app;
}
Tôi khởi tạo thế này:
$app = new MyApplication;
$provider = new \Illuminate\Auth\AuthServiceProvider($app);
$provider->register(); // chắc chắn sẽ báo lỗi "call to undefined method "
Thật là hại não