Laravel5.5开发学习笔记

本博文用来整理在开发中遇到的Laravel新特性的笔记。

1、清除缓存命令

php artisan optimize
php artisan cache:clear
php artisan config:clear    // 清除配置文件缓存
php artisan route:clear
php artisan view:clear

2、composer

修改 composer.json 后需使用命令从新加载:php

composer dumpautoload

3、事务

DB::transaction() 方法会开启一个数据库事务,在回调函数里的全部 SQL 写操做都会被包含在这个事务里,若是回调函数抛出异常则会自动回滚这个事务,不然提交事务。用这个方法能够帮咱们节省很多代码。mysql

// 开启一个数据库事务
  $order = \DB::transaction(function() use ($user, $request){
     // 具体业务...
  });

4、异常处理

异常指的是在程序运行过程当中发生的异常事件,一般是由外部问题所致使的。
异常处理是程序开发中常常遇到的任务,如何优雅地处理异常,从必定程度上反映了你的程序是否足够严谨。laravel

咱们将异常大体分为 用户异常 和 系统异常,接下来咱们将分别对其讲解和代码实现。redis

1.用户错误行为触发的异常

好比上章节中已经验证过邮箱的用户再次去申请激活邮件时触发的异常,对于此类异常咱们须要把触发异常的缘由告知用户。sql

咱们把这类异常命名为 InvalidRequestException,能够经过 make:exception 命令来建立:数据库

$ php artisan make:exception InvalidRequestException

新建立的异常文件保存在 app/Exceptions/ 目录下:json

app/Exceptions/InvalidRequestException.php数组

<?php

namespace App\Exceptions;

use Exception;
use Illuminate\Http\Request;

class InvalidRequestException extends Exception
{
    public function __construct(string $message = "", int $code = 400)
    {
        parent::__construct($message, $code);
    }

    public function render(Request $request)
    {
        if ($request->expectsJson()) {
            // json() 方法第二个参数就是 Http 返回码
            return response()->json(['msg' => $this->message], $this->code);
        }

        return view('pages.error', ['msg' => $this->message]);
    }
}

Laravel 5.5 以后支持在异常类中定义 render() 方法,该异常被触发时系统会调用 render() 方法来输出,咱们在 render() 里判断若是是 AJAX 请求则返回 JSON 格式的数据,不然就返回一个错误页面。缓存

如今来建立这个错误页面:安全

$ touch resources/views/pages/error.blade.php

resources/views/pages/error.blade.php

@extends('layouts.app')
@section('title', '错误')

@section('content')
<div class="panel panel-default">
    <div class="panel-heading">错误</div>
    <div class="panel-body text-center">
        <h1>{{ $msg }}</h1>
        <a class="btn btn-primary" href="{{ route('root') }}">返回首页</a>
    </div>
</div>
@endsection

当异常触发时 Laravel 默认会把异常的信息和调用栈打印到日志里,好比:

而此类异常并非由于咱们系统自己的问题致使的,不会影响咱们系统的运行,若是大量此类日志打印到日志文件里反而会影响咱们去分析真正有问题的异常,所以须要屏蔽这个行为。

Laravel 内置了屏蔽指定异常写日志的解决方案:

app/Exceptions/Handler.php

.
.
.
    protected $dontReport = [
        InvalidRequestException::class,
    ];
.
.
.

当一个异常被触发时,Laravel 会去检查这个异常的类型是否在 $dontReport 属性中定义了,若是有则不会打印到日志文件中。

2.系统内部异常

好比链接数据库失败,对于此类异常咱们须要有限度地告知用户发生了什么,但又不能把全部信息都暴露给用户(好比链接数据库失败的信息里会包含数据库地址和帐号密码),所以咱们须要传入两条信息,一条是给用户看的,另外一条是打印到日志中给开发人员看的。

新建一个 InternalException 类:

$ php artisan make:exception InternalException

app/Exceptions/InternalException.php

<?php

namespace App\Exceptions;

use Exception;
use Illuminate\Http\Request;

class InternalException extends Exception
{
    protected $msgForUser;

    public function __construct(string $message, string $msgForUser = '系统内部错误', int $code = 500)
    {
        parent::__construct($message, $code);
        $this->msgForUser = $msgForUser;
    }

