Laravel: Sự kiện (Events)


Khóa học qua video:
Lập trình Python All Lập trình C# All SQL Server All Lập trình C All Java PHP HTML5-CSS3-JavaScript
Đăng ký Hội viên
Tất cả các video dành cho hội viên

 

Giới thiệu

Các sự kiện của Laravel cung cấp một triển khai mẫu quan sát đơn giản, cho phép bạn đăng ký và lắng nghe các sự kiện khác nhau xảy ra trong ứng dụng của bạn. Các lớp sự kiện thường được lưu trữ trong app/Eventsthư mục, trong khi các bộ lắng nghe của chúng được lưu trữ trong app/Listeners. Đừng lo lắng nếu bạn không thấy những thư mục này trong ứng dụng của mình vì chúng sẽ được tạo cho bạn khi bạn tạo sự kiện và trình nghe bằng cách sử dụng lệnh Artisan console.

Sự kiện là một cách tuyệt vời để tách các khía cạnh khác nhau của ứng dụng của bạn, vì một sự kiện duy nhất có thể có nhiều người nghe không phụ thuộc vào nhau. Ví dụ: bạn có thể muốn gửi thông báo Slack cho người dùng của mình mỗi khi đơn đặt hàng được giao. Thay vì ghép mã xử lý đơn đặt hàng của bạn với mã thông báo Slack, bạn có thể nêu ra một App\Events\OrderShippedsự kiện mà người nghe có thể nhận và sử dụng để gửi thông báo Slack.

 

Đăng ký sự kiện & người nghe

Phần mềm đi App\Providers\EventServiceProviderkèm với ứng dụng Laravel của bạn cung cấp một nơi thuận tiện để đăng ký tất cả các trình nghe sự kiện của ứng dụng của bạn. Các listenbất động sản có chứa một mảng của tất cả các sự kiện (phím) và thính giả của họ (giá trị). Bạn có thể thêm bao nhiêu sự kiện vào mảng này nếu ứng dụng của bạn yêu cầu. Ví dụ: hãy thêm một OrderShippedsự kiện:

use App\Events\OrderShipped;
use App\Listeners\SendShipmentNotification;

/**
 * The event listener mappings for the application.
 *
 * @var array
 */
protected $listen = [
    OrderShipped::class => [
        SendShipmentNotification::class,
    ],
];

 

Các event:listlệnh có thể được sử dụng để hiển thị một danh sách tất cả các sự kiện và thính giả đã đăng ký ứng dụng của bạn.

 

 

Tạo sự kiện & người nghe

Tất nhiên, việc tạo thủ công các tệp cho từng sự kiện và trình nghe là rất phức tạp. Thay vào đó, hãy thêm người nghe và sự kiện vào của bạn EventServiceProvidervà sử dụng event:generatelệnh Artisan. Lệnh này sẽ tạo ra bất kỳ sự kiện hoặc trình nghe nào được liệt kê trong của bạn EventServiceProviderchưa tồn tại:

php artisan event:generate

Ngoài ra, bạn có thể sử dụng lệnh make:eventvà make:listenerArtisan để tạo các sự kiện và trình nghe riêng lẻ:

php artisan make:event PodcastProcessed

php artisan make:listener SendPodcastNotification --event=PodcastProcessed

 

Đăng ký sự kiện theo cách thủ công

Thông thường, các sự kiện nên được đăng ký thông qua EventServiceProvider $listenmảng; tuy nhiên, bạn cũng có thể đăng ký lớp hoặc đóng trình xử lý sự kiện dựa trên thủ công trong bootphương thức của bạn EventServiceProvider:

use App\Events\PodcastProcessed;
use App\Listeners\SendPodcastNotification;
use Illuminate\Support\Facades\Event;

/**
 * Register any other events for your application.
 *
 * @return void
 */
public function boot()
{
    Event::listen(
        PodcastProcessed::class,
        [SendPodcastNotification::class, 'handle']
    );

    Event::listen(function (PodcastProcessed $event) {
        //
    });
}

 

Người nghe sự kiện ẩn danh có thể xếp hàng đợi