    public function render(Request $request)
    {
        if ($request->expectsJson()) {
            return response()->json(['msg' => $this->msgForUser], $this->code);
        }

        return view('pages.error', ['msg' => $this->msgForUser]);
    }
}

这个异常的构造函数第一个参数就是本来应该有的异常信息好比链接数据库失败,第二个参数是展现给用户的信息,一般来讲只须要告诉用户 系统内部错误 便可,由于无论是链接 Mysql 失败仍是链接 Redis 失败对用户来讲都是同样的,就是系统不可用,用户也不可能根据这个信息来解决什么问题。

使用

接下来咱们要把以前验证邮箱功能中的异常替换成咱们刚刚定义的异常。

app/Http/Controllers/EmailVerificationController.php

use App\Exceptions\InvalidRequestException;
.
.
.
    public function verify(Request $request)
    {

        $email = $request->input('email');
        $token = $request->input('token');
        if (!$email || !$token) {
            throw new InvalidRequestException('验证连接不正确');
        }
        if ($token != Cache::get('email_verification_'.$email)) {
            throw new InvalidRequestException('验证连接不正确或已过时');
        }
        if (!$user = User::where('email', $email)->first()) {
            throw new InvalidRequestException('用户不存在');
        }
        .
        .
        .
    }
    public function send(Request $request)
    {
        $user = $request->user();
        if ($user->email_verified) {
            throw new InvalidRequestException('你已经验证过邮箱了');
        }
        .
        .
        .
    }

5、延迟任务

Laravel 提供了延迟任务(Delayed Job)功能来解决购物车长时间占用库存的问题。当咱们的系统触发了一个延迟任务时,Laravel 会用当前时间加上任务的延迟时间计算出任务应该被执行的时间戳,而后将这个时间戳和任务信息序列化以后存入队列,Laravel 的队列处理器会不断查询并执行队列中知足预计执行时间等于或早于当前时间的任务。

一、建立任务

咱们经过 make:job 命令来建立一个任务:

$ php artisan make:job CloseOrder

建立的任务类保存在 app/Jobs 目录下,如今编辑刚刚建立的任务类:

app/Jobs/CloseOrder.php

<?php

namespace App\Jobs;

use Illuminate\Bus\Queueable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use App\Models\Order;

// 表明这个类须要被放到队列中执行,而不是触发时当即执行
class CloseOrder implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    protected $order;

    public function __construct(Order $order, $delay)
    {
        $this->order = $order;
        // 设置延迟的时间,delay() 方法的参数表明多少秒以后执行
        $this->delay($delay);
    }

    // 定义这个任务类具体的执行逻辑
    // 当队列处理器从队列中取出任务时,会调用 handle() 方法
    public function handle()
    {
        // 判断对应的订单是否已经被支付
        // 若是已经支付则不须要关闭订单,直接退出
        if ($this->order->paid_at) {
            return;
        }
        // 经过事务执行 sql
        \DB::transaction(function() {
            // 将订单的 closed 字段标记为 true,即关闭订单
            $this->order->update(['closed' => true]);
            // 循环遍历订单中的商品 SKU,将订单中的数量加回到 SKU 的库存中去
            foreach ($this->order->items as $item) {
                $item->productSku->addStock($item->amount);
            }
        });
    }
}

2. 触发任务

接下来咱们须要在建立订单以后触发这个任务:

app/Http/Controllers/OrdersController.php

use App\Jobs\CloseOrder;
    .
    .
    .
    public function store(Request $request)
    {
        .
        .
        .
        $this->dispatch(new CloseOrder($order, config('app.order_ttl')));

        return $order;
    }

CloseOrder 构造函数的第二个参数延迟时间咱们从配置文件中读取,为了方便咱们测试,把这个值设置成 30 秒:

config/app.php

'order_ttl' => 30,

3. 测试

默认状况下,Laravel 生成的 .env 文件里把队列的驱动设置成了 sync(同步),在同步模式下延迟任务会被当即执行,因此须要先把队列的驱动改为 redis

.env

QUEUE_DRIVER=redis

要使用 redis 做为队列驱动,咱们还须要引入 predis/predis 这个包

$ composer require predis/predis

接下来启动队列处理器:

$ php artisan queue:work

clipboard.png

6、权限控制

为了安全起见咱们只容许订单的建立者能够看到对应的订单信息,这个需求能够经过受权策略类(Policy)来实现。

经过 make:policy 命令建立一个受权策略类:

$ php artisan make:policy OrderPolicy

app/Policies/OrderPolicy.php

<?php

namespace App\Policies;

use App\Models\Order;
use App\Models\User;
use Illuminate\Auth\Access\HandlesAuthorization;

class OrderPolicy
{
    use HandlesAuthorization;

    public function own(User $user, Order $order)
    {
        return $order->user_id == $user->id;
    }
}

而后在 AuthServiceProvider 中注册这个策略:

app/Providers/AuthServiceProvider.php

use App\Models\Order;
use App\Policies\OrderPolicy;
.
.
.
    protected $policies = [
        UserAddress::class => UserAddressPolicy::class,
        Order::class       => OrderPolicy::class,
    ];

最后在 OrdersController@show() 中校验权限:

appHttp/Controllers/OrdersController.php

public function show(Order $order, Request $request)
    {
        // 权限校验
        $this->authorize('own', $order);
        return view('orders.show', ['order' => $order->load(['items.productSku', 'items.product'])]);
    }

7、封装业务代码

通常项目开始的时候业务比较简单,咱们都将业务逻辑写在了控制器,可是随着时间的增长,咱们会发现咱们在 Controller 里面写了大量的包含复杂逻辑的业务代码,这是一个坏习惯,这样子随着需求的增长,咱们的控制器很快就变得臃肿。若是之后咱们要开发 App 端,这些代码可能须要在 Api 的 Controller 里再重复一遍,假如出现业务逻辑的调整就须要修改两个或更多地方,这明显是不合理的。所以咱们须要对 逻辑复杂业务代码 进行封装。

这里咱们将在项目里采用 Service 模式来封装代码。购物车的逻辑,放置于 CartService 类里,将下单的业务逻辑代码放置于 OrderService里。

这里以电商项目的订单作示例:

一、购物车

首先建立一个 CartService 类:

$ mkdir -p app/Services && touch app/Services/CartService.php

app/Services/CartService.php

<?php

namespace App\Services;

use Auth;
use App\Models\CartItem;

class CartService
{
    public function get()
    {
        return Auth::user()->cartItems()->with(['productSku.product'])->get();
    }

    public function add($skuId, $amount)
    {
        $user = Auth::user();
        // 从数据库中查询该商品是否已经在购物车中
        if ($item = $user->cartItems()->where('product_sku_id', $skuId)->first()) {
            // 若是存在则直接叠加商品数量
            $item->update([
                'amount' => $item->amount + $amount,
            ]);
        } else {
            // 不然建立一个新的购物车记录
            $item = new CartItem(['amount' => $amount]);
            $item->user()->associate($user);
            $item->productSku()->associate($skuId);
            $item->save();
        }

        return $item;
    }

    public function remove($skuIds)
    {
        // 能够传单个 ID,也能够传 ID 数组
        if (!is_array($skuIds)) {
            $skuIds = [$skuIds];
        }
        Auth::user()->cartItems()->whereIn('product_sku_id', $skuIds)->delete();
    }
}

接下来咱们要修改 CartController,将其改成调用刚刚建立的 CartService 类:

app/Http/Controllers/CartController.php

<?php

namespace App\Http\Controllers;

use App\Models\ProductSku;
use Illuminate\Http\Request;
use App\Http\Requests\AddCartRequest;
use App\Services\CartService;

class CartController extends Controller
{
    protected $cartService;

    // 利用 Laravel 的自动解析功能注入 CartService 类
    public function __construct(CartService $cartService)
    {
        $this->cartService = $cartService;
    }

    public function index(Request $request)
    {
        // select * from product_skus where id in (xxxx)
        $cartItems = $this->cartService->get();

        $addresses = $request->user()->addresses()->orderBy('last_used_at', 'desc')->get();

        return view('cart.index', ['cartItems' => $cartItems, 'addresses' => $addresses]);
    }

    public function add(AddCartRequest $request)
    {
        $this->cartService->add($request->input('sku_id'), $request->input('amount'));

        return [];
    }



    public function remove(ProductSku $sku, Request $request)
    {
        $this->cartService->remove($sku->id);
        return [];
    }
}

这里咱们使用了 Laravel 容器的自动解析功能,当 Laravel 初始化 Controller 类时会检查该类的构造函数参数,在本例中 Laravel 会自动建立一个 CartService 对象做为构造参数传入给 CartController。