Khi đăng ký trình xử lý sự kiện dựa trên bao đóng theo cách thủ công, bạn có thể bọc trình nghe đóng cửa trong Illuminate\Events\queueablehàm để hướng dẫn Laravel thực thi trình nghe bằng cách sử dụng hàng đợi :

use App\Events\PodcastProcessed;
use function Illuminate\Events\queueable;
use Illuminate\Support\Facades\Event;

/**
 * Register any other events for your application.
 *
 * @return void
 */
public function boot()
{
    Event::listen(queueable(function (PodcastProcessed $event) {
        //
    }));
}

Giống như việc xếp hàng đợi, bạn có thể sử dụng onConnectiononQueuevà delayphương pháp để tùy chỉnh việc thực hiện của người nghe xếp hàng đợi:

Event::listen(queueable(function (PodcastProcessed $event) {
    //
})->onConnection('redis')->onQueue('podcasts')->delay(now()->addSeconds(10)));

Nếu bạn muốn xử lý lỗi trình nghe được xếp hàng đợi ẩn danh, bạn có thể cung cấp catchphương thức đóng lại trong khi xác định trình queueablenghe. Việc đóng này sẽ nhận được phiên bản sự kiện và Throwablephiên bản gây ra lỗi của người nghe:

use App\Events\PodcastProcessed;
use function Illuminate\Events\queueable;
use Illuminate\Support\Facades\Event;
use Throwable;

Event::listen(queueable(function (PodcastProcessed $event) {
    //
})->catch(function (PodcastProcessed $event, Throwable $e) {
    // The queued listener failed...
}));

 

Trình nghe sự kiện ký tự đại diện

Bạn thậm chí có thể đăng ký trình lắng nghe bằng cách sử dụng *tham số ký tự đại diện, cho phép bạn bắt nhiều sự kiện trên cùng một trình nghe. Trình nghe ký tự đại diện nhận tên sự kiện làm đối số đầu tiên và toàn bộ mảng dữ liệu sự kiện làm đối số thứ hai:

Event::listen('event.*', function ($eventName, array $data) {
    //
});

 

Khám phá sự kiện

Thay vì đăng ký các sự kiện và trình nghe theo cách thủ công trong $listenmảng của EventServiceProvider, bạn có thể bật tính năng tự động phát hiện sự kiện. Khi tính năng khám phá sự kiện được bật, Laravel sẽ tự động tìm và đăng ký các sự kiện cũng như trình nghe của bạn bằng cách quét thư mục ứng dụng của bạn Listeners. Ngoài ra, mọi sự kiện được xác định rõ ràng được liệt kê trong di EventServiceProviderchúc vẫn được đăng ký.

Laravel tìm các trình xử lý sự kiện bằng cách quét các lớp trình xử lý bằng cách sử dụng các dịch vụ phản ánh của PHP. Khi Laravel tìm thấy bất kỳ phương thức nào của lớp trình nghe bắt đầu bằng handle, Laravel sẽ đăng ký các phương thức đó làm trình xử lý sự kiện cho sự kiện được gợi ý kiểu trong chữ ký của phương thức:

use App\Events\PodcastProcessed;

class SendPodcastNotification
{
    /**
     * Handle the given event.
     *
     * @param  \App\Events\PodcastProcessed  $event
     * @return void
     */
    public function handle(PodcastProcessed $event)
    {
        //
    }
}

Tính năng khám phá sự kiện bị tắt theo mặc định, nhưng bạn có thể bật tính năng này bằng cách ghi đè shouldDiscoverEventsphương thức của ứng dụng của bạn EventServiceProvider:

/**
 * Determine if events and listeners should be automatically discovered.
 *
 * @return bool
 */
public function shouldDiscoverEvents()
{
    return true;
}

Theo mặc định, tất cả người nghe trong thư mục ứng dụng của app/Listenersbạn sẽ được quét. Nếu bạn muốn xác định các thư mục bổ sung để quét, bạn có thể ghi đè discoverEventsWithinphương thức trong EventServiceProvider:

/**
 * Get the listener directories that should be used to discover events.
 *
 * @return array
 */
protected function discoverEventsWithin()
{
    return [
        $this->app->path('Listeners'),
    ];
}

 

Khám phá sự kiện trong sản xuất