二、订单

原始订单控制器

app/Http/Controllers/OrdersController.php

<?php

namespace App\Http\Controllers;

use App\Http\Requests\OrderRequest;
use App\Models\ProductSku;
use App\Models\UserAddress;
use App\Models\Order;
use Carbon\Carbon;
use Illuminate\Http\Request;
use App\Exceptions\InvalidRequestException;
use App\Jobs\CloseOrder;
use App\Services\CartService;

class OrdersController extends Controller
{
    public function show(Order $order, Request $request)
    {

        // 权限校验
        $this->authorize('own', $order);

        // 这里的 load() 方法与上一章节介绍的 with() 预加载方法有些相似,称为 延迟预加载
        // 不一样点在于 load() 是在已经查询出来的模型上调用,而 with() 则是在 ORM 查询构造器上调用。

        return view('orders.show', ['order' => $order->load(['items.productSku', 'items.product'])]);
    }

    public function index(Request $request)
    {
        $orders = Order::query()
            // 使用 with 方法预加载,避免N + 1问题
            ->with(['items.product', 'items.productSku'])
            ->where('user_id', $request->user()->id)
            ->orderBy('created_at', 'desc')
            ->paginate();

        return view('orders.index', ['orders' => $orders]);
    }

    // 利用 Laravel 的自动解析功能注入 CartService 类
    public function store(OrderRequest $request, CartService $cartService)
    {
        $user = $request->user();

        // 开启一个数据库事务
        $order = \DB::transaction(function() use ($user, $request){
            $address = UserAddress::find($request->input('address_id'));

            // 更新此地址的最后使用时间
            $address->update(['last_used_at' => Carbon::now()]);

            // 建立一个订单
            $order   = new Order([
                'address'      => [ // 将地址信息放入订单中
                    'address'       => $address->full_address,
                    'zip'           => $address->zip,
                    'contact_name'  => $address->contact_name,
                    'contact_phone' => $address->contact_phone,
                ],
                'remark'       => $request->input('remark'),
                'total_amount' => 0,
            ]);

            // 订单关联到当前用户
            $order->user()->associate($user);

            // 写入数据库
            $order->save();

            $totalAmount = 0;
            $items       = $request->input('items');
            // 遍历用户提交的 SKU
            foreach ($items as $data) {
                $sku  = ProductSku::find($data['sku_id']);
                // 建立一个 OrderItem 并直接与当前订单关联
                $item = $order->items()->make([
                    'amount' => $data['amount'],
                    'price'  => $sku->price,
                ]);
                $item->product()->associate($sku->product_id);
                $item->productSku()->associate($sku);
                $item->save();
                $totalAmount += $sku->price * $data['amount'];

                // 减库存
                if ($sku->decreaseStock($data['amount']) <= 0) {
                    throw new InvalidRequestException('该商品库存不足');
                }
            }

            // 更新订单总金额
            $order->update(['total_amount' => $totalAmount]);

            // 将下单的商品从购物车中移除
            $skuIds = collect($request->input('items'))->pluck('sku_id');
            // $user->cartItems()->whereIn('product_sku_id', $skuIds)->delete();
            $cartService->remove($skuIds);

            return $order;

        });

        $this->dispatch(new CloseOrder($order, config('app.order_ttl')));

        return $order;
    }
}

封装服务类

首先建立 OrderService 类:

$ touch app/Services/OrderService.php

app/Services/OrderService.php

<?php

namespace App\Services;

use App\Models\User;
use App\Models\UserAddress;
use App\Models\Order;
use App\Models\ProductSku;
use App\Exceptions\InvalidRequestException;
use App\Jobs\CloseOrder;
use Carbon\Carbon;

class OrderService
{
    public function store(User $user, UserAddress $address, $remark, $items)
    {
        // 开启一个数据库事务
        $order = \DB::transaction(function () use ($user, $address, $remark, $items) {
            // 更新此地址的最后使用时间
            $address->update(['last_used_at' => Carbon::now()]);
            // 建立一个订单
            $order   = new Order([
                'address'      => [ // 将地址信息放入订单中
                    'address'       => $address->full_address,
                    'zip'           => $address->zip,
                    'contact_name'  => $address->contact_name,
                    'contact_phone' => $address->contact_phone,
                ],
                'remark'       => $remark,
                'total_amount' => 0,
            ]);
            // 订单关联到当前用户
            $order->user()->associate($user);
            // 写入数据库
            $order->save();

            $totalAmount = 0;
            // 遍历用户提交的 SKU
            foreach ($items as $data) {
                $sku  = ProductSku::find($data['sku_id']);
                // 建立一个 OrderItem 并直接与当前订单关联
                $item = $order->items()->make([
                    'amount' => $data['amount'],
                    'price'  => $sku->price,
                ]);
                $item->product()->associate($sku->product_id);
                $item->productSku()->associate($sku);
                $item->save();
                $totalAmount += $sku->price * $data['amount'];
                if ($sku->decreaseStock($data['amount']) <= 0) {
                    throw new InvalidRequestException('该商品库存不足');
                }
            }
            // 更新订单总金额
            $order->update(['total_amount' => $totalAmount]);

            // 将下单的商品从购物车中移除
            $skuIds = collect($items)->pluck('sku_id')->all();
            app(CartService::class)->remove($skuIds);

            return $order;
        });

        // 这里咱们直接使用 dispatch 函数
        dispatch(new CloseOrder($order, config('app.order_ttl')));

        return $order;
    }
}

这里大多数的代码都是从 OrdersController 中直接复制过来的,只有些许的变化须要注意:

  • 一、$user、$address 变量改成从参数获取。咱们在封装功能的时候有一点必定要注意$request 不能够出如今控制器和中间件之外的地方,根据【职责单一原则】,获取数据这个任务应该由控制器来完成,封装的类只须要专一于业务逻辑的实现
  • 二、CartService 的调用方式改成了经过 app() 函数建立,由于这个 store() 方法是咱们手动调用的,没法经过 Laravel 容器的自动解析来注入。在咱们代码里调用封装的库时必定 不能够 使用 new 关键字来初始化,而是应该经过 Laravel 的容器来初始化,由于在以后的开发过程当中 CartService 类的构造函数可能会发生变化,好比注入了其余的类,若是咱们使用 new 来初始化的话,就须要在每一个调用此类的地方进行修改;而使用 app() 或者自动解析注入等方式 Laravel 则会自动帮咱们处理掉这些依赖。
  • 三、以前在控制器中能够经过 $this->dispatch() 方法来触发任务类,但在咱们的封装的类中并无这个方法,所以关闭订单的任务类改成 dispatch() 辅助函数来触发。

修改后的控制器

app/Http/Controllers/OrdersController.php

<?php

namespace App\Http\Controllers;

use App\Http\Requests\OrderRequest;
use App\Models\UserAddress;
use App\Models\Order;
use Illuminate\Http\Request;
use App\Services\OrderService;

class OrdersController extends Controller
{
    .
    .
    .
    public function store(OrderRequest $request, OrderService $orderService)
    {
        $user    = $request->user();
        $address = UserAddress::find($request->input('address_id'));

        return $orderService->store($user, $address, $request->input('remark'), $request->input('items'));
    }
}

三、关于 Service 模式

Service 模式将 PHP 的商业逻辑写在对应责任的 Service 类里,解決 Controller 臃肿的问题。而且符合 SOLID 的单一责任原则,购物车的逻辑由 CartService 负责,而不是 CartController ,控制器是调度中心,编码逻辑更加清晰。后面若是咱们有 API 或者其余会使用到购物车功能的需求,也能够直接使用 CartService ,代码可复用性大大增长。再加上 Service 能够利用 Laravel 提供的依赖注入机制,大大提升了 Service 部分代码的可测试性,程序的健壮性越佳。

8、容器

容器是现代 PHP 开发的一个重要概念,Laravel 就是在容器的基础上构建的。咱们将支付操做类实例注入到容器中,在之后的代码里就能够直接经过 app('alipay') 来取得对应的实例,而不须要每次都从新建立。

在这个示例中,咱们引入第三方支付库yansongda/pay,而后使用容器能够直接调用实例代码。

一、引入支付库

yansongda/pay 这个库封装了支付宝和微信支付的接口,经过这个库咱们就不须要去关注不一样支付平台的接口差别,使用相同的方法、参数来完成支付功能,节省开发时间。

首先经过 composer 引入这个包:

$ composer require yansongda/pay

配置参数:
建立一个新的配置文件来保存支付所需的参数:
config/pay.php