Trong quá trình sản xuất, khung công tác quét tất cả người nghe của bạn theo mọi yêu cầu sẽ không hiệu quả. Do đó, trong quá trình triển khai, bạn nên chạy event:cachelệnh Artisan để lưu vào bộ nhớ cache một tệp kê khai của tất cả các sự kiện và trình nghe ứng dụng của bạn. Bản kê khai này sẽ được khuôn khổ sử dụng để tăng tốc quá trình đăng ký sự kiện. Các event:clearlệnh có thể được sử dụng để tiêu diệt các bộ nhớ cache.

 

Xác định sự kiện

Một lớp sự kiện về cơ bản là một vùng chứa dữ liệu chứa thông tin liên quan đến sự kiện. Ví dụ: giả sử một App\Events\OrderShippedsự kiện nhận được một đối tượng Eloquent ORM :

<?php

namespace App\Events;

use App\Models\Order;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

class OrderShipped
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

    /**
     * The order instance.
     *
     * @var \App\Models\Order
     */
    public $order;

    /**
     * Create a new event instance.
     *
     * @param  \App\Models\Order  $order
     * @return void
     */
    public function __construct(Order $order)
    {
        $this->order = $order;
    }
}

Như bạn có thể thấy, lớp sự kiện này không chứa logic. Nó là một thùng chứa cho App\Models\Orderví dụ đã được mua. Các SerializesModelsđặc điểm sử dụng bởi sự kiện này một cách duyên dáng sẽ serialize bất kỳ mô hình hùng biện nếu đối tượng Sự kiện này được đăng bằng PHP serializechức năng, chẳng hạn như khi sử dụng các thính giả xếp hàng đợi .

 

Xác định người nghe

Tiếp theo, chúng ta hãy xem xét trình lắng nghe cho sự kiện ví dụ của chúng tôi. Người nghe sự kiện nhận các cá thể sự kiện trong handlephương thức của họ . Các lệnh event:generatevà make:listenerArtisan sẽ tự động nhập lớp sự kiện thích hợp và nhập gợi ý sự kiện trên handlephương thức. Trong handlephương pháp này, bạn có thể thực hiện bất kỳ hành động nào cần thiết để phản hồi sự kiện:

<?php

namespace App\Listeners;

use App\Events\OrderShipped;

class SendShipmentNotification
{
    /**
     * Create the event listener.
     *
     * @return void
     */
    public function __construct()
    {
        //
    }

    /**
     * Handle the event.
     *
     * @param  \App\Events\OrderShipped  $event
     * @return void
     */
    public function handle(OrderShipped $event)
    {
        // Access the order using $event->order...
    }
}

 

Người nghe sự kiện của bạn cũng có thể nhập gợi ý bất kỳ phụ thuộc nào họ cần vào các hàm tạo của chúng. Tất cả các trình xử lý sự kiện được giải quyết thông qua vùng chứa dịch vụ Laravel , vì vậy các phần phụ thuộc sẽ được đưa vào tự động.

 

 

Ngừng truyền bá một sự kiện

Đôi khi, bạn có thể muốn dừng việc truyền bá một sự kiện cho những người nghe khác. Bạn có thể làm như vậy bằng cách quay lại falsetừ handlephương pháp của người nghe của bạn .

 

Người nghe sự kiện được xếp hàng đợi

Xếp hàng người nghe có thể có lợi nếu người nghe của bạn thực hiện một tác vụ chậm như gửi email hoặc thực hiện một yêu cầu HTTP. Trước khi sử dụng trình nghe được xếp hàng đợi, hãy đảm bảo định cấu hình hàng đợi của bạn và khởi động trình xử lý hàng đợi trên máy chủ hoặc môi trường phát triển cục bộ của bạn.

Để chỉ định rằng một người nghe phải được xếp hàng đợi, hãy thêm ShouldQueuegiao diện vào lớp người nghe. Các trình nghe được tạo bởi lệnh event:generateand make:listenerArtisan đã có giao diện này được nhập vào không gian tên hiện tại để bạn có thể sử dụng nó ngay lập tức:

<?php

namespace App\Listeners;

use App\Events\OrderShipped;
use Illuminate\Contracts\Queue\ShouldQueue;

class SendShipmentNotification implements ShouldQueue
{
    //
}