<?php

return [
    'alipay' => [
        'app_id'         => '',
        'ali_public_key' => '',
        'private_key'    => '',
        'log'            => [
            'file' => storage_path('logs/alipay.log'),
        ],
    ],

    'wechat' => [
        'app_id'      => '',
        'mch_id'      => '',
        'key'         => '',
        'cert_client' => '',
        'cert_key'    => '',
        'log'         => [
            'file' => storage_path('logs/wechat_pay.log'),
        ],
    ],
];

二、容器建立

咱们一般在 AppServiceProviderregister() 方法中往容器中注入实例:

app/Providers/AppServiceProvider.php

<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use Monolog\Logger;
use Yansongda\Pay\Pay;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot()
    {
        //
    }

    /**
     * Register any application services.
     *
     * @return void
     */
    public function register()
    {
        // 往服务容器中注入一个名为 alipay 的单例对象
        $this->app->singleton('alipay', function () {
            $config = config('pay.alipay');

            // $config['notify_url'] = route('payment.alipay.notify');
            $config['notify_url'] = 'http://requestbin.leo108.com/1nj6jt11';
            $config['return_url'] = route('payment.alipay.return');

            // 判断当前项目运行环境是否为线上环境
            if (app()->environment() !== 'production') {
                $config['mode']         = 'dev';
                $config['log']['level'] = Logger::DEBUG;
            } else {
                $config['log']['level'] = Logger::WARNING;
            }
            // 调用 Yansongda\Pay 来建立一个支付宝支付对象
            return Pay::alipay($config);
        });

        $this->app->singleton('wechat_pay', function () {
            $config = config('pay.wechat');
            if (app()->environment() !== 'production') {
                $config['log']['level'] = Logger::DEBUG;
            } else {
                $config['log']['level'] = Logger::WARNING;
            }
            // 调用 Yansongda\Pay 来建立一个微信支付对象
            return Pay::wechat($config);
        });
    }
}

代码解析:

  • $this->app->singleton() 往服务容器中注入一个单例对象,第一次从容器中取对象时会调用回调函数来生成对应的对象并保存到容器中,以后再去取的时候直接将容器中的对象返回。
  • app()->environment() 获取当前运行的环境,线上环境会返回 production。对于支付宝,若是项目运行环境不是线上环境,则启用开发模式,而且将日志级别设置为 DEBUG。因为微信支付没有开发模式,因此仅仅将日志级别设置为 DEBUG。

三、测试

接下来咱们来测试一下刚刚注入到容器中的实例,进入 tinker:

> php artisan tinker

而后分别输入 app('alipay')app('wechat_pay')

clipboard.png

能够看到已经OK了。

9、事件与监听器

Laravel 的事件提供了一个简单的观察者实现,可以订阅和监听应用中发生的各类事件。事件类保存在 app/Events 目录中,而这些事件的的监听器则被保存在 app/Listeners 目录下。这些目录只有当你使用 Artisan 命令来生成事件和监听器时才会被自动建立

事件机制是一种很好的应用解耦方式,由于一个事件能够拥有多个互不依赖的监听器。
好比咱们的订单系统,支付以后要给订单中的商品增长销量,好比咱们要发邮件给用户告知订单支付成功。

商品增长销量和发送邮件并不会影响到订单的支付状态,即便这两个操做失败了也不影响咱们后续的业务流程,对于此类需求咱们一般使用异步事件来解决。

一、建立支付成功事件

php artisan make:event OrderPaid

app/Events/OrderPaid.php

<?php

namespace App\Events;

use Illuminate\Broadcasting\Channel;
use Illuminate\Queue\SerializesModels;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use App\Models\Order;

class OrderPaid
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

    protected $order;

    public function __construct(Order $order)
    {
        $this->order = $order;
    }

    public function getOrder()
    {
        return $this->order;
    }

    /**
     * Get the channels the event should broadcast on.
     *
     * @return \Illuminate\Broadcasting\Channel|array
     */
    public function broadcastOn()
    {
        return new PrivateChannel('channel-name');
    }
}
事件自己不须要有逻辑,只须要包含相关的信息便可,在咱们这个场景里就只须要一个订单对象

接下来咱们在支付成功的服务器端回调里触发这个事件:
app/Http/Controllers/PaymentController.php

use App\Events\OrderPaid;
.
.
.
    public function alipayNotify()
    {
        .
        .
        .
        $this->afterPaid($order);

        return app('alipay')->success();
    }

    protected function afterPaid(Order $order)
    {
        event(new OrderPaid($order));
    }

二、建立监听器

咱们但愿订单支付以后对应的商品销量会对应地增长,因此建立一个更新商品销量的监听器:

> php artisan make:listener UpdateProductSoldCount --event=OrderPaid

app/Listeners/UpdateProductSoldCount.php

<?php

namespace App\Listeners;

use App\Events\OrderPaid;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use App\Models\OrderItem;

//  implements ShouldQueue 表明此监听器是异步执行的
class UpdateProductSoldCount implements ShouldQueue
{
    /**
     * Create the event listener.
     *
     * @return void
     */
    public function __construct()
    {
        //
    }

    /**
     * Handle the event.
     * Laravel 会默认执行监听器的 handle 方法,触发的事件会做为 handle 方法的参数
     * @param  OrderPaid  $event
     * @return void
     */
    public function handle(OrderPaid $event)
    {
        // 从事件对象中取出对应的订单
        $order = $event->getOrder();

        // 循环遍历订单的商品
        foreach($order->items as $item)
        {
            $product = $item->product;

            // 计算对应商品的销量
            $soldCount = OrderItem::query()
                ->where('product_id', $product->id)
                ->whereHas('order', function ($query) {
                    $query->whereNotNull('paid_at');  // 关联的订单状态是已支付
                })->sum('amount');

            // 更新商品销量
            $product->update([
                'sold_count' => $soldCount,
            ]);
        }
    }
}

三、关联事件和监听器

别忘了在 EventServiceProvider 中将事件和监听器关联起来:

app/Providers/EventServiceProvider.php

<?php

namespace App\Providers;

use App\Listeners\RegisteredListener;
use Illuminate\Auth\Events\Registered;
use Illuminate\Support\Facades\Event;

use App\Events\OrderPaid;
use App\Listeners\UpdateProductSoldCount;
use App\Listeners\SendOrderPaidMail;

use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;

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

        // 监听器建立完成以后还须要在 EventServiceProvider 中将事件和监听器关联起来才能生效
        // @url https://laravel-china.org/courses/laravel-shop/1584/verification-mailbox-below
        Registered::class => [
            RegisteredListener::class,
        ],

        OrderPaid::class => [
            UpdateProductSoldCount::class,
            SendOrderPaidMail::class,
        ],
    ];

    /**
     * Register any events for your application.
     *
     * @return void
     */
    public function boot()
    {
        parent::boot();

        //
    }
}

四、测试

因为咱们定义的事件监听器都是异步的,所以在测试以前须要先启动队列处理器:

> php artisan queue:work

从数据库中找到一条已经支付成功的订单并记录下其 ID:

clipboard.png

而后在终端里进入 tinker:

php artisan tinker

在 tinker 中触发订单支付成功的事件,事件对应的订单就是咱们刚刚在数据库中找的那一条:

>>> event(new App\Events\OrderPaid(App\Models\Order::find(16)))

clipboard.png

这个时候看到启动队列处理的窗口有了输出:

clipboard.png

能够看到更新库存的事件监听器已经在队列中执行了。

10、MySQL命令导出数据

由于这是一个一次性的工做,没有必要专门写代码来处理导入和导出,因此咱们选择直接用 mysqldump 这个命令行程序来导出数据库中的数据,从成本上来讲比较合适:

mysqldump -t laravel-shop admin_menu admin_permissions admin_role_menu admin_role_permissions admin_role_users admin_roles admin_user_permissions admin_users > database/admin.sql

命令解析:

  • -t 选项表明不导出数据表结构,这些表的结构咱们会经过 Laravel 的 migration 迁移文件来建立;
  • laravel-shop 表明咱们要导出的数据库名称,后面则是要导出的表列表;
  • database/admin.sql 把导出的内容保存到 database/admin.sql 文件中。
在 Homestead 环境中咱们执行 Mysql 相关的命令都不须要帐号密码,由于 Homestead 都已经帮咱们配置好了。在线上执行 Mysql 命令时则须要在命令行里经过 -u 和 -p 参数指明帐号密码,如: mysqldump -uroot -p123456 laravel-shop > database/admin.sql

clipboard.png

相关文章
相关标签/搜索