Đó là nó! Bây giờ, khi một sự kiện được xử lý bởi trình lắng nghe này được gửi đi, trình xử lý sẽ tự động được xếp hàng bởi trình điều phối sự kiện bằng cách sử dụng hệ thống hàng đợi của Laravel . Nếu không có ngoại lệ nào được ném ra khi trình lắng nghe được thực thi bởi hàng đợi, công việc được xếp hàng sẽ tự động bị xóa sau khi nó xử lý xong.

 

Tùy chỉnh kết nối hàng đợi & tên hàng đợi

Nếu bạn muốn tùy chỉnh kết nối hàng đợi, tên hàng đợi, hoặc đợi thời gian trễ của một người biết lắng nghe sự kiện, bạn có thể xác định $connection$queuehoặc $delaytài sản trên lớp nghe của bạn:

<?php

namespace App\Listeners;

use App\Events\OrderShipped;
use Illuminate\Contracts\Queue\ShouldQueue;

class SendShipmentNotification implements ShouldQueue
{
    /**
     * The name of the connection the job should be sent to.
     *
     * @var string|null
     */
    public $connection = 'sqs';

    /**
     * The name of the queue the job should be sent to.
     *
     * @var string|null
     */
    public $queue = 'listeners';

    /**
     * The time (seconds) before the job should be processed.
     *
     * @var int
     */
    public $delay = 60;
}

Nếu bạn muốn xác định kết nối hàng đợi của trình lắng nghe hoặc tên hàng đợi trong thời gian chạy, bạn có thể xác định viaConnectionhoặc viaQueuecác phương thức trên trình nghe:

/**
 * Get the name of the listener's queue connection.
 *
 * @return string
 */
public function viaConnection()
{
    return 'sqs';
}

/**
 * Get the name of the listener's queue.
 *
 * @return string
 */
public function viaQueue()
{
    return 'listeners';
}

 

Người nghe xếp hàng có điều kiện

Đôi khi, bạn có thể cần phải xác định xem một người nghe có nên được xếp hàng đợi hay không dựa trên một số dữ liệu chỉ có sẵn trong thời gian chạy. Để thực hiện điều này, một shouldQueuephương pháp có thể được thêm vào trình nghe để xác định xem người nghe có nên được xếp hàng đợi hay không. Nếu shouldQueuephương thức trả về false, trình nghe sẽ không được thực thi:

<?php

namespace App\Listeners;

use App\Events\OrderCreated;
use Illuminate\Contracts\Queue\ShouldQueue;

class RewardGiftCard implements ShouldQueue
{
    /**
     * Reward a gift card to the customer.
     *
     * @param  \App\Events\OrderCreated  $event
     * @return void
     */
    public function handle(OrderCreated $event)
    {
        //
    }

    /**
     * Determine whether the listener should be queued.
     *
     * @param  \App\Events\OrderCreated  $event
     * @return bool
     */
    public function shouldQueue(OrderCreated $event)
    {
        return $event->order->subtotal >= 5000;
    }
}

 

Tương tác thủ công với hàng đợi

Nếu bạn cần truy cập thủ công các phương thức deletevà công việc hàng đợi cơ bản của người nghe release, bạn có thể làm như vậy bằng cách sử dụng Illuminate\Queue\InteractsWithQueueđặc điểm. Đặc điểm này được nhập theo mặc định trên các trình nghe đã tạo và cung cấp quyền truy cập vào các phương thức sau:

<?php

namespace App\Listeners;

use App\Events\OrderShipped;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;

class SendShipmentNotification implements ShouldQueue
{
    use InteractsWithQueue;

    /**
     * Handle the event.
     *
     * @param  \App\Events\OrderShipped  $event
     * @return void
     */
    public function handle(OrderShipped $event)
    {
        if (true) {
            $this->release(30);
        }
    }
}

 

Người nghe sự kiện được xếp hàng đợi & Giao dịch cơ sở dữ liệu

Khi các trình nghe trong hàng đợi được gửi đi trong các giao dịch cơ sở dữ liệu, chúng có thể được hàng đợi xử lý trước khi giao dịch cơ sở dữ liệu được cam kết. Khi điều này xảy ra, bất kỳ cập nhật nào bạn đã thực hiện cho các mô hình hoặc bản ghi cơ sở dữ liệu trong quá trình giao dịch cơ sở dữ liệu có thể chưa được phản ánh trong cơ sở dữ liệu. Ngoài ra, bất kỳ mô hình hoặc bản ghi cơ sở dữ liệu nào được tạo trong giao dịch có thể không tồn tại trong cơ sở dữ liệu. Nếu trình lắng nghe của bạn phụ thuộc vào các mô hình này, các lỗi không mong muốn có thể xảy ra khi công việc điều động trình nghe được xếp hàng đợi được xử lý.

Nếu after_committùy chọn cấu hình của kết nối hàng đợi của bạn được đặt thành false, bạn vẫn có thể chỉ ra rằng một trình nghe được xếp hàng cụ thể sẽ được gửi đi sau khi tất cả các giao dịch cơ sở dữ liệu mở đã được cam kết bằng cách xác định một thuộc $afterCommittính trên lớp trình nghe:

<?php

namespace App\Listeners;

use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;

class SendShipmentNotification implements ShouldQueue
{
    use InteractsWithQueue;

    public $afterCommit = true;
}

 

Để tìm hiểu thêm về cách giải quyết những vấn đề này, vui lòng xem lại tài liệu liên quan đến các công việc được xếp hàng đợi và giao dịch cơ sở dữ liệu .

 

 

Xử lý công việc không thành công

Đôi khi người nghe sự kiện được xếp hàng đợi của bạn có thể thất bại. Nếu trình nghe trong hàng đợi vượt quá số lần thử tối đa do nhân viên hàng đợi của bạn xác định, failedphương thức sẽ được gọi trên trình nghe của bạn. Các failedphương pháp nhận trường hợp sự kiện và Throwablegây ra sự thất bại:

<?php

namespace App\Listeners;

use App\Events\OrderShipped;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;

class SendShipmentNotification implements ShouldQueue
{
    use InteractsWithQueue;

    /**
     * Handle the event.
     *
     * @param  \App\Events\OrderShipped  $event
     * @return void
     */
    public function handle(OrderShipped $event)
    {
        //
    }

    /**
     * Handle a job failure.
     *
     * @param  \App\Events\OrderShipped  $event
     * @param  \Throwable  $exception
     * @return void
     */
    public function failed(OrderShipped $event, $exception)
    {
        //
    }
}

 

Chỉ định nỗ lực tối đa của người nghe được xếp hàng đợi

Nếu một trong những người nghe trong hàng đợi của bạn gặp lỗi, bạn có thể không muốn nó tiếp tục thử lại vô thời hạn. Do đó, Laravel cung cấp nhiều cách khác nhau để chỉ định số lần hoặc bao lâu một người nghe có thể được thử.

Bạn có thể xác định thuộc $triestính trên lớp trình nghe của mình để chỉ định số lần trình nghe có thể được thử trước khi nó được coi là không thành công:

<?php

namespace App\Listeners;

use App\Events\OrderShipped;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;

class SendShipmentNotification implements ShouldQueue
{
    use InteractsWithQueue;

    /**
     * The number of times the queued listener may be attempted.
     *
     * @var int
     */
    public $tries = 5;
}

Để thay thế cho việc xác định số lần người nghe có thể được thử trước khi thất bại, bạn có thể xác định thời điểm mà người nghe không nên thử nữa. Điều này cho phép người nghe được thử bất kỳ số lần nào trong một khung thời gian nhất định. Để xác định thời gian mà một người nghe không còn được cố gắng nữa, hãy thêm một retryUntilphương thức vào lớp người nghe của bạn. Phương thức này sẽ trả về một DateTimephiên bản:

/**
 * Determine the time at which the listener should timeout.
 *
 * @return \DateTime
 */
public function retryUntil()
{
    return now()->addMinutes(5);
}

 

Sự kiện cử

Để gửi một sự kiện, bạn có thể gọi dispatchphương thức tĩnh trên sự kiện đó. Phương pháp này được tạo sẵn trên sự kiện bởi Illuminate\Foundation\Events\Dispatchableđặc điểm. Bất kỳ đối số nào được truyền cho dispatchphương thức sẽ được chuyển đến phương thức khởi tạo của sự kiện:

<?php

namespace App\Http\Controllers;

use App\Events\OrderShipped;
use App\Http\Controllers\Controller;
use App\Models\Order;
use Illuminate\Http\Request;

class OrderShipmentController extends Controller
{
    /**
     * Ship the given order.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    public function store(Request $request)
    {
        $order = Order::findOrFail($request->order_id);

        // Order shipment logic...

        OrderShipped::dispatch($order);
    }
}

 

Khi kiểm tra, có thể hữu ích khi khẳng định rằng một số sự kiện nhất định đã được gửi đi mà không thực sự kích hoạt người nghe của chúng. Trình trợ giúp kiểm tra tích hợp của Laravel khiến nó trở nên dễ dàng.

 

 

Người đăng ký sự kiện

 

Viết người đăng ký sự kiện

Người đăng ký sự kiện là các lớp có thể đăng ký nhiều sự kiện từ chính lớp người đăng ký, cho phép bạn xác định một số trình xử lý sự kiện trong một lớp duy nhất. Người đăng ký nên xác định một subscribephương thức, phương thức này sẽ được chuyển qua một phiên bản điều phối sự kiện. Bạn có thể gọi listenphương thức trên trình điều phối đã cho để đăng ký trình xử lý sự kiện:

<?php

namespace App\Listeners;

use Illuminate\Auth\Events\Login;
use Illuminate\Auth\Events\Logout;

class UserEventSubscriber
{
    /**
     * Handle user login events.
     */
    public function handleUserLogin($event) {}

    /**
     * Handle user logout events.
     */
    public function handleUserLogout($event) {}

    /**
     * Register the listeners for the subscriber.
     *
     * @param  \Illuminate\Events\Dispatcher  $events
     * @return void
     */
    public function subscribe($events)
    {
        $events->listen(
            Login::class,
            [UserEventSubscriber::class, 'handleUserLogin']
        );

        $events->listen(
            Logout::class,
            [UserEventSubscriber::class, 'handleUserLogout']
        );
    }
}

Nếu các phương thức của trình xử lý sự kiện của bạn được xác định trong chính người đăng ký, bạn có thể thấy thuận tiện hơn khi trả về một mảng sự kiện và tên phương thức từ subscribephương thức của người đăng ký . Laravel sẽ tự động xác định tên lớp của người đăng ký khi đăng ký trình nghe sự kiện:

<?php

namespace App\Listeners;

use Illuminate\Auth\Events\Login;
use Illuminate\Auth\Events\Logout;

class UserEventSubscriber
{
    /**
     * Handle user login events.
     */
    public function handleUserLogin($event) {}

    /**
     * Handle user logout events.
     */
    public function handleUserLogout($event) {}

    /**
     * Register the listeners for the subscriber.
     *
     * @param  \Illuminate\Events\Dispatcher  $events
     * @return array
     */
    public function subscribe($events)
    {
        return [
            Login::class => 'handleUserLogin',
            Logout::class => 'handleUserLogout',
        ];
    }
}

 

Đăng ký người đăng ký sự kiện

Sau khi viết người đăng ký, bạn đã sẵn sàng để đăng ký nó với người điều phối sự kiện. Bạn có thể đăng ký người đăng ký bằng cách sử dụng $subscribetài sản trên EventServiceProvider. Ví dụ: hãy thêm UserEventSubscriberdanh sách vào danh sách:

<?php

namespace App\Providers;

use App\Listeners\UserEventSubscriber;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;

class EventServiceProvider extends ServiceProvider
{
    /**
     * The event listener mappings for the application.
     *
     * @var array
     */
    protected $listen = [
        //
    ];

    /**
     * The subscriber classes to register.
     *
     * @var array
     */
    protected $subscribe = [
        UserEventSubscriber::class,
    ];
}
» Tiếp: Thông báo (Notifications)
« Trước: Lập lịch tác vụ
Khóa học qua video:
Lập trình Python All Lập trình C# All SQL Server All Lập trình C All Java PHP HTML5-CSS3-JavaScript
Đăng ký Hội viên
Tất cả các video dành cho hội viên
Copied !!!