再读 Laravel 5.5 文档

Laravel 学习交流 QQ 群:375462817

laravel 先走控制器,再走中间件。
开发 api,laravel 会在认证时候就存储 auth()->user() 到内存中,下次再取直接从内存中拿。

本文档前言

Laravel 文档写的很好,只是新手看起来会有点吃力,需要结合经验和网上的文章,多读、细读才能更好的理解。Again,多读、细读官方文档。本文类似于一个大纲,欲知其中详情,且去细读官方文档:Laravel 5.5 docs

#########################################################################################
#########################################################################################
#########################################################################################
#########################################################################################
#########################################################################################
#########################################################################################

第一章:前言(一、版本控制方案。二、Laravel 5.5 新特性。三、贡献指南。四、api 文档。)

#########################################################################################
#########################################################################################
#########################################################################################
#########################################################################################
#########################################################################################
#########################################################################################

一、版本控制方案

  1. 安装版本包时应当指定主版本号和次版本号,如:5.5.*
  2. Laravel 5.5 是 LTS 版本,提供两年的 bug 修复和三年的安全修复。非 LTS 版本,只提供六个月的 bug 修复和一年的安全修复。

二、Laravel 5.5 的新特性(简单介绍)

Horizon ( redis 队列配置仪表盘)、包自动发现、api 资源、控制台命令自动注册、队列任务链、队列任务速率控制、基于时间的任务尝试、自定义的验证规则、可信任的代理集成、按需通知、可渲染的邮件、自定义可报告可渲染的异常、请求对象中的认证方法、一致的异常处理、缓存锁、读写数据库配置 sticky 选项。

三、贡献指南

本节主要指示用户在提出增加框架新特性或者提交框架使用反馈时,需要做些什么、怎么做。

四、api 文档

官方 api 文档地址:https://laravel.com/api/5.5/


#########################################################################################
#########################################################################################
#########################################################################################
#########################################################################################
#########################################################################################
#########################################################################################

第二章:开始(一、安装。二、配置。三、维护模式。四、目录结构。五、homestead。六、部署。)

#########################################################################################
#########################################################################################
#########################################################################################
#########################################################################################
#########################################################################################
#########################################################################################

一、安装

两种安装方式

  1. composer global require "laravel/installer"
    laravel new blog // 无法指定版本
  2. composer create-project --prefer-dist laravel/laravel blog "5.5.*"

安装须知

  1. 根目录是public
  2. 必须设置 web 服务器可读写storagebootstrap/cache目录及其子目录
  3. 在 web 服务器配置中设置优雅链接 (即去除index.php

二、配置

环境配置

  1. .env 文件内的变量会被系统级别或服务器级别的变量覆盖。
  2. .env 文件内的变量通过env()函数获取,config目录下的变量通过config()函数获取。
  3. 在运行PHPUnit测试时或执行以--env=testing为选项Artisan命令时,.env.testing会覆盖 .env 文件中的值。

操作配置、缓存配置

$value = config('app.timezone');                           // 获取
config(['app.timezone' => 'America/Chicago']);             // 设置
php artisan config:cache                 // 缓存(执行该命令后 env 函数将会失效)
php artisan config:clear                 // 清除 

三、维护模式

维护模式 === 503 === Service Unavailable

php artisan down                                           
php artisan down --message="Upgrading Database" --retry=60 
// 优先加载:resources/views/errors/503.blade.php

# 尝试第二条命令的时候,你会发现并不能生效,一开始我也以为是个 bug,
# 为此我还在 github 上 pull request
# 官方解释说你要自建模板 resources/views/errors/503.blade.php 去实现任何你想实现的功能。
# 我觉得这边做的不是很好,既然有这个命令选项,就应该实现对应的功能,
# 否则新手根据文档学习到的时候,会困惑为什么该指令的参数不能生效。

当应用程序处于维护模式时,不会处理队列任务。退出维护模式后会继续处理。

四、目录结构

根目录下的各个目录

  1. app:应用程序核心目录,几乎项目所有的类都在这里。
  2. bootstrap:包含框架启动文件 app.php,和启动时为了优化性能而生成的文件。
  3. config:包含所有配置文件。最好是读一遍这些文件,了解你可以轻松配置哪些内容。
  4. database:包含数据库填充、迁移、模型工厂文件。可以用作 SQLite 数据库存放目录。
  5. public:静态资源目录,并包含了首页文件 index.php
  6. resource:包含了未编译的源文件(模板、语言、资源)。
  7. routes:包含了所有的路由定义。
  8. storage:包含了编译好的模板文件,session 文件,缓存文件,日志等文件。
  9. tests:包含了自动测试文件。运行测试命令php vendor/bin/phpunit
  10. vendorcomposer 依赖目录。

app目录下的各个目录

app 目录下的很多目录是命令生成的。查看生成命令:php artisan make:list

  1. Console:包含自定义的命令和用来注册命令、定义计划任务的内核文件。
  2. Events:事件目录。
  3. Exceptions:异常和异常处理目录。
  4. Http:包含了控制器、中间件和表单请求。几乎所有请求的处理逻辑都被放在这里。
  5. Jobs:储存队列任务。
  6. Listeners:存储事件的监听。
  7. Mail:存储邮件类目录。
  8. Notifications:存放通知类。laravel 内置了很多驱动: email, Slack, SMS, database。
  9. Policies:授权策略类目录。
  10. Providers:包含了所有服务提供者。服务提供者通过在服务容器上绑定服务、注册事件或者执行其他任务来启动你的应用。
  11. Rules:储存自定义验证规则对象。

routes目录下的各个目录

  1. web.php内的路由将应用 web 中间件组(功能:session 状态,CSRF 保护,cookie 加密等)。
  2. api.php内的路由将应用 api 中间件组(功能:访问速率控制等)。所有的请求将通过令牌认证,无 session 状态。
  3. consoles.php定义了所有基于控制台命令的闭包。
  4. channels.php注册了所有的事件广播频道。

五、Homestead

本节介绍了 homestead 是什么,怎么用。

介绍

Homestead 是 Laravel 官方提供的 vagrant box。
vagrant 一个用来给虚拟机提供物品(box)的容器,放在容器(vagrant)里的东西被称作 box 。box 一般就是一个操作系统的镜像。

Homestead 提供了 ubantu、git、php、nginx、apache、mysql、mariadb、sqlite3、postgreSQL、composer、node(With Yarn, Bower, Grunt, and Gulp)、redis、memcached、beanstalkd、mailhog、elasticsearch、ngrok。

windows 需要在 BIOS 中开启硬件虚拟功能 (VT-x)。如果在 UEFI 上使用 Hyper-V,为了使用 VT-x 需要禁用 Hyper-V。

安装

  1. 安装虚拟机和 vagrant。
    (建议使用 virtualbox,vmware 收费并需要购买插件,parallels 也需要插件,Hyper-V 有局限性)
    vagrant 下载页
    virtualbox 下载页
  2. 往 vagrant 里面放 laravel/homestead box。
    vagrant box add laravel/homestead

国内墙的厉害,需要下载下来再进行本地安装:
https://vagrantcloud.com/laravel/boxes/homestead/versions/x.x.x/providers/virtualbox.box
在上面链接中找到想要下载的版本,然后替换掉 x.x.x ,复制到迅雷中进行下载。
重命名为 larabox
vagrant box add laravel/homestead ./larabox

  1. 克隆仓库 laravel/homestead。(用来初始化 vagrant,配置 homestead box 的程序。)
    a. git clone https://github.com/laravel/homestead.git ~/Homestead
    b. cd ~/Homestead
    c. git checkout v7.1.2(切换到稳定版本
    d. bash init.sh或者init.bat(生成 Homestead.yaml 文件)

配置(配置文件 Homestead.yaml)

指定虚拟机

可以是 virtualbox, vmware_fusion, vmware_workstation, parallels 或者 hyperv
provider: virtualbox

共享文件夹

folders属性用于配置本地和homestead环境的同步文件夹。
由于多站点或项目有太多文件而导致性能变差时候,可以设置不同的文件夹来同步不同的项目。
NFS也可以用来优化性能,windows 不支持。使用NFS时,安装vagrant-bindfs插件可维护正确的文件和文件夹权限。
利用 options 键可以设置其他选项。

nginx 站点

homestead使用sites属性让你轻松配置nginx域名。使用vagrant reload --provision更新虚拟机中的nginx配置。
配置好域名,记得在本地 hosts 文件添加解析记录。例如:192.168.10.10 homestead.test

启动和删除 box
# 修改 homestead.rb 
# settings[“version”] ||= “>= 0”  
# 找到 composer self-update 删除它 
vagrant up
vagrant destroy --force
为每个项目安装 homestead

这样你就可以在其他项目目录内,通过 vagrant up 来进行该项目的工作。

composer require laravel/homestead --dev
// make 命令自动生成 Vagrantfile 和 Homestead.yaml
php vendor/bin/homestead make    // linux & mac
vendor\\bin\\homestead make         // windows
安装 mariadb
box: laravel/homestead
ip: "192.168.10.10"
memory: 2048
cpus: 4
provider: virtualbox
mariadb: true
安装 elasticsearch

需要指定版本,默认安装将创建一个名为 'homestead' 的集群,确保你的虚拟机内存是elasticsearch的两倍。

box: laravel/homestead
ip: "192.168.10.10"
memory: 4096
cpus: 4
provider: virtualbox
elasticsearch: 6
bash命令别名
alias ..='cd ..'      // 随后记得 vagrant reload --provision

日常用法

全局访问 homestead

mac 或 linux

function homestead() {
    ( cd ~/Homestead && vagrant $* )
}

windows

@echo off

set cwd=%cd%
set homesteadVagrant=C:\Homestead

cd /d %homesteadVagrant% && vagrant %*
cd /d %cwd%

set cwd=
set homesteadVagrant=

替换C:\Homestead,再将脚本加入环境变量即可。

设置完成后可以在任何地方使用 homestead uphomestead ssh

连接数据库

mysql 127.0.0.1 33060
postgreSQL 127.0.0.1 54320
在虚拟机里面连接数据库的端口还是默认值 33065432
账号密码:homestead / secret

添加多个网站
sites:
    - map: homestead.test
      to: /home/vagrant/code/Laravel/public
    - map: another.test
      to: /home/vagrant/code/another/public

192.168.10.10  homestead.test
192.168.10.10  another.test
添加 nginx 其他配置
sites:
    - map: homestead.test
      to: /home/vagrant/code/Laravel/public
      params:
          - key: FOO
            value: BAR
配置时间任务

如果需要开启 artisan 命令schedule:run一样的效果,配置如下
schedule:run命令的原理是每分钟都会去检查 App\Console\Kernel 类中是否有任务需要执行,有则执行。)

sites:
    - map: homestead.test
      to: /home/vagrant/code/Laravel/public
      schedule: true
配置 Mailhog(.env)

Mailhog 用来捕获你的邮件,并检查它。你的邮件并不会真的发出去。

MAIL_DRIVER=smtp
MAIL_HOST=localhost
MAIL_PORT=1025
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
端口配置

默认:

SSH: 2222 → Forwards To 22
ngrok UI: 4040 → Forwards To 4040
HTTP: 8000 → Forwards To 80
HTTPS: 44300 → Forwards To 443
MySQL: 33060 → Forwards To 3306
PostgreSQL: 54320 → Forwards To 5432
Mailhog: 8025 → Forwards To 8025

自定义:

ports:
    - send: 50000
      to: 5000
    - send: 7777
      to: 777
      protocol: udp
分享你的环境

vagrant share 但是如果Homestead.yaml配置了多个站点,此命令不再支持。
解决方案,使用 homestead 内置命令:

vagrant ssh
share homestead.test

vagrant 天生就不安全,当你使用 share 命令,就会在互联网中暴露的你虚拟机。

切换 php 版本

只兼容 nginx

切换 web 服务器

apache 和 nginx 可以同时安装,但不可同时运行,原理应该是抢占 80 端口。
运行命令 flip 可以关闭其中一个,开启另一个。

配置网络接口

可以配置任意数量的接口:

networks:
    - type: "private_network"
      ip: "192.168.10.20"

想启用 桥接 接口,请配置 bridge 设置,并将网络类型更改为 public_network

networks:
    - type: "public_network"
      ip: "192.168.10.20"
      bridge: "en1: Wi-Fi (AirPort)"

要启用 DHCP,只需从配置中删除 ip 选项:

networks:
    - type: "public_network"
      bridge: "en1: Wi-Fi (AirPort)"
更新 homestead
vagrant box update
git pull origin master

或者

vagrant box update
composer update      # 确保 composer.json 包含 "laravel/homestead": "^7"

如果 windows 下符号链接不生效,添加以下代码块到 Vagrantfile

config.vm.provider "virtualbox" do |v|
    v.customize ["setextradata", :id, "VBoxInternal2/SharedFoldersEnableSymlinksCreate/v-root", "1"]
end

六、部署

本节介绍了几个部署要点。

nginx 部署

server {
    listen 80;
    server_name example.com;
    root /example.com/public;

    add_header X-Frame-Options "SAMEORIGIN";
    add_header X-XSS-Protection "1; mode=block";
    add_header X-Content-Type-Options "nosniff";

    index index.html index.htm index.php;

    charset utf-8;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location = /favicon.ico { access_log off; log_not_found off; }
    location = /robots.txt  { access_log off; log_not_found off; }

    error_page 404 /index.php;

    location ~ \.php$ {
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_pass unix:/var/run/php/php7.1-fpm.sock;
        fastcgi_index index.php;
        include fastcgi_params;
    }

    location ~ /\.(?!well-known).* {
        deny all;
    }
}

部署优化

composer install --optimize-autoloader
# 官方不建议使用了 php artisan optimize --force          // 对应的删除 php artisan clear-compiled
php artisan config:cache
php artisan route:cache
# 开启 OpCache
#########################################################################################
#########################################################################################
#########################################################################################
#########################################################################################
#########################################################################################
#########################################################################################

第三章:架构(一、生命周期。二、服务容器。三、服务提供者。四、facades。五、contracts。)

#########################################################################################
#########################################################################################
#########################################################################################
#########################################################################################
#########################################################################################
#########################################################################################

一、请求生命周期

本节主要概括了框架运行的生命周期。

  1. 所有请求必定首先通过 public/index.php
  2. 在上述这个文件中首先加载 composer 自动加载文件,然后从 bootstrap/app.php 实例化一个服务容器(服务容器,顾名思义是一个容器,可以比作是一个药箱,药箱当然要放药了,药就是所谓的服务提供者。在启动时候当然不能把框架里的所有的药都加载进来,只能加载基础的药。所以这一步还加载了一些基础的服务提供者。)。
  3. 接下来,框架会根据请求类型传送请求至 app/Http/Kernel.php 或者 app/Console/Kernel.php
  4. app/Http/Kernel.php扩展了Illuminate\Foundation\Http\Kernel类,父类强制在处理请求前应该做哪些操作,操作内容都放到了 bootstrappers数组里面(配置错误处理、配置记录日志、检测应用环境、注册和启动服务提供者等)。子类在数组middleware中规定了请求在被处理前必须经过的一些处理(读写session、判断是否处于维护模式、验证 csrf 令牌等)。
  5. 实例化Http Kernel,处理请求,返回响应内容。请求将作为参数传入handle方法,返回值就是响应内容。

二、服务容器

本节主要讲了服务容器中的绑定,解析,解析事件(类似于在药瓶中放药,拿药,拿药时会发生什么)。

  1. 如果不依赖任何接口,则不需要将类绑定到容器中。
  2. 无需指示容器如何构建这些对象,因为它可以使用反射自动解析。

绑定

基础绑定

$this->app->bind('HelpSpot\API', function ($app) {
    return new HelpSpot\API($app->make('HttpClient'));
});  
// Note that we receive the container itself as an argument to the resolver. We can then use the container to resolve sub-dependencies of the object we are building.
// 然而我并不知道上面说的什么鸡脖子东西。

绑定单例

$this->app->singleton('HelpSpot\API', function ($app) {
    return new HelpSpot\API($app->make('HttpClient'));
});   

绑定实例

$api = new HelpSpot\API(new HttpClient);

$this->app->instance('HelpSpot\API', $api);   

绑定实例时给定初始化数据

$this->app->when('App\Http\Controllers\UserController')
          ->needs('$variableName')
          ->give($value);     // 利用上下文给绑定设置初始数据

绑定接口到实例

$this->app->bind(
    'App\Contracts\EventPusher',
    'App\Services\RedisEventPusher'
);     

根据上下文绑定

$this->app->when(PhotoController::class)
          ->needs(Filesystem::class)
          ->give(function () {
              return Storage::disk('local');
          });

$this->app->when(VideoController::class)
          ->needs(Filesystem::class)
          ->give(function () {
              return Storage::disk('s3');
          });

给绑定设置标签

$this->app->bind('SpeedReport', function () {
    //
});

$this->app->bind('MemoryReport', function () {
    //
});

$this->app->tag(['SpeedReport', 'MemoryReport'], 'reports');

$this->app->bind('ReportAggregator', function ($app) {
    return new ReportAggregator($app->tagged('reports'));
});

Service已经被解析,extend方法可以用来修改解析出来的实例$service

$this->app->extend(Service::class, function($service) {
    return new DecoratedService($service);
});

解析

基础解析

$api = $this->app->make('HelpSpot\API');

无法访问 $app 时,这样解析

$api = resolve('HelpSpot\API');

容器无法解决依赖时,通过关联数组注入依赖解析

$api = $this->app->makeWith('HelpSpot\API', ['id' => 1]);      // 解析时候通过关联数组注入依赖

类型提示解析(最常用)

public function __construct(UserRepository $users)

# laravel 实现了 PSR-11 接口,所以就可以用该接口的类型提示解析
# get 方法通过 id 解析,比较局限,建议别用
use Psr\Container\ContainerInterface;
Route::get('/', function (ContainerInterface $container) {
    $service = $container->get('Service');
    //
});

容器事件

容器解析任何对象时调用

$this->app->resolving(function ($object, $app) {
    
});

容器解析HelpSpot\API时调用

$this->app->resolving(HelpSpot\API::class, function ($api, $app) {
    
});

三、服务提供者

加载服务提供者是框架启动的关键步骤之一,他们负责启动不同的组件(数据库、队列、验证、路由等),服务提供者被配置在 config/app.php中的providers数组。首先,他们的register方法会被调用,全部调用结束,才会依次调用他们的boot方法。当所有服务提供者都注册好了,Request将会被路由器分发到路由或控制器,同时运行路由指定的中间件。

制作一个服务提供者

  1. php artisan make:provider RiakServiceProvider
  2. 服务提供者主要由两个方法:registerbootregister只负责绑定一些东西到容器。boot可以使用类型提示解析等来完成任意你想做的事情,这些都归功于容器调用所有服务提供者的register方法之后才去调用boot方法。
  3. config/app.phpproviders数组中注册服务提供者。

制作一个延迟服务提供者

如果只是绑定服务到服务容器,可以选择将该服务提供者设置为延迟

protected $defer = true;
public function provides()
{
    return [Connection::class];
}

四、Facades

原理

其实facades就是各种别名。当你使用某个facade的静态方法时,会触发它的父类的__callStatic方法,该方法会找到注册在容器中的facade原类名,最终调用原类名中的对应方法。

不要在一个类中,用太多的facades。过于臃肿的情况下应该将大类分解成几个小类。

优点

方便测试(辅助函数和 facades 没什么区别,测试方法也是一样的)。

Route::get('/cache', function () {
    return Cache::get('key');     // === return cache('key');
});
public function testBasicExample()
{
    Cache::shouldReceive('get')
         ->with('key')
         ->andReturn('value');

    $this->visit('/cache')
         ->see('value');
}

实时的 facades

原生用法 vs 实时用法

use App\Contracts\Publisher;
...
# 注入 Publisher $publisher
$publisher->publish($this);
use Facades\App\Contracts\Publisher;
...
Publisher::publish($this);

测试实时的 facades

use Facades\App\Contracts\Publisher;
Publisher::shouldReceive('publish')->once()->with($podcast);

facades 列表

Facade Class Service Container Binding
App Illuminate\Foundation\Application app
Artisan Illuminate\Contracts\Console\Kernel artisan
Auth Illuminate\Auth\AuthManager auth
Auth (Instance) Illuminate\Contracts\Auth\Guard auth.driver
Blade Illuminate\View\Compilers\BladeCompiler blade.compiler
Broadcast Illuminate\Contracts\Broadcasting\Factory  
Broadcast (Instance) Illuminate\Contracts\Broadcasting\Broadcaster  
Bus Illuminate\Contracts\Bus\Dispatcher  
Cache Illuminate\Cache\CacheManager cache
Cache (Instance) Illuminate\Cache\Repository cache.store
Config Illuminate\Config\Repository config
Cookie Illuminate\Cookie\CookieJar cookie
Crypt Illuminate\Encryption\Encrypter encrypter
DB Illuminate\Database\DatabaseManager db
DB (Instance) Illuminate\Database\Connection db.connection
Event Illuminate\Events\Dispatcher events
File Illuminate\Filesystem\Filesystem files
Gate Illuminate\Contracts\Auth\Access\Gate  
Hash Illuminate\Contracts\Hashing\Hasher hash
Lang Illuminate\Translation\Translator translator
Log Illuminate\Log\Writer log
Mail Illuminate\Mail\Mailer mailer
Notification Illuminate\Notifications\ChannelManager  
Password Illuminate\Auth\Passwords\PasswordBrokerManager auth.password
Password (Instance) Illuminate\Auth\Passwords\PasswordBroker auth.password.broker
Queue Illuminate\Queue\QueueManager queue
Queue (Instance) Illuminate\Contracts\Queue\Queue queue.connection
Queue (Base Class) Illuminate\Queue\Queue  
Redirect Illuminate\Routing\Redirector redirect
Redis Illuminate\Redis\RedisManager redis
Redis (Instance) Illuminate\Redis\Connections\Connection redis.connection
Request Illuminate\Http\Request request
Response Illuminate\Contracts\Routing\ResponseFactory  
Response (Instance) Illuminate\Http\Response  
Route Illuminate\Routing\Router router
Schema Illuminate\Database\Schema\Builder  
Session Illuminate\Session\SessionManager session
Session (Instance) Illuminate\Session\Store session.store
Storage Illuminate\Filesystem\FilesystemManager filesystem
Storage (Instance) Illuminate\Contracts\Filesystem\Filesystem filesystem.disk
URL Illuminate\Routing\UrlGenerator url
Validator Illuminate\Validation\Factory validator
Validator (Instance) Illuminate\Validation\Validator  
View Illuminate\View\Factory view
View (Instance) Illuminate\View\View  

五、Contracts

FacadesContracts没有什么值得注意的区别,但是当你开发第三方包的时候,最好使用Contracts,这样有利于你编写测试,否则如果使用Facades,因为是第三方包,将不能访问facade测试函数。

使用方法

在构造函数中类型提示注入就行了。

Contracts 列表

Contract References Facade
Illuminate\Contracts\Auth\Access\Authorizable  
Illuminate\Contracts\Auth\Access\Gate Gate
Illuminate\Contracts\Auth\Authenticatable  
Illuminate\Contracts\Auth\CanResetPassword  
Illuminate\Contracts\Auth\Factory Auth
Illuminate\Contracts\Auth\Guard Auth::guard()
Illuminate\Contracts\Auth\PasswordBroker Password::broker()
Illuminate\Contracts\Auth\PasswordBrokerFactory Password
Illuminate\Contracts\Auth\StatefulGuard  
Illuminate\Contracts\Auth\SupportsBasicAuth  
Illuminate\Contracts\Auth\UserProvider  
Illuminate\Contracts\Bus\Dispatcher Bus
Illuminate\Contracts\Bus\QueueingDispatcher Bus::dispatchToQueue()
Illuminate\Contracts\Broadcasting\Factory Broadcast
Illuminate\Contracts\Broadcasting\Broadcaster Broadcast::connection()
Illuminate\Contracts\Broadcasting\ShouldBroadcast  
Illuminate\Contracts\Broadcasting\ShouldBroadcastNow  
Illuminate\Contracts\Cache\Factory Cache
Illuminate\Contracts\Cache\Lock  
Illuminate\Contracts\Cache\LockProvider  
Illuminate\Contracts\Cache\Repository Cache::driver()
Illuminate\Contracts\Cache\Store  
Illuminate\Contracts\Config\Repository Config
Illuminate\Contracts\Console\Application  
Illuminate\Contracts\Console\Kernel Artisan
Illuminate\Contracts\Container\Container App
Illuminate\Contracts\Cookie\Factory Cookie
Illuminate\Contracts\Cookie\QueueingFactory Cookie::queue()
Illuminate\Contracts\Database\ModelIdentifier  
Illuminate\Contracts\Debug\ExceptionHandler  
Illuminate\Contracts\Encryption\Encrypter Crypt
Illuminate\Contracts\Events\Dispatcher Event
Illuminate\Contracts\Filesystem\Cloud Storage::cloud()
Illuminate\Contracts\Filesystem\Factory Storage
Illuminate\Contracts\Filesystem\Filesystem Storage::disk()
Illuminate\Contracts\Foundation\Application App
Illuminate\Contracts\Hashing\Hasher Hash
Illuminate\Contracts\Http\Kernel  
Illuminate\Contracts\Logging\Log Log
Illuminate\Contracts\Mail\MailQueue Mail::queue()
Illuminate\Contracts\Mail\Mailable  
Illuminate\Contracts\Mail\Mailer Mail
Illuminate\Contracts\Notifications\Dispatcher Notification
Illuminate\Contracts\Notifications\Factory Notification
Illuminate\Contracts\Pagination\LengthAwarePaginator  
Illuminate\Contracts\Pagination\Paginator  
Illuminate\Contracts\Pipeline\Hub  
Illuminate\Contracts\Pipeline\Pipeline  
Illuminate\Contracts\Queue\EntityResolver  
Illuminate\Contracts\Queue\Factory Queue
Illuminate\Contracts\Queue\Job  
Illuminate\Contracts\Queue\Monitor Queue
Illuminate\Contracts\Queue\Queue Queue::connection()
Illuminate\Contracts\Queue\QueueableCollection  
Illuminate\Contracts\Queue\QueueableEntity  
Illuminate\Contracts\Queue\ShouldQueue  
Illuminate\Contracts\Redis\Factory Redis
Illuminate\Contracts\Routing\BindingRegistrar Route
Illuminate\Contracts\Routing\Registrar Route
Illuminate\Contracts\Routing\ResponseFactory Response
Illuminate\Contracts\Routing\UrlGenerator URL
Illuminate\Contracts\Routing\UrlRoutable  
Illuminate\Contracts\Session\Session Session::driver()
Illuminate\Contracts\Support\Arrayable  
Illuminate\Contracts\Support\Htmlable  
Illuminate\Contracts\Support\Jsonable  
Illuminate\Contracts\Support\MessageBag  
Illuminate\Contracts\Support\MessageProvider  
Illuminate\Contracts\Support\Renderable  
Illuminate\Contracts\Support\Responsable  
Illuminate\Contracts\Translation\Loader  
Illuminate\Contracts\Translation\Translator Lang
Illuminate\Contracts\Validation\Factory Validator
Illuminate\Contracts\Validation\ImplicitRule  
Illuminate\Contracts\Validation\Rule  
Illuminate\Contracts\Validation\ValidatesWhenResolved  
Illuminate\Contracts\Validation\Validator Validator::make()
Illuminate\Contracts\View\Engine  
Illuminate\Contracts\View\Factory View
Illuminate\Contracts\View\View View::make()
#########################################################################################
#########################################################################################
#########################################################################################
#########################################################################################
#########################################################################################
#########################################################################################

第四章:基础(一、路由。二、中间件。三、CSRF 保护。四、控制器。五、请求。六、响应。七、视图。八、Url 生成。九、Session 。十、验证。十一、错误与日志)

#########################################################################################
#########################################################################################
#########################################################################################
#########################################################################################
#########################################################################################
#########################################################################################

一、路由

web/api.php中定义的路由会自动添加api前缀,如需修改该前缀,可以在RouteServiceProvider修改。
web.php 路由里的POST, PUT, DELETE方法,在提交表单时候必须加上CSRF参数。

两个 api RouterRoute

resource

* GET /test
index()  //  展示列表

* GET /test/create
create()  // 展示用来创建的表单

* POST /test
store()  // 增加资源

* GET /test/{id}
show($id)  // 展示一个资源

* GET /test/{id}/edit
edit($id)  // 展示编辑表单

* PUT /test/{id}
update($id)  // 更新特定资源

* DELETE /test/{id} 
destroy($id)  // 移除资源

基本路由

Route::get($uri, $callback);
Route::post($uri, $callback);
Route::put($uri, $callback);        // 全体更新
Route::patch($uri, $callback);      // 局部更新
Route::delete($uri, $callback);
Route::options($uri, $callback);    // 允许客户端检查性能

Route::any($uri, $callback);        // 任意 method

Route::match(['get', 'post'], '/', function () {
    //
});
# 重定向路由
Route::redirect('/here', '/there', 301);

# 只需要返回一个视图
Route::view('/welcome', 'welcome', ['name' => 'Taylor']);

路由参数

Route::get('posts/{post}/comments/{comment}', function ($postId, $commentId) {
    //
});

Route::get('user/{name?}', function ($name = 'John') {   // 一定要给可选参数设置默认值
    return $name;
});

正则表达式约束


Route::get('user/{id}', function ($id) {
    //
})->where('id', '[0-9]+');

Route::get('user/{id}/{name}', function ($id, $name) {
    //
})->where(['id' => '[0-9]+', 'name' => '[a-z]+']);

全局约束

// RouteServiceProvider
public function boot()
{
    Route::pattern('id', '[0-9]+');

    parent::boot();
}

命名路由

Route::get('user/profile', function () {
    //
})->name('profile');

// 使用
$url = route('profile');
// 生成重定向...
return redirect()->route('profile');
// 在中间件中检查当前路由
public function handle($request, Closure $next)
{
    if ($request->route()->named('profile')) {
        //
    }
    return $next($request);
}

添加中间件

Route::middleware(['first', 'second'])->group(function () {
    Route::get('/', function () {
        // 使用 `first` 和 `second` 中间件
    });

    Route::get('user/profile', function () {
        // 使用 `first` 和 `second` 中间件
    });
});

命名空间

Route::namespace('Admin')->group(function () {
    // 在 "App\Http\Controllers\Admin" 命名空间下的控制器
});

子域名路由

Route::domain('{account}.myapp.com')->group(function () {
    Route::get('user/{id}', function ($account, $id) {
        //
    });
});

路由前缀

Route::prefix('admin')->group(function () {
    Route::get('users', function () {
        // 匹配包含 "/admin/users" 的 URL
    });
});

路由命名前缀

Route::name('admin.')->group(function () {
    Route::get('users', function () {
        // Route assigned name "admin.users"...
    });
});

表单伪造

<input type="hidden" name="_method" value="PUT">
// 或者 {{ method_field('PUT') }}

获取当前路由信息

// Route::get('/', 'TestController@test')->name("mytest");
$route = Route::current(); //  object(Illuminate\Routing\Route)
$name = Route::currentRouteName(); // mytest 
$action = Route::currentRouteAction(); // 控制器中:App\Http\Controllers\TestController@test  路由中:null

隐式绑定

Route::get('api/users/{user}', function (App\User $user) {
    return $user->email;    // 传人的 id
});

# 自定义键名 在模型中修改
# App/User.php
public function getRouteKeyName()
{
    return 'slug';
}

显式绑定

# RouteServiceProvider
public function boot()
{
    parent::boot();

    Route::model('user', App\User::class);
}

Route::get('profile/{user}', function ($user) {
    //
});

自定义解析逻辑

public function boot()
{
    parent::boot();

    Route::bind('user', function ($value) {
        return App\User::where('name', $value)->first() ?? abort(404);
    });
}

二、中间件

php artisan make:middleware TestMiddleware

# 中间件操作发生请求被处理之前
public function handle($request, Closure $next)
{
    // Perform action
    return $next($request);
}

# 中间件操作发生请求被处理之后
public function handle($request, Closure $next)
{
    $response = $next($request);
    // Perform action
    return $response;
}

注册全局中间件,就将完整类名写在app/Http/Kernel.php文件中的$middleware数组中。如果是非全局,部分的,可以放在文件的其他数组中。也可以不注册,在需要使用时,引入直接使用。

使用中间件的两种方式

# 常用方式
Route::get('admin/profile', function () {
    
})->middleware('auth');

# 不用注册的方式
use App\Http\Middleware\CheckAge;
Route::get('admin/profile', function () {

})->middleware(CheckAge::class);

如果想应用中间件组,请注册一个关联数组到app/Http/Kernel.php$middlewareGroups中。这样就可以使用时填写该数组的键值就行了。默认情况下,RouteServiceProvider已将中间件组web应用在你的web.php的路由中。

给 middleware 传参

// role 中间件
public function handle($request, Closure $next, $role)
{
    if (! $request->user()->hasRole($role)) {
        // Redirect...
    }
   return $next($request);
}

Route::put('post/{id}', function ($id) {
    //
})->middleware('role:editor');

terminate方法调用于将响应发送到浏览器之后。

# terminate 会从服务容器解析一个新的中间件实例。
# 如果你希望 handle 和 terminate 是同一实例,请将这个中间件单例绑定到服务容器中。
public function handle($request, Closure $next)
{
    return $next($request);
}

public function terminate($request, $response)
{
    // Store the session data...
}

三、CSRF 保护

VerifyCsrfToken中间件存在于web中间件组中,它实现了 CSRF 保护。而该中间件组被默认应用在web.php文件中的所有路由。

默认情况下,resources/assets/js/bootstrap.js文件会使用Axios HTTP库注册csrf-token meta 标签的值。如果不使用这个库,需要自己去设置。

指定 uri 不去应用 csrf 保护

  1. 不将路由写入route/web.php
  2. VerifyCsrfToken中间件的排除数组中添加你的 uri
protected $except = [
    'stripe/*',
    'http://example.com/foo/bar',
    'http://example.com/foo/*',
];

除了可以在验证 post 时作为表单参数传递 csrf ,还可以设置 meta 标签来进行 csrf 保护(如 ajax )。
设置:<meta name="csrf-token" content="{{ csrf_token() }}">
ajax 获取:'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')

cookie中,XSRF-TOKEN的值可以直接设为X-CSRF-TOKEN请求头的值。AngularAxios等会自动执行上述操作。

四、控制器

定义控制器可以不去继承Controllers,这样你将不能使用middlewarevalidatedispatch等方法。

如果你的控制器只有一个方法,可以这么玩儿:

# Route::get('foo', 'Photos\AdminController@method');
public function __invoke($id)
{
     return view('user.profile', ['user' => User::findOrFail($id)]);
}

控制器的构造函数中的中间件的玩法

$this->middleware('auth');
$this->middleware('log')->only('index');
$this->middleware('subscribed')->except('store');
$this->middleware(function ($request, $next) {
       return $next($request);
});

Resource 控制器

php artisan make:controller PhotoController --resource
php artisan make:controller PhotoController --resource --model=Photo

Route::resource('photos', 'PhotoController');
Route::apiResource('photo', 'PhotoController');    // 没有 create 和 edit

Route::resources([
    'photos' => 'PhotoController',
    'posts' => 'PostController'
]);
Route::resource('photo', 'PhotoController', ['only' => [
    'index', 'show'
]]);
Route::resource('photo', 'PhotoController', ['except' => [
    'create', 'store', 'update', 'destroy'
]]);

Route::resource('photo', 'PhotoController', ['names' => [
    'create' => 'photo.build'
]]);

# 模拟 http 请求方法
{{ method_field('PUT') }}
Verb URI Action Route Name
GET /photos index photos.index
GET /photos/create create photos.create
POST /photos store photos.store
GET /photos/{photo} show photos.show
GET /photos/{photo}/edit edit photos.edit
PUT/PATCH /photos/{photo} update photos.update
DELETE /photos/{photo} destroy photos.destroy

命名 resource 路由的参数

默认情况下,Route::resource 会根据资源名称的「单数」形式创建资源路由的路由参数。你可以在选项数组中传入 parameters 参数来轻松地覆盖每个资源。parameters 数组应该是资源名称和参数名称的关联数组:

Route::resource('user', 'PhotoController', ['parameters' => [
    'photo' => 'photo_in_phone'
]]);
# /user/{photo_in_phone}

重命名动词名 create、edit

# AppServiceProvider
Route::resourceVerbs([
    'create' => 'crear',
    'edit' => 'editar',
]);
  1. 如果想加入其他控制器方法,尽量写在resource控制器路由之前,否则可能会被resource控制器的路由覆盖。
  2. 如果你需要典型的resource操作之外的方法,可以考虑将你的控制器分成两个更小的控制器。

参数必须在依赖注入之后传入

# Route::put('user/{id}', 'UserController@update');
public function update(Request $request, $id)
{
    //
}

五、请求

请求将经过TrimStringsConvertEmptyStringsToNull等中间件,这样可以不用担心标准化的问题。

$request->path()
$request->is('admin/*')
$request->url()
$request->fullUrl()
$request->isMethod('post')
$request->all()
$request->input('name')
$request->input('name', 'Sally')     // 第二个参数为默认值
$request->input('products.0.name')
$request->input('products.*.name')
$request->query('name')
$request->query('name', 'Helen')
$request->query()    // 返回所有 query string 的关联数组
$request->name      // laravel 首先查找请求数据,在查找路由参数
$request->input('user.name')     // 获取 json,请求 contentType: "application/json"  
$request->only(['username', 'password'])    // 返回请求关联数组,但不会返回不存在请求数据
$input = $request->only('username', 'password');   // 同上
$input = $request->except(['credit_card']);
$input = $request->except('credit_card');
$request->has('name')
$request->has(['name', 'email'])    // 同时存在
$request->filled('name')    // 存在并且为非空
$request->flash()     // 将当前输入存进 session  中,以便下次请求可以使用它们
$request->flashOnly(['username', 'email'])
$request->flashExcept('password')
redirect('form')->withInput()    // 等同于:$request->flash(); redirect('form');
redirect('form')->withInput(
    $request->except('password')
);
$request->old('username')   // 取出 flash() 内容,并从 session 中清除
<input type="text" name="username" value="{{ old('username') }}">   // 不存在返回 null
$request->cookie('name')   // === Cookie::get('name')
response('Hello World')->cookie(
    'name', 'value', $minutes
);
response('Hello World')->cookie(
    'name', 'value', $minutes, $path, $domain, $secure, $httpOnly
);
Cookie::queue(Cookie::make('name', 'value', $minutes));
Cookie::queue('name', 'value', $minutes);

$cookie = cookie('name', 'value', $minutes);
response('Hello World')->cookie($cookie);

$request->file('photo')
$request->photo
$request->hasFile('photo')
$request->file('photo')->isValid()
$request->photo->path()
$request->photo->extension()
$path = $request->photo->store('images');
$path = $request->photo->store('images', 's3');
$path = $request->photo->storeAs('images', 'filename.jpg');
$path = $request->photo->storeAs('images', 'filename.jpg', 's3');

六、响应

laravel 会自动将你的 return 数据封装成响应。如果你返回数组,会自动封装成 json。如果返回 Eloquent 集合,也会自动封装成 json。

自定义

return response('Hello World', 200)
               ->header('Content-Type', 'text/plain');
// 或者
return response($content)
            ->withHeaders([
                'Content-Type' => $type,
                'X-Header-One' => 'Header Value',
                'X-Header-Two' => 'Header Value',
            ])

return response($content)
                ->header('Content-Type', $type)
                ->cookie('name', 'value', $minutes);
# ->cookie($name, $value, $minutes, $path, $domain, $secure, $httpOnly)
# Cookie::queue(Cookie::make('name', 'value', $minutes));
# Cookie::queue('name', 'value', $minutes);
# 如果不想某个 cookie 在客户端中加密,
# 请在 App\Http\Middleware\EncryptCookies 的 except 数组中进行配置。

重定向

return redirect('home/dashboard');
return back()->withInput();     // 请确保该路由在`web.php`中
return redirect()->route('login');
return redirect()->route('profile', ['id' => 1]);

return redirect()->route('profile', [$user]);    
// 第二个参数也可以是 $user,路由:profile/{id}。该写法会自动提取 $user 中的 id
# 如果想定制自动提取的字段
public function getRouteKey()
{
    return $this->slug;
} 

return redirect()->action(
    'UserController@profile', ['id' => 1]
);

return redirect()->away('https://www.google.com');

return redirect('dashboard')->with('status', 'Profile updated!');  
// 设置 cookie,获取 {{ session('status') }}

生成其他类型的响应

return response()
            ->view('hello', $data, 200)
            ->header('Content-Type', $type);

return response()->json([
    'name' => 'Abigail',
    'state' => 'CA'
]);     // json 会自动设置响应头 Content-Type => application/json

实现 jsonp 响应

return response()
            ->json(['name' => 'Abigail', 'state' => 'CA'])
            ->withCallback($request->input('callback'));

实现下载

return response()->download($pathToFile);
return response()->download($pathToFile, $name, $headers);
return response()->download($pathToFile)->deleteFileAfterSend(true);

管理文件下载的扩展包 Symfony HttpFoundation,要求下载文件名必须是 ASCII 编码。

展示图片、pdf 等

return response()->file($pathToFile);
return response()->file($pathToFile, $headers);

定制通用响应

// 定制
Response::macro('caps', function ($value) {
    return Response::make(strtoupper($value));
});

// 使用 
return response()->caps('foo');

七、视图

return view('greeting', ['name' => 'James']);
if (View::exists('emails.customer')) 
return view()->first(['custom.admin', 'admin'], $data);  
return View::first(['custom.admin', 'admin'], $data);   // 同上
return view('greeting')->with('name', 'Victoria');   // === ['name' => 'Victoria']
    
public function boot()
{
    View::share('key', 'value');
    // 在所有视图中都可以使用 {{ $key }}
}

视图 composers

// service provider
public function boot()
{
    View::composer(
        'profile', 'App\Http\ViewComposers\ProfileComposer'
    );            // ProfileComposer@compose 在 profile 视图生成前调用

    View::composer('dashboard', function ($view) {
        //
    });
}

class ProfileComposer
{
    protected $users;

    public function __construct(UserRepository $users)
    {
        $this->users = $users;
    }

    public function compose(View $view)
    {
        $view->with('count', $this->users->count());
    }
}
# 多个视图
View::composer(
    ['profile', 'dashboard'],
    'App\Http\ViewComposers\MyViewComposer'
);
# 所有
View::composer('*', function ($view) {
    //
});

视图 creator

View::creator('profile', 'App\Http\ViewCreators\ProfileCreator');

视图 creator 和视图合成器非常相似。唯一不同之处在于:视图构造器在视图实例化之后立即执行,而视图合成器在视图即将渲染时执行。creator 在 composer 之前。

八、url 生成

echo url("/posts/{$post->id}");
// http://example.com/posts/1

echo url()->current();     // === URL::current();
echo url()->full();        // === URL::full();
echo url()->previous();    // === URL::previous();

echo route('post.show', ['post' => 1]);
// http://example.com/post/1

echo route('post.show', ['post' => $post]);      
// $post 是一个Eloquent model,该写法自动提取出主键

$url = action('HomeController@index');
$url = action('UserController@profile', ['id' => 1]);

URL::defaults(['locale' => $request->user()->locale]);  
// 设置默认值 Route::get('/{locale}/posts', function () { ... })->name('post.index');

九、session

驱动设置为 array,是用在测试的时候保证不会持久存储 session。

使用 database 作为驱动

Schema::create('sessions', function ($table) {
    $table->string('id')->unique();
    $table->unsignedInteger('user_id')->nullable();
    $table->string('ip_address', 45)->nullable();
    $table->text('user_agent')->nullable();
    $table->text('payload');
    $table->integer('last_activity');
});

php artisan session:table
php artisan migrate

session 操作

$request->session()->get('key');
$request->session()->get('key', 'default');
$request->session()->get('key', function () {
    return 'default';
});

session('key');
session('key', 'default');
session(['key' => 'value']);

$request->session()->all();
if ($request->session()->has('users'))     // 存在且不为 null
if ($request->session()->exists('users'))     // 可以是 null

$request->session()->put('key', 'value');
session(['key' => 'value']);

$request->session()->push('user.teams', 'developers');
$value = $request->session()->pull('key', 'default');     // 取出并删除

$request->session()->flash('status', 'Task was successful!');    // 短暂存储 session 一次
$request->session()->reflash();     // 保持 session 再存储一次
$request->session()->keep(['username', 'email']);     // 保持特定 session 存储一次

$request->session()->forget('key');
$request->session()->flush();

$request->session()->regenerate();    
// 手动重新生成 session id,LoginController 内置自动重新生成 session id

自定义 session 驱动

# SessionHandlerInterface 必须实现此接口
<?php
namespace App\Extensions;    // 随意放置于哪个目录

class MongoSessionHandler implements \SessionHandlerInterface
{
    public function open($savePath, $sessionName) {}
    public function close() {}
    public function read($sessionId) {}
    public function write($sessionId, $data) {}
    public function destroy($sessionId) {}
    public function gc($lifetime) {}
}


// service provider
public function boot()
{
    Session::extend('mongo', function ($app) {
        // Return implementation of SessionHandlerInterface...
        return new MongoSessionStore;
    });
}

// 在 config/session.php 中配置 mongo

十、验证

基础验证

如果请求验证失败,会返回跳转响应,如果是 ajax ,会返回带有状态码 422 的 json 格式的响应。

$validatedData = $request->validate([
    'title' => 'required|unique:posts|max:255',
    'body' => 'required',
]);

如果希望验证了某个字段失败后,不再验证后面的字段。请在前一个字段验证规则中添加bail

嵌套验证
$request->validate([
    'title' => 'required|unique:posts|max:255',
    'author.name' => 'required',
    'author.description' => 'required',
]);
验证失败

Illuminate\View\Middleware\ShareErrorsFromSession这个中间件在默认应用在web.php的路由中。使得验证失败的错误信息都可以在模板中用$errors获取。

@if ($errors->any())
    <div class="alert alert-danger">
        <ul>
            @foreach ($errors->all() as $error)
                <li>{{ $error }}</li>
            @endforeach
        </ul>
    </div>
@endif

因为全局都会经过TrimStringsConvertEmptyStringsToNull中间件,所以如果要设置某个字段验证时可以为null,请在验证规则中添加nullable

表单请求验证

命令生成
php artisan make:request StoreBlogPost

验证规则

public function rules()
{
    return [
        'title' => 'required|unique:posts|max:255',
        'body' => 'required',
    ];
}

添加 after 钩子

public function withValidator($validator)      // withValidator 在验证之前做一些操作
{
    $validator->after(function ($validator) {
        if ($this->somethingElseIsInvalid()) {
            $validator->errors()->add('field', 'Something is wrong with this field!');
        }
    });
}

授权验证

public function authorize()
{
    $comment = Comment::find($this->route('comment'));

    return $comment && $this->user()->can('update', $comment);
}

使用表单验证

public function store(StoreBlogPost $request)
{
    // The incoming request is valid...
}

定制错误消息

public function messages()
{
    return [
        'title.required' => 'A title is required',
        'body.required'  => 'A message is required',
    ];
}
手动创建验证
$validator = Validator::make($request->all(), [
    'title' => 'required|unique:posts|max:255',
    'body' => 'required',
]);

if ($validator->fails()) {
    return redirect('post/create')
        ->withErrors($validator)       // withErrors 接受 validator 或者 MessageBag 或者 array 作为参数
        ->withInput();
}

自动跳转

Validator::make($request->all(), [
    'title' => 'required|unique:posts|max:255',
    'body' => 'required',
])->validate();

命名错误数组

return redirect('register')
            ->withErrors($validator, 'login');

// {{ $errors->login->first('email') }}

after 钩子

$validator = Validator::make(...);

$validator->after(function ($validator) {
    if ($this->somethingElseIsInvalid()) {
        $validator->errors()->add('field', 'Something is wrong with this field!');
    }
});

if ($validator->fails()) {
    //
}
错误信息
$errors = $validator->errors();
echo $errors->first('email');

foreach ($errors->get('email') as $message) {
    //
}

foreach ($errors->get('attachments.*') as $message) {
    //
}

foreach ($errors->all() as $message) {
    //
}

if ($errors->has('email')) {
    //
}

自定义错误消息

$messages = [
    'required' => 'The :attribute field is required.',
];
$validator = Validator::make($input, $rules, $messages);
$messages = [
    'email.required' => 'We need to know your e-mail address!',
];

可用的验证规则

Accepted
Active URL
After (Date)
After Or Equal (Date)
Alpha
Alpha Dash
Alpha Numeric
Array
Before (Date)
Before Or Equal (Date)
Between
Boolean
Confirmed
Date
Date Equals
Date Format
Different
Digits
Digits Between
Dimensions (Image Files)
Distinct
E-Mail
Exists (Database)
File
Filled
Image (File)
In
In Array
Integer
IP Address
JSON
Max
MIME Types
MIME Type By File Extension
Min
Nullable
Not In
Numeric
Present
Regular Expression
Required
Required If
Required Unless
Required With
Required With All
Required Without
Required Without All
Same
Size
String
Timezone
Unique (Database)
URL

官方详细内容:验证规则

按条件添加验证规则
Validator::make($data, [
    'email' => 'sometimes|required|email',               // sometimes $data 内含有 email 才验证
]);

复杂的验证规则

$v = Validator::make($data, [
    'email' => 'required|email',
    'games' => 'required|numeric',
]);

$v->sometimes('reason', 'required|max:500', function ($input) {
    return $input->games >= 100;
});

$v->sometimes(['reason', 'cost'], 'required', function ($input) {
    return $input->games >= 100;
});
$validator = Validator::make($request->all(), [
    'person.*.email' => 'email|unique:users',
    'person.*.first_name' => 'required_with:person.*.last_name',
]);
自定义验证规则
php artisan make:rule Uppercase
public function passes($attribute, $value)
{
    return strtoupper($value) === $value;
}

public function message()
{
    return 'The :attribute must be uppercase.';
}

直接使用

use App\Rules\Uppercase;

$request->validate([
    'name' => ['required', new Uppercase],
]);

注册再使用

public function boot()
{
    Validator::extend('foo', function ($attribute, $value, $parameters, $validator) {
        return $value == 'foo';
    });
}
Validator::extend('foo', 'FooValidator@validate');

自定义替换占位符
为错误消息定义自定义替换占位符

# "accepted" => "The :attribute must be accepted.",
public function boot()
{
    Validator::extend(...);

    Validator::replacer('foo', function ($message, $attribute, $rule, $parameters) {
        return str_replace(...);
    });
}

隐藏验证

默认情况下,如果字段值为 null,并且未设置规则含有 required,那么验证规则都将不再使用,也就是结果会被通过。
所以如果是必须的字段,那么一定要设置 required。通过下面,使得字段一定是 required 。

Validator::extendImplicit('foo', function ($attribute, $value, $parameters, $validator) {  
    return $value == 'foo';
});

“隐式”扩展只意味着该属性是必需的。 它是否实际上使缺失或空属性失效取决于您。(TODO:不是太明白,也用不太到,暂时不管)

十一、错误与日志

  1. 生产环境下,一定要设置APP_DEBUGfalse
  2. Laravel 提供四种记录日志方式 "single", "daily", "syslog", "errorlog",默认 single。
  3. 设置 daily 等级的存储文件数,'log_max_files' => 30
  4. 设置记录等级'log_level' => env('APP_LOG_LEVEL', 'error')。debug, info, notice, warning, error, critical, alert, emergency。

自定义 monolog 配置
bootstrap/app.php中,生成$app之前:

$app->configureMonologUsing(function ($monolog) {
    $monolog->pushHandler(...);
});
return $app;

自定义日志频道名称

'log_channel' => env('APP_LOG_CHANNEL', 'my-app-name'),

Handler.php中的report方法

public function report(Exception $exception)
{
    if ($exception instanceof CustomException) {
        //
    }

    return parent::report($exception);
}

// 使用
public function isValid($value)
{
    try {
        // Validate the value...
    } catch (Exception $e) {
        report($e);

        return false;
    }
}

dontReport不报告错误

protected $dontReport = [
    \Illuminate\Auth\AuthenticationException::class,
    \Illuminate\Auth\Access\AuthorizationException::class,
    \Symfony\Component\HttpKernel\Exception\HttpException::class,
    \Illuminate\Database\Eloquent\ModelNotFoundException::class,
    \Illuminate\Validation\ValidationException::class,
];

Handler.php中的render方法

public function render($request, Exception $exception)
{
    if ($exception instanceof CustomException) {
        return response()->view('errors.custom', [], 500);
    }

    return parent::render($request, $exception);
}

自定义异常

php artisan make:exception TalkException

简单粗暴的 abort

abort(404);

abort(403, 'Unauthorized action.');     // <h2>{{ $exception->getMessage() }}</h2>

Log facades

public function showProfile($id)
{
    Log::info('Showing user profile for user: '.$id);

    return view('user.profile', ['user' => User::findOrFail($id)]);
}

Log::emergency($message);
Log::alert($message);
Log::critical($message);
Log::error($message);
Log::warning($message);
Log::notice($message);
Log::info($message);
Log::debug($message);

Log::info('User failed to login.', ['id' => $user->id]);

获取 monolog 实例,以便使用它的方法

$monolog = Log::getMonolog();
#########################################################################################
#########################################################################################
#########################################################################################
#########################################################################################
#########################################################################################
#########################################################################################

第五章:前端开发(一、Blade。二、本地化。三、前端脚手架。四、Laravel mix 。)

#########################################################################################
#########################################################################################
#########################################################################################
#########################################################################################
#########################################################################################
#########################################################################################

一、Blade

section 有内容则生效

@hasSection('navigation')
    <div class="pull-right">
        @yield('navigation')
    </div>

    <div class="clearfix"></div>
@endif

输出 json

<script>
    var app = @json($array);  // === <?php echo json_encode($array); ?>
</script>

拓展 Blade

# class AppServiceProvider 中 boot 方法
Blade::directive('datetime', function ($expression) {
    return "<?php echo ($expression)->format('m/d/Y H:i'); ?>";
});

更新Blade指令的逻辑之后,您需要删除所有缓存的Blade视图(php artisan view:clear)。

<!-- some.blade.php -->
@datetime($var)
// <?php echo ($var)->format('m/d/Y H:i'); ?>

自定义模版语法

use Illuminate\Support\Facades\Blade;

public function boot()
{
    Blade::if('env', function ($environment) {
        return app()->environment($environment);
    });
}
@env('local')
    // The application is in the local environment...
@else
    // The application is not in the local environment...
@endenv

堆栈

<!-- extend.blade.php -->
@push('scripts')
    <script src="/example.js"></script>
@endpush

/**  输出 start
<script src="/example.js"></script>
<script src="/example.js"></script>
     输出 end  **/
<!-- common.blade.php -->
@stack('scripts')
@stack('scripts')

服务注入

@inject('metrics', 'App\Services\MetricsService')

<div>
    Monthly Revenue: {{ $metrics->monthlyRevenue() }}.
</div>

继承

<!-- layouts/app.blade.php -->
@yield('title')

@section('sidebar')
这是 master 的侧边栏。
@show
<!-- child.blade.php -->
@extends('layouts.app')
  
@section('title',' Here is a div ')
// 等同于
@section('title') Here is a div @endsection
  
@section('sidebar')
    @parent
    <p>This is appended to the master sidebar.</p>
@endsection

slot

<!-- alert.blade.php -->
<div class="alert alert-danger">
    {{ $slot }}
</div>
  
<!-- other.blade.php -->
@component('alert')
    <strong>哇!</strong> 出现了一些问题!
@endcomponent
<!-- alert.blade.php -->
<div class="alert alert-danger">
    <div class="alert-title">{{ $title }}</div>
    {{ $slot }}
</div>
  
<!-- other.blade.php -->  
@component('alert')
    @slot('title')
        拒绝
    @endslot
    你没有权限访问这个资源!
@endcomponent

/**  输出 start
<div class="alert alert-danger">
    <div class="alert-title">拒绝</div>
    你没有权限访问这个资源!
</div>
     输出 end  **/
@component('alert', ['foo' => 'bar'])
    ...
@endcomponent
# 所有的数据都将以变量的形式传递给组件模版 alert.blade.php

Blade & JavaScript 框架

@{{ name }}
# 最终输出 {{ name }} ,供一些 js 框架使用。 

如果是大量的需要这么做,可以使用

@verbatim
    <div class="container">
        Hello, {{ name }}.
    </div>
@endverbatim

控制结构

@unless (Auth::check())
    你尚未登录。
@endunless
// 和 if 相反
@for ($i = 0; $i < 10; $i++)
    目前的值为 {{ $i }}
@endfor

@foreach ($users as $user)
    <p>此用户为 {{ $user->id }}</p>
@endforeach

@forelse ($users as $user)
    <li>{{ $user->name }}</li>
@empty          // 指的是 $users
    <p>没有用户</p>
@endforelse

@while (true)
    <p>死循环了。</p>
@endwhile
@isset($records)
    // $records is defined and is not null...
@endisset

@empty($records)
    // $records is "empty"...
@endempty
@auth('admin')
    // The user is authenticated...
@endauth

@guest('admin')
    // The user is not authenticated...
@endguest
@switch($i)
    @case(1)
        First case...
        @break

    @case(2)
        Second case...
        @break

    @default
        Default case...
@endswitch
@foreach ($users as $user)
    @if ($user->type == 1)
        @continue       # 结束此次循环
    @endif

    <li>{{ $user->name }}</li>

    @if ($user->number == 5)
        @break          # 跳出当前循环
    @endif
@endforeach
  
# 等同于
@foreach ($users as $user)
    @continue($user->type == 1)

    <li>{{ $user->name }}</li>

    @break($user->number == 5)
@endforeach

循环结构

当前循环是不是首次迭代,又或者当前循环是不是最后一次迭代:

@foreach ($users as $user)
    @if ($loop->first)
        This is the first iteration.
    @endif

    @if ($loop->last)
        This is the last iteration.
    @endif

    <p>This is user {{ $user->id }}</p>
@endforeach

# 嵌套结构还可以这么玩儿
@foreach ($users as $user)
    @foreach ($user->posts as $post)
        @if ($loop->parent->first)
            This is first iteration of the parent loop.
        @endif
    @endforeach
@endforeach
// 适用于 forelse
$loop->index       当前循环所迭代的索引,起始为 0。
$loop->iteration   当前迭代数,起始为 1。
$loop->remaining   循环中迭代剩余的数量。 // 不包含当前的这次
$loop->count       被迭代项的总数量。
$loop->firs        当前迭代是否是循环中的首次迭代。
$loop->last        当前迭代是否是循环中的最后一次迭代。
$loop->depth       当前循环的嵌套深度。
$loop->parent      当在嵌套的循环内时,可以访问到父循环中的 $loop 变量。

子视图

所有在父视图的可用变量在被引入的视图中都是可用的。

@include('shared.errors')

如果你想引入一个视图,而你又无法确认这个视图存在与否,你可以使用 @includeIf 指令:

@includeIf('view.name', ['some' => 'data'])
@includeWhen($boolean, 'view.name', ['some' => 'data'])
@includeFirst(['custom.admin', 'admin'], ['some' => 'data'])     // 谁先存在先加载谁

为集合数组渲染视图

// 子视图使用 key 变量作为当前迭代的键名。
// 注意:通过 @each 不会从父视图继承变量。 如果子需要这些变量,应该使用 @foreach 和 @include。
@each('view.name', $jobs, 'job')
@each('view.name', $jobs, 'job', 'view.empty')
  
// 自己做的例子
<!-- alert.blade.php -->
<div class="alert alert-danger">
    {{ $aa }}
</div>

<!-- other.blade.php -->
@each('alert', ['aaa','bbb'], 'aa')

/********** 输出 *********
<div class="alert alert-danger">
    aaa
</div>
<div class="alert alert-danger">
    bbb
</div>
************************/

二、本地化

  1. 实时设置本地化语言:App::setLocale($locale);
  2. config/app.php中的fallback_locale选项是设置备用语言,当第一语言不可用,则使用这个。
  3. if (App::isLocale('en'))确认当前语言。
  4. resource/lang文件夹下可以自定义其他国家的语言,与已存在的文件相对应即可。

长语句翻译

如果有大量东西需要翻译,你会发现如果用短字符串或者单词,容易看蒙了,这时候可以使用长的语句去翻译。例如建立文件resources/lang/es.json

{
    "I love programming.": "Me encanta programar."
}

取出翻译

echo __('messages.welcome');       // 如果不存在,返回 messages.welcome
echo __('I love programming.');

{{ __('messages.welcome') }}
@lang('messages.welcome')

echo __('messages.welcome', ['name' => 'dayle']);    // 'welcome' => 'Welcome, :name',

'welcome' => 'Welcome, :NAME', // Welcome, DAYLE
'goodbye' => 'Goodbye, :Name', // Goodbye, Dayle

'apples' => 'There is one apple|There are many apples',

'apples' => '{0} There are none|[1,19] There are some|[20,*] There are many',

echo trans_choice('messages.apples', 10);          

'minutes_ago' => '{1} :value minute ago|[2,*] :value minutes ago',
echo trans_choice('time.minutes_ago', 5, ['value' => 5]);

包语言覆盖

建立对应文件:resources/lang/vendor/{package}/{locale}
举个栗子, skyrim/hearthfire覆盖messages.php翻译,你应该建立文件resources/lang/vendor/hearthfire/en/messages.php

三、前端脚手架

如果不需要 vue 和 bootstrap:php artisan preset none

写 css

在编译 css 之前,请安装前端依赖

npm install

npm run dev命令会处理webpack.mix.js文件中的指令。编译的 css 文件将放在目录public/css中。

npm run dev

写 js

安装依赖

npm install

编译

npm run dev

编写 vue 组件

Vue.component(
    'example-component',
    require('./components/ExampleComponent.vue')
);

使用组件

@extends('layouts.app')

@section('content')
    <example-component></example-component>
@endsection

使用 react

php artisan preset react

四、Laravel mix

安装与配置

安装 node 和 npm。

node -v
npm -v

Laravel Mix

安装 mix

npm install

运行 mix

// Run all Mix tasks...
npm run dev

// Run all Mix tasks and minify output...
npm run production

监听运行 mix

npm run watch

如果无效请尝试以下命令:

npm run watch-poll

与样式一起编译

Less

less 方法编译 less 到 css。

mix.less('resources/assets/less/app.less', 'public/css');
mix.less('resources/assets/less/app.less', 'public/css')
   .less('resources/assets/less/admin.less', 'public/css');

指定文件名

mix.less('resources/assets/less/app.less', 'public/stylesheets/styles.css');

设置 less 选项

mix.less('resources/assets/less/app.less', 'public/css', {
    strictMath: true
});

Sass

mix.sass('resources/assets/sass/app.scss', 'public/css');
mix.sass('resources/assets/sass/app.sass', 'public/css')
   .sass('resources/assets/sass/admin.sass', 'public/css/admin');
mix.sass('resources/assets/sass/app.sass', 'public/css', {
    precision: 5
});

Stylus

mix.stylus('resources/assets/stylus/app.styl', 'public/css');

安装插件

# npm install rupture

mix.stylus('resources/assets/stylus/app.styl', 'public/css', {
    use: [
        require('rupture')()
    ]
});

PostCSS

mix.sass('resources/assets/sass/app.scss', 'public/css')
   .options({
        postCss: [
            require('postcss-css-variables')()
        ]
   });

原生 CSS

mix.styles([
    'public/css/vendor/normalize.css',
    'public/css/vendor/videojs.css'
], 'public/css/all.css');

URL 处理

.example {
    background: url('../images/example.png');
}

默认情况下, Laravel Mix 和 Webpack 会找到 example.png, 拷贝到 public/images,然后改写url(),变成这样子:

.example {
  background: url(/images/example.png?d41d8cd98f00b204e9800998ecf8427e);
}

url('/images/thing.png') 或者 url('http://example.com/images/thing.png') 不会变.

如果你的目录结构不是默认那样,可以禁用重写url()

mix.sass('resources/assets/app/app.scss', 'public/css')
   .options({
      processCssUrls: false
   });

禁用后,原来是啥样,现在还是啥样

.example {
    background: url("../images/thing.png");
}

Source Maps

启用 source maps

mix.js('resources/assets/js/app.js', 'public/js')
   .sourceMaps();

与 js 一起编译

mix.js('resources/assets/js/app.js', 'public/js');

上面一句代码就支持了以下功能

  1. ES 2015 语法
  2. 模块
  3. 编译 .vue 文件
  4. 生产环境压缩代码

依赖提取
如果将依赖绑定你的 js。这样你更新一点点代码就要使得客户端重新下载你的依赖。这时候可以使用依赖提取方法extract

mix.js('resources/assets/js/app.js', 'public/js')
   .extract(['vue'])

运行后生成以下文件

  • public/js/manifest.js: The Webpack 运行时的 manifest 文件
  • public/js/vendor.js: 你的依赖
  • public/js/app.js: 你的 js

引用顺序不能出错。

React

Mix 可以自动安装 Babel 插件来支持 React。为了使用 React,你只需将 mix.js() 的调用替换成 mix.react() 即可:

mix.react('resources/assets/js/app.jsx', 'public/js');

原生 JS

类似使用 mix.styles() 来合并多个样式表一样,你也可以使用 scripts() 方法来合并并压缩多个 JavaScript 文件:

mix.scripts([
    'public/js/admin.js',
    'public/js/dashboard.js'
], 'public/js/all.js');

mix.babel()mix.scripts()有点不同,那就是mix.babel()会将所有 ES2015 的代码转换为所有浏览器都能识别的原生 JavaScript。

自定义 webpack 配置

两种方式

合并定制配置

mix.webpackConfig({
    resolve: {
        modules: [
            path.resolve(__dirname, 'vendor/laravel/spark/resources/assets/js')
        ]
    }
});

定制配置文件

复制node_modules/laravel-mix/setup/webpack.config.js到你的根目录。然后修改package.json文件中的--config参数。采用这种方法进行自定义,如果后续有更新时,需要手动合并到你的自定义文件中。

拷贝文件和目录

mix.copy('node_modules/foo/bar.css', 'public/css/bar.css');

拷贝目录使用 copyDirectory 防止扁平化目录结构(目录分别都是从属关系)

mix.copyDirectory('assets/img', 'public/img');

版本控制以清除缓存

mix.js('resources/assets/js/app.js', 'public/js')
   .version();

获取文件名

<link rel="stylesheet" href="{{ mix('/css/app.css') }}">

指定版本控制只适用于生产环境

npm run production:

mix.js('resources/assets/js/app.js', 'public/js');

if (mix.inProduction()) {
    mix.version();
}

Browsersync 重新加载

BrowserSync 可以自动监控你的文件变化,并将更改注入浏览器,而无需手动刷新。你可以通过调用 mix.browserSync() 方法来启用这个功能的支持:

mix.browserSync('my-domain.test');

// Or...

// https://browsersync.io/docs/options
mix.browserSync({
    proxy: 'my-domain.test'
});

你可以将字符串 (代理) 或者对象 (BrowserSync 设置) 传给这个方法。再使用 npm run watch 命令来开启 Webpack 的开发服务器。现在,当你修改脚本或者 PHP 文件时,浏览器会即时刷新页面以响应你的更改。

环境变量

.env文件中设置

MIX_SENTRY_DSN_PUBLIC=http://example.com

如果 npm run watch 下更改了值,需要重新 npm run watch

process.env.MIX_SENTRY_DSN_PUBLIC

通知

默认会进行系统通知编译情况,如果不希望在生产服务器上通知:

mix.disableNotifications();
#########################################################################################
#########################################################################################
#########################################################################################
#########################################################################################
#########################################################################################
#########################################################################################

第六章:安全(一、认证。二、api 认证。三、授权。四、加密。五、哈希。六、重置密码。)

#########################################################################################
#########################################################################################
#########################################################################################
#########################################################################################
#########################################################################################
#########################################################################################

一、认证

简介

认证系统主要由两部分组成:

guards
每个请求中,怎么认证用户
例如:通过 session guard

providers
怎样从持久化存储中获取用户
例如:通过 Eloquent, DB

一般来说,不用修改默认的认证配置

使用数据库认证时候,并且使用内置的认证机制时:

  1. 必须设置密码字段大于等于 60 个字符长。
  2. remember_token 必须100个字符长以上,且可为空。

认证快速开始

1. php artisan make:auth
2. php artisan migrate
3. 访问 http://your-app.dev/register

修改跳转地址

protected $redirectTo = '/';
# 方法的优先级高于属性定义
protected function redirectTo()
{
    return '/path';
}

认证字段修改

public function username(){
    return 'username';
}

使用其他的 guard

# LoginController, RegisterController, ResetPasswordController
use Illuminate\Support\Facades\Auth;
# 返回的应该是 a guard instance
protected function guard()
{
    return Auth::guard('guard-name');
}

取出认证信息

$user = Auth::user();
$id = Auth::id();
if (Auth::check())

$request->user()

路由认证指定 guard

->middleware('auth:api');

如果登录失败次数过多,会禁止登录一段时间。判断的标准是 username 方法返回值和 ip 。

手动认证用户

# 当你不喜欢自带的控制器去认证用户,你可以移除这些控制器,
# 引入 Auth facade,利用 attempt 手动认证
if (Auth::attempt(['email' => $email, 'password' => $password])) {
    // 认证成功后会产生 session
    return redirect()->intended('dashboard');
}

if (Auth::attempt(['email' => $email, 'password' => $password, 'active' => 1])) {
    // 字段 active 必须是 1
}

if (Auth::guard('admin')->attempt($credentials)) {
    // 指定 guard
}

登出

Auth::logout();

记住用户 (无限期)

# $remember 是个 bool 值
if (Auth::attempt(['email' => $email, 'password' => $password], $remember)) {
    // The user is being remembered...
}

# 判断是否选择了记住用户
if (Auth::viaRemember()) {
    //
}
Auth::login($user);
Auth::login($user, true);  // 记住
Auth::guar('admin')->login($user);
Auth::loginUsingId(1);
Auth::loginUsingId(1, true);
Auth::once($credentials); // 临时认证,无状态的。

无登录页面, 利用弹窗请求认证用户

Route::get('profile', function(){
    // ...
})->middleware('auth.basic');

# 如果使用 php fastcgi,可能会失效,可以在 .htaccess 里面加入
RewriteCond %{HTTP:Authorization} ^(.+)$
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]

利用中间件

namespace App\Http\Middleware;

use Illuminate\Support\Facades\Auth;

class AuthenticateOnceWithBasicAuth
{
    public function handle($request, $next)
    {
        return Auth::onceBasic() ?: $next($request);     // Auth::onceBasic() 无返回值就代表认证成功
    }

}
# 注册中间件后 ->middleware('auth.basic.once');

增加自定义 guard

# extend 方法
class AuthServiceProvider extends ServiceProvider
{
    /**
     * Register any application authentication / authorization services.
     *
     * @return void
     */
    public function boot()
    {
        $this->registerPolicies();

        Auth::extend('jwt', function ($app, $name, array $config) {
            // Return an instance of Illuminate\Contracts\Auth\Guard...

            return new JwtGuard(Auth::createUserProvider($config['provider']));
        });
    }
}

增加自定义 provider

# extend 方法
class AuthServiceProvider extends ServiceProvider
{
    /**
     * Register any application authentication / authorization services.
     *
     * @return void
     */
    public function boot()
    {
        $this->registerPolicies();

        Auth::provider('riak', function ($app, array $config) {
            // Return an instance of Illuminate\Contracts\Auth\UserProvider...

            return new RiakUserProvider($app->make('riak.connection'));
        });
    }
}

认证事件

protected $listen = [
    'Illuminate\Auth\Events\Registered' => [
        'App\Listeners\LogRegisteredUser',
    ],

    'Illuminate\Auth\Events\Attempting' => [
        'App\Listeners\LogAuthenticationAttempt',
    ],

    'Illuminate\Auth\Events\Authenticated' => [
        'App\Listeners\LogAuthenticated',
    ],

    'Illuminate\Auth\Events\Login' => [
        'App\Listeners\LogSuccessfulLogin',
    ],

    'Illuminate\Auth\Events\Failed' => [
        'App\Listeners\LogFailedLogin',
    ],

    'Illuminate\Auth\Events\Logout' => [
        'App\Listeners\LogSuccessfulLogout',
    ],

    'Illuminate\Auth\Events\Lockout' => [
        'App\Listeners\LogLockout',
    ],

    'Illuminate\Auth\Events\PasswordReset' => [
        'App\Listeners\LogPasswordReset',
    ],
];

二、api 认证

TODO

三、授权

Gates 和 Policies 异同点

一般情况下,可以替换使用这两者进行认证。相比较而言,Gates 一般是个闭包,简单的逻辑。而 Policies 可能会涉及到模型或者资源。

Gates 定义

# App\Providers\AuthServiceProvider
public function boot()
{
    $this->registerPolicies();

    Gate::define('update-post', function ($user, $post) {
        return $user->id == $post->user_id;
    });
}

public function boot()
{
    $this->registerPolicies();

    Gate::define('update-post', 'PostPolicy@update');
}

Gate::resource('posts', 'PostPolicy');  // 等价于下面
# Gate::define('posts.view', 'PostPolicy@view');
# Gate::define('posts.create', 'PostPolicy@create');
# Gate::define('posts.update', 'PostPolicy@update');
# Gate::define('posts.delete', 'PostPolicy@delete');

Gate::resource('posts', 'PostPolicy', [
    'image' => 'updateImage',
    'photo' => 'updatePhoto',
]);
# Gate::define('post.image', 'PostPolicy@updateImage');
# Gate::define('post.photo', 'PostPolicy@updatePhoto');

Gates 使用

if (Gate::allows('update-post', $post)) {
    // The current user can update the post...
}

if (Gate::denies('update-post', $post)) {
    // The current user can't update the post...
}

if (Gate::forUser($user)->allows('update-post', $post)) {
    // The user can update the post...
}

if (Gate::forUser($user)->denies('update-post', $post)) {
    // The user can't update the post...
}

Policies 定义

php artisan make:policy PostPolicy --model=Post

# AuthServiceProvider 
use App\Policies\PostPolicy;
use App\Post;
protected $policies = [
    Post::class => PostPolicy::class,
];

# PostPolicy
public function update(User $user, Post $post)
{
    return $user->id === $post->user_id;
}
// 没有模型,只有一个参数的情况,一般 create 会是这种情况
public function create(User $user)
{
    //
}
# 在最前端认证,一般是管理员在这里认证
# 不能返回 null,返回了就会就续执行 policy 方法
# 如果未能找到和被检测的能力的名称对应的 policy 方法名, before 方法不被调用
public function before($user, $ability): bool
{
    if ($user->isSuperAdmin()) {
        return true;
    }
}

Policies 使用

if ($user->can('update', $post)) {
    //
}
# 如果给定的模型 即 $post 已经注册了 policy ,会调用合适的 policy,
# 如果没有注册,会尝试查找和操作名称相匹配的 Gate 。

# 如果是只有一个参数,如 create 操作
use App\Post;
if ($user->can('create', Post::class)) {
    // Executes the "create" method on the relevant policy...
}



# 通过中间件
use App\Post;
Route::put('/post/{post}', function (Post $post) {
    // The current user may update the post...
})->middleware('can:update,post');   // 第二个参数是 路由参数
// 认证失败返回 403

# 如果是只有一个参数,如 create 操作
Route::post('/post', function () {
    // The current user may create posts...
})->middleware('can:create,App\Post');



# 在控制器中
public function update(Request $request, Post $post)
{
    $this->authorize('update', $post);
    // The current user can update the blog post...
    // 授权失败返回 403
}
# 如果是只有一个参数,如 create 操作
public function create(Request $request)
{
    $this->authorize('create', Post::class);
}



# 在 blade 中使用
@can('update', $post)
    <!-- The Current User Can Update The Post -->
@elsecan('create', App\Post::class)
    <!-- The Current User Can Create New Post -->
@endcan

@cannot('update', $post)
    <!-- The Current User Can't Update The Post -->
@elsecannot('create', App\Post::class)
    <!-- The Current User Can't Create New Post -->
@endcannot
# 类似于
@if (Auth::user()->can('update', $post))
    <!-- The Current User Can Update The Post -->
@endif

@unless (Auth::user()->can('update', $post))
    <!-- The Current User Can't Update The Post -->
@endunless

@can('create', App\Post::class)
    <!-- The Current User Can Create Posts -->
@endcan
# 如果是只有一个参数,如 create 操作
@cannot('create', App\Post::class)
    <!-- The Current User Can't Create Posts -->
@endcannot

四、加密

encrypt($yourString)   // 这是内部序列化后再加密
decrypt($yourString) 
try {
    $decrypted = decrypt($encryptedValue);   
    // 如果加密值被修改,或非法值传入,抛出异常 DecryptException
} catch (DecryptException $e) {
    //
}


# 如果不需要序列化加解密
$encrypted = Crypt::encryptString('Hello world.');
$decrypted = Crypt::decryptString($encrypted);

五、哈希

利用了 Bcrypt,会随着硬件的加强而加强加密 hash。

$passwordIntoDB = Hash::make($request->newPassword);
// 等价于 bcrypt

# 验证
if (Hash::check('plain-text', $hashedPassword)) {
    // The passwords match...
}

# 验证是否需要重新加密
/**
The needsRehash function allows you to determine 
if the work factor used by the hasher has changed 
since the password was hashed.
这句话理解很久,不太明白它的意思。
since 这儿应该翻译成自从。
意思是自从密码哈希后,你可以利用这个函数确定哈希的哈希因子是否改变过,
以至于我们需要重新进行哈希。
*/

if (Hash::needsRehash($hashed)) {
    $hashed = Hash::make('plain-text');
}

六、重置密码

重置密码 token 有效时长为一小时。可在config/auth.php中修改时长。

自定义密码 broker

auth.php配置文件中,你可以配置多个密码 brokers,用于多个用户表上的密码重置。你可以通过重写ForgotPasswordControllerResetPasswordController中的brokers方法来选择你想使用的自定义brokers

protected function broker()
{
    return Password::broker('name');
}

自定义发送重置密码通知

# App\User
public function sendPasswordResetNotification($token)
{
    $this->notify(new ResetPasswordNotification($token));
}
#########################################################################################
#########################################################################################
#########################################################################################
#########################################################################################
#########################################################################################
#########################################################################################

第七章:深入挖掘(一、Artisan 命令行。二、广播。三、缓存。四、集合。五、事件。六、文件存储。七、辅助函数。八、通知。九、开发拓展包。十、队列。十一、任务计划。)

#########################################################################################
#########################################################################################
#########################################################################################
#########################################################################################
#########################################################################################
#########################################################################################

一、Artisan 命令行

基础

php artisan list
php artisan help migrate
php artisan tinker

编写一条命令

第一种

php artisan make:command SendEmails
class SendEmails extends Command
{
    protected $signature = 'email:send {user}';

    protected $description = 'Send drip e-mails to a user';

    protected $drip;

    public function __construct(DripEmailer $drip)
    {
        parent::__construct();

        $this->drip = $drip;
    }

    public function handle()
    {
        $this->drip->send(User::find($this->argument('user')));
    }
}

第二种

Artisan::command('build {project}', function ($project) {
    $this->info("Building {$project}!");
})->describe('Build the project');

定义预期输入

email:send {user}
email:send {user?}
email:send {user=foo}

两种 选项,一种可以赋值,另一种不用赋值。不用赋值的作为一个开关存在。

# protected $signature = 'email:send {user} {--queue}';
# php artisan email:send 1 --queue
# protected $signature = 'email:send {user} {--queue=}';       // 可以设置默认值 --queue=default
# php artisan email:send 1 --queue=default

设置选项的快捷方式

email:send {user} {--Q|queue}

如果想要参数接受数组

email:send {user*}
# php artisan email:send foo bar

如果需要选项接受数组

email:send {user} {--id=*}
php artisan email:send --id=1 --id=2

给选项或参数设置描述

protected $signature = 'email:send
                        {user : The ID of the user}
                        {--queue= : Whether the job should be queued}';

命令输入与输出

获取输入,如果不存在,返回 null。

public function handle()
{
    $userId = $this->argument('user');
}

$arguments = $this->arguments();

$queueName = $this->option('queue');

$options = $this->options();

提示输入,confirm 默认为 false,输入 y 或 yes 为 true。

$name = $this->ask('What is your name?');

$password = $this->secret('What is the password?');

if ($this->confirm('Do you wish to continue?')) {
    //
}
$name = $this->anticipate('What is your name?', ['Taylor', 'Dayle']);
// 可以自己输入,不输入则随机其中一个

$name = $this->choice('What is your name?', ['Taylor', 'Dayle'], $defaultIndex);
// 只可以选择,第三个参数设置默认的,值为数组的键

编写输出

颜色

# line, info, comment, question, error
$this->info('Display this on the screen');

表格

$headers = ['Name', 'Email'];

$users = App\User::all(['name', 'email'])->toArray();

$this->table($headers, $users);

进度条

$users = App\User::all();

$bar = $this->output->createProgressBar(count($users));

foreach ($users as $user) {
    $this->performTask($user);

    $bar->advance();
}

$bar->finish();

注册命令

由于在控制台内核文件中的 commands 方法调用了 load 方法,所以 app/Console/Commands 目录下的所有命令都将自动注册到 Artisan。 实际上,你可以自由地调用 load 方法来扫描 Artisan 命令的其他目录:

protected function commands()
{
    $this->load(__DIR__.'/Commands');
    $this->load(__DIR__.'/MoreCommands');

    // ...
}

手动注册命令

protected $commands = [
    Commands\SendEmails::class
];

代码执行命令

Route::get('/foo', function () {
    $exitCode = Artisan::call('email:send', [
        'user' => 1, '--queue' => 'default'
    ]);
});


Route::get('/foo', function () {
    Artisan::queue('email:send', [             // 后台执行,确保你已经配置了队列并运行了队列
        'user' => 1, '--queue' => 'default'
    ]);

    //
});

Route::get('/foo', function () {
    $exitCode = Artisan::call('email:send', [
        'user' => 1, '--id' => [5, 13]
    ]);
});

$exitCode = Artisan::call('migrate:refresh', [
    '--force' => true,
]);

有时候你希望从现有的 Artisan 命令中调用其它命令。你可以使用 call 方法。

public function handle()
{
    $this->call('email:send', [
        'user' => 1, '--queue' => 'default'
    ]);

    //
}

如果要调用另一个控制台命令并阻止其所有输出,可以使用 callSilent 方法。 callSilent 和 call 方法用法一样:

$this->callSilent('email:send', [
    'user' => 1, '--queue' => 'default'
]);

二、广播

TODO

三、缓存

缓存配置

数据库

php artisan cache:table

memcache

'memcached' => [
    [
        'host' => '127.0.0.1',
        'port' => 11211,
        'weight' => 100
    ],
],
或者
'memcached' => [
    [
        'host' => '/var/run/memcached/memcached.sock',
        'port' => 0,
        'weight' => 100
    ],
],

基础操作

Cache::has('key')       // 未设置 key 或者值是 null 时候,返回 null
Cache::get('key')       // 值是什么返回什么,未设置,返回 null
Cache::get('key', 'defult') 
Cache::increment('key') 
Cache::increment('key', 3)      // 如果 key 不存在,返回 3,并存储 key 为 3; 失败返回 false

访问多个缓存地方

$value = Cache::store('file')->get('foo');

Cache::store('redis')->put('bar', 'baz', 10);

同名方法

sear = rememberForever
put = set
has = offsetExists
get = offsetGet
forget = offsetUnset

remember 实现获取缓存数据,不存在则存储缓存

$users = Cache::remember('users', 10, function () {
    return DB::table('users')->get();
});
// 等同于
if (! $users = Cache::get('users')) {
    $users = DB::table('users')->get();
    Cache::set('users', $users, 10);  // 10 => 10 分钟
}

many: 同时获取多个 key,不存在返回 null,此时等同于 get

$aStudent = Cache::many(['helen', 'jack']);
// 等同于 get

// 设置默认值
$aStudent = Cache::many(['helen' => new Student(), 'jack']);
// 等同于 get

putMany: 设定多个值

Cache::putMany([
    'helen' => new Student('helen'), 
    'jack' => '10'
], 10);

pull: 获取 key 然后删除 key

$value = Cache::pull('key');  // 如果不存在 key,返回 null
$value = Cache::pull('key', 'value');

put: 设置缓存

Cache::put('key', 'value', 10);

$expiresAt = Carbon::now()->addMinutes(10);
Cache::put('key', 'value', $expiresAt);  

add: 如果缓存存在了,就返回 false;如果不存在,则添加缓存,然后返回 true

Cache::add('key', 'value', 10);  

forever | forget | flush | clear: 永久设置缓存;删除缓存;全部删除

Cache::forever('key', 'value');

Cache::forget('key');

Cache::flush();   // 不区别前缀,删除所有,小心使用
// 等同于 clear

当你使用 memcached 驱动,缓存达到极限时候,forever 可能被删除。

deleteMultiple: 删除多个 key

Cache::deleteMultiple(['key1', 'key2']);

tag: 设置标签

# file、database 驱动不支持
# 使用标签使得缓存是 forever
# 此时性能最好的是 memcached
# 取的时候标签内容必须一致,不能多,不能少。

Cache::tags(['people', 'artists'])->put('John', 141, $minutes);
Cache::tags(['people', 'authors'])->put('Anne', 22, $minutes);

Cache::tags('authors')->flush();
// 只删除 authors 的缓存

Cache::tags(['people', 'authors'])->flush();  
// 标签 people、authors、people 和 authors 都会被删除

dump(Cache::tags(['people', 'authors'])->get('Anne'));    // null
dump(Cache::tags(['people', 'artists'])->get('John'));    // '141'

辅助全局函数

cache('key');

cache(['key' => 'value'], $minutes);

cache(['key' => 'value'], Carbon::now()->addSeconds(10));

自定义驱动,举个栗子 MongoDB

<?php

namespace App\Extensions;

use Illuminate\Contracts\Cache\Store;

class MongoStore implements Store
{
    public function get($key) {}
    public function many(array $keys);
    public function put($key, $value, $minutes) {}
    public function putMany(array $values, $minutes);
    public function increment($key, $value = 1) {}
    public function decrement($key, $value = 1) {}
    public function forever($key, $value) {}
    public function forget($key) {}
    public function flush() {}
    public function getPrefix() {}
}

// 注册
# CacheServiceProvider 
Cache::extend('mongo', function ($app) {
    return Cache::repository(new MongoStore);
});

getStore: 返回缓存实例

# sample 1
Illuminate\Cache\FileStore Object
(
    [files:protected] => Illuminate\Filesystem\Filesystem Object
        (
        )

    [directory:protected] => /var/www/html/larabbs/storage/framework/cache/data
)

# sample 2

Illuminate\Cache\RedisStore Object
(
    [redis:protected] => Illuminate\Redis\RedisManager Object
        (
            [driver:protected] => predis
            [config:protected] => Array
                (
                    [default] => Array
                        (
                            [host] => 127.0.0.1
                            [password] => 
                            [port] => 22222
                            [database] => 0
                        )

                )

            [connections:protected] => 
        )

    [prefix:protected] => myCachePrefix:
    [connection:protected] => default
)

缓存事件

protected $listen = [
    'Illuminate\Cache\Events\CacheHit' => [
        'App\Listeners\LogCacheHit',
    ],

    'Illuminate\Cache\Events\CacheMissed' => [
        'App\Listeners\LogCacheMissed',
    ],

    'Illuminate\Cache\Events\KeyForgotten' => [
        'App\Listeners\LogKeyForgotten',
    ],

    'Illuminate\Cache\Events\KeyWritten' => [
        'App\Listeners\LogKeyWritten',
    ],
];

四、集合

Eloquent 查询的返回值总是一个集合。

集合拓展

use Illuminate\Support\Str;

Collection::macro('toUpper', function () {
    return $this->map(function ($value) {
        return Str::upper($value);
    });
});

$collection = collect(['first', 'second']);

$upper = $collection->toUpper();

// ['FIRST', 'SECOND']

集合 api

all
average
avg
chunk
collapse
combine
concat
contains
containsStrict
count
crossJoin
dd
diff
diffAssoc
diffKeys
dump
each
eachSpread
every
except
filter
first
firstWhere
flatMap
flatten
flip
forget
forPage
get
groupBy
has
implode
intersect
intersectByKeys
isEmpty
isNotEmpty
keyBy
keys
last
macro
make
map
mapInto
mapSpread
mapToGroups
mapWithKeys
max
median
merge
min
mode
nth
only
pad
partition
pipe
pluck
pop
prepend
pull
push
put
random
reduce
reject
reverse
search
shift
shuffle
slice
sort
sortBy
sortByDesc
splice
split
sum
take
tap
times
toArray
toJson
transform
union
unique
uniqueStrict
unless
unwrap
values
when
where
whereStrict
whereIn
whereInStrict
whereNotIn
whereNotInStrict
wrap
zip
集合 api 详细用法:集合 api 官方文档

toArray 会递归的把集合转化为数组,如果你想转化成原来的数组,请用 all 方法。

五、事件

注册事件和监听器
事件指的是某个操作,某个时间点,等等。监听器监听到事件的发生,会触发对应的方法。

# EventServiceProvider 
protected $listen = [
    'App\Events\OrderShipped' => [
        'App\Listeners\SendShipmentNotification',
    ],
];

# 也可以注册在 boot 方法里
Event::listen('event.name', function ($foo, $bar) {
      
});
# 通配符
Event::listen('event.*', function ($eventName, array $data) {
    //
});

根据你的注册,生成

php artisan event:generate

定义事件

<?php

namespace App\Events;

use App\Order;
use Illuminate\Queue\SerializesModels;

class OrderShipped
{
    use SerializesModels;

    public $order;

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

定义监听器

<?php

namespace App\Listeners;

use App\Events\OrderShipped;

class SendShipmentNotification
{
    public function __construct()
    {
        //
    }

    public function handle(OrderShipped $event)
    {
        // Access the order using $event->order...
        // 想禁止冒泡,请再次 return false
    }
}

队列化事件监听器

# 需要实现接口 ShouldQueue
# 用命令生成的自动加上 implement ShouldQueue
<?php

namespace App\Listeners;

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

class SendShipmentNotification implements ShouldQueue
{
    //
}

在监听器中设置队列链接和名称

public $connection = 'sqs';
public $queue = 'listeners';

手动访问队列

# 如果想在监听器中 delete 和 release 任务,
# 需要使用 Illuminate\Queue\InteractsWithQueue trait。
public function handle(OrderShipped $event)
{
    if (true) {
        $this->release(30);
    }
}

处理失败的任务

# 超过尝试次数,依然异常,则调用监听器的 failed 方法
public function failed(OrderShipped $event, $exception)
{
    //
}

分发事件

$order = Order::findOrFail($orderId);
event(new OrderShipped($order));

事件订阅器

实现了在单个类中包含多个监听器。

<?php
namespace App\Listeners;

class UserEventSubscriber
{
    public function onUserLogin($event) {}

    public function onUserLogout($event) {}

    public function subscribe($events)
    {
        $events->listen(
            'Illuminate\Auth\Events\Login',
            'App\Listeners\UserEventSubscriber@onUserLogin'
        );

        $events->listen(
            'Illuminate\Auth\Events\Logout',
            'App\Listeners\UserEventSubscriber@onUserLogout'
        );
    }

}

注册事件订阅器

class EventServiceProvider extends ServiceProvider
{
    protected $listen = [
        //
    ];

    protected $subscribe = [
        'App\Listeners\UserEventSubscriber',
    ];
}

六、文件系统

查看config/filesystems.php的配置,可以明显看到

Storage::disk('local')->put('file.txt', 'Contents');
// 存储于 storage/app/file.txt

ftp 驱动系统

'ftp' => [
    'driver'   => 'ftp',
    'host'     => 'ftp.example.com',
    'username' => 'your-username',
    'password' => 'your-password',

    // Optional FTP Settings...
    // 'port'     => 21,
    // 'root'     => '',
    // 'passive'  => true,
    // 'ssl'      => true,
    // 'timeout'  => 30,
],

获得磁盘实例

Storage::put('avatars/1', $fileContents);
Storage::disk('s3')->put('avatars/1', $fileContents);
$contents = Storage::get('file.jpg');
$exists = Storage::disk('s3')->exists('file.jpg');

$url = Storage::url('file1.jpg');  // 获取相对链接

$url = Storage::temporaryUrl(
    'file1.jpg', now()->addMinutes(5)
);

$size = Storage::size('file1.jpg');

$time = Storage::lastModified('file1.jpg');

存文件

Storage::put('file.jpg', $contents);

Storage::put('file.jpg', $resource);

Storage::putFile('photos', new File('/path/to/photo'));

// Manually specify a file name...
Storage::putFileAs('photos', new File('/path/to/photo'), 'photo.jpg');

Storage::putFile('photos', new File('/path/to/photo'), 'public');
// 设置为可见

Storage::prepend('file.log', 'Prepended Text');
Storage::append('file.log', 'Appended Text');
Storage::copy('old/file1.jpg', 'new/file1.jpg');
Storage::move('old/file1.jpg', 'new/file1.jpg');

$path = $request->file('avatar')->store('avatars');
$path = Storage::putFile('avatars', $request->file('avatar'));

$path = $request->file('avatar')->storeAs(
    'avatars', $request->user()->id
);
$path = Storage::putFileAs(
    'avatars', $request->file('avatar'), $request->user()->id
);

$path = $request->file('avatar')->store(
    'avatars/'.$request->user()->id, 's3'
);

Storage::put('file.jpg', $contents, 'public');

$visibility = Storage::getVisibility('file.jpg');
Storage::setVisibility('file.jpg', 'public')

删文件

Storage::delete('file.jpg');
Storage::delete(['file1.jpg', 'file2.jpg']);

Storage::disk('s3')->delete('folder_path/file_name.jpg');

文件夹

获取所有文件

$files = Storage::files($directory);

$files = Storage::allFiles($directory);

获取所有文件夹

$directories = Storage::directories($directory);

// Recursive...
$directories = Storage::allDirectories($directory);

创建文件夹(包含子文件夹)

Storage::makeDirectory($directory);

删除文件夹(包含子文件夹和文件)

Storage::deleteDirectory($directory);

定制文件系统

composer require spatie/flysystem-dropbox

<?php
namespace App\Providers;

use Storage;
use League\Flysystem\Filesystem;
use Illuminate\Support\ServiceProvider;
use Spatie\Dropbox\Client as DropboxClient;
use Spatie\FlysystemDropbox\DropboxAdapter;

class DropboxServiceProvider extends ServiceProvider
{
    public function boot()
    {
        Storage::extend('dropbox', function ($app, $config) {
            $client = new DropboxClient(
                $config['authorizationToken']
            );

            return new Filesystem(new DropboxAdapter($client));
        });
    }

    public function register()
    {
        //
    }
}

七、辅助函数

Arrays & Objects

array_add
array_collapse
array_divide
array_dot
array_except
array_first
array_flatten
array_forget
array_get
array_has
array_last
array_only
array_pluck
array_prepend
array_pull
array_random
array_set
array_sort
array_sort_recursive
array_where
array_wrap
data_fill
data_get
data_set
head
last

Paths

app_path
base_path
config_path
database_path
mix
public_path
resource_path
storage_path

Strings

__
camel_case
class_basename
e
ends_with
kebab_case
preg_replace_array
snake_case
starts_with
str_after
str_before
str_contains
str_finish
str_is
str_limit
str_plural
str_random
str_replace_array
str_replace_first
str_replace_last
str_singular
str_slug
str_start
studly_case
title_case
trans
trans_choice

URLs

action
asset
secure_asset
route
secure_url
url

Miscellaneous

abort
abort_if
abort_unless
app
auth
back
bcrypt
blank
broadcast
cache
class_uses_recursive
collect
config
cookie
csrf_field
csrf_token
dd
decrypt
dispatch
dispatch_now
dump
encrypt
env
event
factory
filled
info
logger
method_field
now
old
optional
policy
redirect
report
request
rescue
resolve
response
retry
session
tap
today
throw_if
throw_unless
trait_uses_recursive
transform
validator
value
view
with

八、邮件

生成邮件

php artisan make:mail OrderShipped

编写邮件

from

  1. 在 build 方法中
public function build()
{
    return $this->from('example@example.com')
                ->view('emails.orders.shipped');
}
  1. 全局
# config/mail.php
'from' => ['address' => 'example@example.com', 'name' => 'App Name'],

view

return $this->view('emails.orders.shipped');
return $this->text('emails.orders.shipped_plain');    // 纯文本
return $this->view('emails.orders.shipped')
                ->text('emails.orders.shipped_plain');    // 结合

给邮件模板传值

  1. 全局属性自动传值
public function __construct(Order $order)
{
    $this->order = $order;
}

public function build()
{
    return $this->view('emails.orders.shipped');
}

// 使用
<div>
    Price: {{ $order->price }}
</div>
  1. with 方法
# $order 只有属性是全局属性才能自动传值。否则只能通过 with 方法。
public function build()
{
    return $this->view('emails.orders.shipped')
        ->with([
            'orderName' => $this->order->name,
            'orderPrice' => $this->order->price,
        ]);
}

// 使用
<div>
    Price: {{ $orderPrice }}
</div>

附件

return $this->view('emails.orders.shipped')
                    ->attach('/path/to/file');

return $this->view('emails.orders.shipped')
                    ->attach('/path/to/file', [
                        'as' => 'name.pdf',     // 指定名称
                        'mime' => 'application/pdf',
                    ]); 

附件是源数据,例如:内存中的 pdf

return $this->view('emails.orders.shipped')
                    ->attachData($this->pdf, 'name.pdf', [
                        'mime' => 'application/pdf',
                    ]);

内联附件

<body>
    Here is an image:
    <img src="{{ $message->embed($pathToFile) }}">
</body>

$message 在 markdown 中不可用

内联嵌入源数据附件

<body>
    Here is an image from raw data:
    <img src="{{ $message->embedData($data, $name) }}">
</body>

自定义 SwiftMailer 消息

public function build()
{
    $this->view('emails.orders.shipped');

    $this->withSwiftMessage(function ($message) {
        $message->getHeaders()
            ->addTextHeader('Custom-Header', 'HeaderValue');
    });
}

markdown 邮件

php artisan make:mail OrderShipped --markdown=emails.orders.shipped
public function build()
{
    return $this->from('example@example.com')
                ->markdown('emails.orders.shipped');
}
@component('mail::button', ['url' => $url, 'color' => 'green'])
View Order
@endcomponent

@component('mail::panel')
This is the panel content.
@endcomponent

@component('mail::table')
| Laravel       | Table         | Example  |
| ------------- |:-------------:| --------:|
| Col 2 is      | Centered      | $10      |
| Col 3 is      | Right-Aligned | $20      |
@endcomponent

自定义 markdown 的渲染

# 执行改命令后,resources/views/vendor/mail 下面的文件可以根据自己需要进行修改
# 修改主题,可以在 theme 文件夹下新建 css,然后再配置文件中修改配置
php artisan vendor:publish --tag=laravel-mail

在浏览器中预览邮件

Route::get('/mailable', function () {
    $invoice = App\Invoice::find(1);
    return new App\Mail\InvoicePaid($invoice);
});

发送邮件

Mail::to($request->user())->send(new OrderShipped($order));

Mail::to($request->user())
    ->cc($moreUsers)
    ->bcc($evenMoreUsers)
    ->send(new OrderShipped($order));

# cc 抄送,bcc 暗抄送

队列化邮件

Mail::to($request->user())
    ->cc($moreUsers)
    ->bcc($evenMoreUsers)
    ->queue(new OrderShipped($order));

延迟队列中的邮件

$when = now()->addMinutes(10);

Mail::to($request->user())
    ->cc($moreUsers)
    ->bcc($evenMoreUsers)
    ->later($when, new OrderShipped($order));

推送指定的队列

$message = (new OrderShipped($order))
                ->onConnection('sqs')
                ->onQueue('emails');

Mail::to($request->user())
    ->cc($moreUsers)
    ->bcc($evenMoreUsers)
    ->queue($message);

默认塞进队列(实现 ShouldQueue)

use Illuminate\Contracts\Queue\ShouldQueue;

class OrderShipped extends Mailable implements ShouldQueue
{
    //
}

邮件和本地开发

  1. config/mail.php设置 to 选项,将开发时候的邮件都发给这个邮箱
'to' => [
    'address' => 'example@example.com',
    'name' => 'Example'
],
  1. 设置邮件驱动为 log

邮件事件

邮件在发送的前后生效,是发送邮件的时候,在队列中不算。

protected $listen = [
    'Illuminate\Mail\Events\MessageSending' => [
        'App\Listeners\LogSendingMessage',
    ],
    'Illuminate\Mail\Events\MessageSent' => [
        'App\Listeners\LogSentMessage',
    ],
];

八、通知

介绍

Laravel 支持利用不同的传送渠道来发送通知,包括邮件、sms、slack 。

创建通知

php artisan make:notification InvoicePaid

发送通知

默认渠道 mail

第一种方式

use Notifiable;
# App\User
$user->notify(new InvoicePaid($invoice));

第二种方式:优点是可以针对集合发送

Notification::send($user, new InovicePaid($invoice));   

指定渠道

开箱即用的渠道:mail, database, broadcast, nexmo, slack

public function via($notifiable)
{
    return $notifiable->prefers_sms ? ['nexmo'] : ['mail', 'database'];
}

塞入队列

引入 Queueable trait,实现 ShouldQueue 接口即可。

延迟发送

$when = now()->addMinutes(10);
$user->notify((new InvoicePaid($invoice))->delay($when));

按需通知

举个例子:用户未存储在应用中,指定临时通知路由信息。

Notification::route('mail', 'taylor@laravel.com')
            ->route('nexmo', '5555555555')
            ->notify(new InvoicePaid($invoice));

邮件通知

public function toMail($notifiable)
{
    $url = url('/invoice/'.$this->invoice->id);
// $this->invoice: 你可以在构造器中传入你需要用到的数据。
    return (new MailMessage)
                ->greeting('Hello!')
                ->line('One of your invoices has been paid!')
                ->action('View Invoice', $url)
                ->line('Thank you for using our application!');
}
public function toMail($notifiable)
{
    return (new MailMessage)->view(
        'emails.name', ['invoice' => $this->invoice]
    );
}
public function toMail($notifiable)
{
    return (new Mailable($this->invoice))->to($this->user->email);
}
public function toMail($notifiable)
{
    return (new MailMessage)
                ->error()
                ->subject('Notification Subject')
                ->line('...');
}

定制邮件接收者

class User extends Authenticatable
{
    use Notifiable;

    public function routeNotificationForMail()
    {
        return $this->email_address;
    }
}

定制主题

如果不定制主题,会根据通知类的名词默认主题名。如InvoicePaid转化成 Invoice Paid 。

public function toMail($notifiable)
{
    return (new MailMessage)
                ->subject('Notification Subject')
                ->line('...');
}

自定义通知模板

# 执行改命令后,resources/views/vendor/notifications 下面的文件可以根据自己需要进行修改
php artisan vendor:publish --tag=laravel-notifications 

markdown 邮件通知

php artisan make:notification InvoicePaid --markdown=mail.invoice.paid
public function toMail($notifiable)
{
    $url = url('/invoice/'.$this->invoice->id);

    return (new MailMessage)
                ->subject('Invoice Paid')
                ->markdown('mail.invoice.paid', ['url' => $url]);
}

如何定制模板,怎么编写模板中的 markdown,请参考上一节 邮件

数据库通知

php artisan notifications:table
php artisan migrate

定义通知格式

toArray 和 toDatabase 都可以实现。区别是,toArray 还可以作用于broadcast渠道。

public function toArray($notifiable)
{
    return [
        'invoice_id' => $this->invoice->id,
        'amount' => $this->invoice->amount,
    ];
}

访问通知

# use Illuminate\Notifications\Notifiable;
$user = App\User::find(1);

foreach ($user->notifications as $notification) {
    echo $notification->type;
}
$user = App\User::find(1);

foreach ($user->unreadNotifications as $notification) {
    echo $notification->type;
}

标记已读,删除通知

$user = App\User::find(1);

foreach ($user->unreadNotifications as $notification) {
    $notification->markAsRead();
}

$user->unreadNotifications->markAsRead();
$user = App\User::find(1);

$user->unreadNotifications()->update(['read_at' => now()]);
$user->notifications()->delete();

广播通知

use Illuminate\Notifications\Messages\BroadcastMessage;

public function toBroadcast($notifiable)
{
    return new BroadcastMessage([
        'invoice_id' => $this->invoice->id,
        'amount' => $this->invoice->amount,
    ]);
}
return (new BroadcastMessage($data))
                ->onConnection('sqs')
                ->onQueue('broadcasts');

监听通知

Echo.private('App.User.' + userId)
    .notification((notification) => {
        console.log(notification.type);
    });

定制通知渠道

# App\User
public function receivesBroadcastNotificationsOn()
{
    return 'users.'.$this->id;    // 就是替换上面的 'App.User.' + userId
}

sms 通知

slack 通知

通知事件

protected $listen = [
    'Illuminate\Notifications\Events\NotificationSent' => [
        'App\Listeners\LogNotification',
    ],
];
public function handle(NotificationSent $event)
{
    // $event->channel
    // $event->notifiable
    // $event->notification
}

定制渠道

<?php

namespace App\Channels;

use Illuminate\Notifications\Notification;

class VoiceChannel
{

    public function send($notifiable, Notification $notification)
    {
        $message = $notification->toVoice($notifiable);

        // Send notification to the $notifiable instance...
    }
}
<?php

namespace App\Notifications;

use Illuminate\Bus\Queueable;
use App\Channels\VoiceChannel;
use App\Channels\Messages\VoiceMessage;
use Illuminate\Notifications\Notification;
use Illuminate\Contracts\Queue\ShouldQueue;

class InvoicePaid extends Notification
{
    use Queueable;

    public function via($notifiable)
    {
        return [VoiceChannel::class];
    }

    public function toVoice($notifiable)
    {
        // ...
    }
}

九、开发拓展包

当你想要 laravel 内置的测试辅助函数,请安装依赖orchestra/testbench

"extra": {
    "laravel": {
        "providers": [
            "Barryvdh\\Debugbar\\ServiceProvider"
        ],
        "aliases": {
            "Debugbar": "Barryvdh\\Debugbar\\Facade"
        }
    }
},

包自动发现

如果你不想你的用户在配置文件中手动添加你的服务提供者,你可以在依赖声明文件composer.json中的extra部分中添加你的服务提供者。

"extra": {
    "laravel": {
        "providers": [
            "Barryvdh\\Debugbar\\ServiceProvider"
        ],
        "aliases": {
            "Debugbar": "Barryvdh\\Debugbar\\Facade"
        }
    }
},

如果不想被发现

"extra": {
    "laravel": {
        "dont-discover": [
            "barryvdh/laravel-debugbar"
        ]
    }
},

# 所有
"extra": {
    "laravel": {
        "dont-discover": [
            "*"
        ]
    }
},

发布配置文件到 config 目录

public function boot()
{
    $this->publishes([
        __DIR__.'/path/to/config/courier.php' => config_path('courier.php'),
    ]);
}

# php artisan vendor:publish

默认配置
TODO:未能理解与上面的区别。

public function register()
{
    $this->mergeConfigFrom(
        __DIR__.'/path/to/config/courier.php', 'courier'
    );
}

loadRoutesFrom
用来决定你的路由是否被缓存。如果应用缓存了,你的路由将不会被缓存。

public function boot()
{
    $this->loadRoutesFrom(__DIR__.'/routes.php');
}

loadMigrationsFrom

public function boot()
{
    $this->loadMigrationsFrom(__DIR__.'/path/to/migrations');
}

# php artisan migrate

loadTranslationsFrom

public function boot()
{
    $this->loadTranslationsFrom(__DIR__.'/path/to/translations', 'courier');
}

# echo trans('courier::messages.welcome');
# courier 包的 messages 文件的 welcome 选项

发布翻译

public function boot()
{
    $this->loadTranslationsFrom(__DIR__.'/path/to/translations', 'courier');

    $this->publishes([
        __DIR__.'/path/to/translations' => resource_path('lang/vendor/courier'),
    ]);
}

loadViewsFrom

public function boot()
{
    $this->loadViewsFrom(__DIR__.'/path/to/views', 'courier');
}

# Route::get('admin', function () {
#    return view('courier::admin');
# });

使用这个方法,laravel 会在两个地方发布你的模板,一个是你指定的,另一个是在resources/views/vendor/courier,laravel 会首先搜索后者,找不到,找你指定的位置。这么做是为了用户可以更方便的去定制包的视图。

发布翻译

public function boot()
{
    $this->loadViewsFrom(__DIR__.'/path/to/views', 'courier');

    $this->publishes([
        __DIR__.'/path/to/views' => resource_path('views/vendor/courier'),
    ]);
}

注册命令

public function boot()
{
    if ($this->app->runningInConsole()) {
        $this->commands([
            FooCommand::class,
            BarCommand::class,
        ]);
    }
}

assets

public function boot()
{
    $this->publishes([
        __DIR__.'/path/to/assets' => public_path('vendor/courier'),
    ], 'public');     // 我们也会增加一个 public 的资源分类标签,可用于发布与分类关联的资源文件
}
php artisan vendor:publish --tag=public --force    // --force 更新包的时候强行覆盖资源目录

分类发布

public function boot()
{
    $this->publishes([
        __DIR__.'/../config/package.php' => config_path('package.php')
    ], 'config');

    $this->publishes([
        __DIR__.'/../database/migrations/' => database_path('migrations')
    ], 'migrations');
}

# php artisan vendor:publish --tag=config

十、队列

介绍

支持:数据库,redis,Beanstalk,Amazon SQS,synchronous,null。
其中,sync 是用来本地开发用的,会立即执行。null 是用来禁止使用队列的。

// queue 的名字是配置文件中的 queue 的值
Job::dispatch();

// queue 的名字是 email
Job::dispatch()->onQueue('emails');

设置 queue 优先级

php artisan queue:work --queue=high,default    // 队列 hign 比队列 default 的优先级高

队列驱动 database

php artisan queue:table
php artisan migrate

队列驱动 redis

# config/database.php
# 如果是 redis 群组,请在队列名里面包含键哈希标志
'redis' => [
    'driver' => 'redis',
    'connection' => 'default',
    'queue' => '{default}',
    'retry_after' => 90,
],

创建作业

php artisan make:job ProcessPodcast

handle 方法在作业被处理时候调用。

二进制数据应当 base64_encode 后再塞入队列。

分发作业

class PodcastController extends Controller
{
    public function store(Request $request)
    {
        // Create podcast...

        ProcessPodcast::dispatch($podcast);
    }
}

延迟分发

ProcessPodcast::dispatch($podcast)
                ->delay(now()->addMinutes(10));

作业链
withChain 方法中的作业依次执行,一个不成功,后续的不会进行。

ProcessPodcast::withChain([
    new OptimizePodcast,
    new ReleasePodcast
])->dispatch();

分发作业到特定队列

ProcessPodcast::dispatch($podcast)->onQueue('processing');

分发作业到特定队列处理链接

ProcessPodcast::dispatch($podcast)->onConnection('sqs');

指定最大尝试次数为 3 次
php artisan queue:work --tries=3
在作业类里面指定的最大尝试次数优先级更高

# App\Jobs\ProcessPodcast
public $tries = 3;

基于时间的尝试:在多久范围内可以不断尝试

public function retryUntil(){
    return now()->addSeconds(5);
}

# 你也可以在队列事件监听器内去定义该方法。

timeout

php artisan queue:work --timeout=30
public $timeout = 120;

速率控制

这个特性需要你的应用和 redis 服务器互动,多应用于 APIs 。

Redis::throttle('key')->allow(10)->every(60)->then(function () {
    // Job logic...
}, function () {
    // Could not obtain lock...

    return $this->release(10);
});
# key 可以是唯一的字符串,用来指定速率控制的作业类型。
# release ,如果锁不能被获取,你应该 release 到队列中,以便 10s 后再次尝试。

限制一个作业处理器一次性可以做多少个作业

Redis::funnel('key')->limit(1)->then(function () {
    // Job logic...
}, function () {
    // Could not obtain lock...

    return $this->release(10);
});

当使用速率控制时,很难决定作业被成功执行完所需要的次数。因此,使用速率控制和基于时间的尝试相结合将非常的有用。

错误处理

有异常时,会自动释放回队列,在规定次数内不断尝试,直到成功。控制参数就是命令行的参数--tries

运行作业处理器

作业处理器一旦运行,不会关闭,除非你手动关闭它。

php artisan queue:work
# 确保在后台永久的运行中,你可以使用进程监控 Supervisor。

处理一个单一的作业

php artisan queue:work once

指定使用哪个队列连接

php artisan queue:work redis

指定队列

php artisan queue:work redis --emails

守护进程队列在处理每个作业的时候不会重启框架,所以要注意释放资源。例如:在使用 GD 库的时候,记得使用 imagedestroy 释放内存。

队列优先级

php artisan queue:work --queue=high-queue,low-queue

重启队列

php artisan queue:restart
# 重启信号存储与缓存。所以在使用该命令之前记得配置好 cache 。

retry_after

retry_after=90 在作业被执行 90 秒后,依然没有被删除,则会被释放回队列。注意,设置合理的值(你的作业可能被处理的最大合理秒数)。

主队列程序杀死子队列程序

php artisan queue:work --timeout=50

--timeout 的数值应该总是小于 retry_after 。

当没有作业时候,设置休息时间

php artisan queue:work --sleep=2

Supervisor 配置

sudo apt-get install supervisor

如果不会玩这样,可以使用 Laravel Forge (无痛的 php 服务器)。
/etc/supervisor/conf.d目录下创建任意个监控进程配置文件。例如:laravel-worder.conf 监控 queue:work 进程。

[program:laravel-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /home/forge/app.com/artisan queue:work sqs --sleep=3 --tries=3
autostart=true
autorestart=true
user=forge
numprocs=8   # 指示 Supervisor 运行 8 个进程并监控他们,如果失败自动重启
redirect_stderr=true
stdout_logfile=/home/forge/app.com/worker.log

写好配置文件,你可以更新 Supervisor 配置并且重启进程

sudo supervisorctl reread

sudo supervisorctl update

sudo supervisorctl start laravel-worker:*

更多内容查看 Supervisor 文档

处理失败任务

多次尝试后依然失败的任务会存储在表 failed_jobs 中。

php artisan queue:failed-table
php artisan migrate

如果不设置 tries 参数,将会无限期的进行尝试。

failed 方法
常用来作业最终失败后提醒用户或者恢复作业所做的一些操作。传入的参数是作业失败的异常类。

# App\Jobs\ProcessPodcast 
public function failed(Exception $exception)
{

}

失败作业事件

# AppServiceProvider
Queue::failing(function (JobFailed $event) {
    // $event->connectionName
    // $event->job
    // $event->exception
});

重试失败作业

查看失败的作业

php artisan queue:failed

重试 ID 是 5 的作业

php artisan queue:retry 5

重试所有

php artisan queue:retry all

删除作业

php artisan queue:forget 5
php artisan queue:flush # 删除所有

队列事件

  1. before
  2. after
  3. looping 在队列处理器从队列中获取作业之前调用。例如:回滚之前失败作业的事务
# AppServiceProvider 
public function boot()
{
    Queue::before(function (JobProcessing $event) {
        // $event->connectionName
        // $event->job
        // $event->job->payload()
    });

    Queue::after(function (JobProcessed $event) {
        // $event->connectionName
        // $event->job
        // $event->job->payload()
    });

    Queue::looping(function () {      // 回滚之前失败作业的事务
        while (DB::transactionLevel() > 0) {
            DB::rollBack();
        }
    });
}

十一、任务计划

介绍

服务器自带的任务计划不足之处是你必须每次 ssh 上去配置。

# app/Console/Kernel.php
protected function schedule(Schedule $schedule)
{
    // $schedule->command('inspire')
    //          ->hourly();
}

为了使 laravel 的生效,请在服务器中 crontab -e

* * * * * php /path-to-your-project/artisan schedule:run >> /dev/null 2>&1

定义计划执行

# app/Console/Kernel.php     schedule 方法
$schedule->call(function () {
    DB::table('recent_users')->delete();
})->daily();

// 在每天的午夜清空 recent_users 表

计划执行 artisan 命令

$schedule->command('emails:send --force')->daily();

$schedule->command(EmailsCommand::class, ['--force'])->daily();

计划执行队列里的作业

$schedule->job(new Heartbeat)->everyFiveMinutes();

计划执行 shell 命令

$schedule->exec('node /home/forge/script.js')->daily();
Method Description
->cron('* * * * *'); Run the task on a custom Cron schedule
->everyMinute(); Run the task every minute
->everyFiveMinutes(); Run the task every five minutes
->everyTenMinutes(); Run the task every ten minutes
->everyFifteenMinutes(); Run the task every fifteen minutes
->everyThirtyMinutes(); Run the task every thirty minutes
->hourly(); Run the task every hour
->hourlyAt(17); Run the task every hour at 17 mins past the hour
->daily(); Run the task every day at midnight
->dailyAt('13:00'); Run the task every day at 13:00
->twiceDaily(1, 13); Run the task daily at 1:00 & 13:00
->weekly(); Run the task every week
->monthly(); Run the task every month
->monthlyOn(4, '15:00'); Run the task every month on the 4th at 15:00
->quarterly(); Run the task every quarter
->yearly(); Run the task every year
->timezone('America/New_York'); Set the timezone

结合使用,制作更精确的时间

// Run once per week on Monday at 1 PM...
$schedule->call(function () {
    //
})->weekly()->mondays()->at('13:00');

// Run hourly from 8 AM to 5 PM on weekdays...
$schedule->command('foo')
          ->weekdays()
          ->hourly()
          ->timezone('America/Chicago')
          ->between('8:00', '17:00');
Method Description
->weekdays(); Limit the task to weekdays
->sundays(); Limit the task to Sunday
->mondays(); Limit the task to Monday
->tuesdays(); Limit the task to Tuesday
->wednesdays(); Limit the task to Wednesday
->thursdays(); Limit the task to Thursday
->fridays(); Limit the task to Friday
->saturdays(); Limit the task to Saturday
->between($start, $end); Limit the task to run between start and end times
->when(Closure); Limit the task based on a truth test
$schedule->command('reminders:send')
                    ->hourly()
                    ->between('7:00', '22:00');
$schedule->command('reminders:send')
                    ->hourly()
                    ->unlessBetween('23:00', '4:00');
$schedule->command('emails:send')->daily()->when(function () {
    return true;
});
# 和 when 相反
$schedule->command('emails:send')->daily()->skip(function () {
    return true;
});

阻止任务重叠
TODO:没怎么搞懂这是个什么鬼

# 默认情况下,即使任务的前一个实例仍在运行,计划任务也会运行。
$schedule->command('emails:send')->withoutOverlapping();

默认 withoutOverlapping 的过期时间是 24 小时。

$schedule->command('emails:send')->withoutOverlapping(10);

在维护模式时候强制执行计划任务

$schedule->command('emails:send')->evenInMaintenanceMode();

任务输出

$schedule->command('emails:send')
         ->daily()
         ->sendOutputTo($filePath);
$schedule->command('emails:send')
         ->daily()
         ->appendOutputTo($filePath)

将输出邮寄给别人

$schedule->command('foo')
         ->daily()
         ->sendOutputTo($filePath)
         ->emailOutputTo('foo@example.com');

输出只支持 command 方法,不支持 call 方法。

任务钩子

$schedule->command('emails:send')
         ->daily()
         ->before(function () {
             // Task is about to start...
         })
         ->after(function () {
             // Task is complete...
         });
#########################################################################################
#########################################################################################
#########################################################################################
#########################################################################################
#########################################################################################
#########################################################################################

第八章:数据库(一、开始。二、查询构造器。三、分页。四、迁移。五、填充。六、Redis。)

#########################################################################################
#########################################################################################
#########################################################################################
#########################################################################################
#########################################################################################
#########################################################################################

一、开始

介绍

Laravel 支持四种数据库 MySQL,PostgreSQL,SQLite,SQL Server。

配置 sqlite

# touch database/database.sqlite
DB_CONNECTION=sqlite
DB_DATABASE=/absolute/path/to/database.sqlite

读写分离

'mysql' => [
    'read' => [
        'host' => '192.168.1.1',
    ],
    'write' => [
        'host' => '196.168.1.2'
    ],
    'sticky'    => true,   # 该选项作用:使得 read 操作可以和刚刚执行的 write 操作使用同一个连接
    'driver'    => 'mysql',
    'database'  => 'database',
    'username'  => 'root',
    'password'  => '',
    'charset' => 'utf8mb4',
    'collation' => 'utf8mb4_unicode_ci',
    'prefix'    => '',
],

连接不同数据库

$users = DB::connection('foo')->select(...);

获取原生 pdo

$pdo = DB::connection()->getPdo();

没有返回值的语句,请用 statement

DB::statement('drop table users');

监听事件

DB::listen(function ($query) {
    // $query->sql
    // $query->bindings
    // $query->time
});

数据库事务

DB::transaction(function () {
    DB::table('users')->update(['votes' => 1]);

    DB::table('posts')->delete();
});
# 也可以在闭包内使用 Eloquent

处理死锁

DB::transaction(function () {
    DB::table('users')->update(['votes' => 1]);

    DB::table('posts')->delete();
}, 5);    // 五次重新尝试后还是失败,抛出异常

手动使用事务

DB::beginTransaction();

DB::rollBack();

DB::commit();

二、查询构造器

DB::select('select * from users where id = :id', ['id' => 1]);   // 原生语句

DB::table('users')->get();                             // 返回包含对象的集合
DB::table('users')->distinct()->get();
DB::table('users')->select('id', 'age as user_age')->get();
DB::table('users')->where('name', 'John')->first();    // 返回对象
DB::table('users')->where('id', 17)->value('age');     // 返回 17
DB::table('users')->orderBy('id')->value('age');       // 返回 id 最小的 age
DB::table('users')->pluck('age');                      // 返回包含字段值的集合
DB::table('users')->pluck('age', 'id');                // 返回关联集合 id => age,最多 2 个参数
DB::table('users')->count();                           // 返回数字   类似的统计函数支持 (count, max, min, avg, sum)
DB::table('users')->max('id');                         // 返回数字或 null
DB::table('users')->where('class_id', '1')->average('age'); // 返回四位小数的平均值或 null
DB::table('users')->orderBy('id')->chunk(100, function ($users) {  // 取每 100 个一组
    foreach ($users as $user) {
        // ...
        // return false;                                           // 随时可以退出
    }
});                                                     
$query = DB::table('users')->select('name');
$users = $query->addSelect('age')->get();
$users = DB::table('users')
                ->select(DB::raw('count(*) as user_count, status'))
                ->where('status', '<>', 1)
                ->groupBy('status')
                ->get();

// selectRaw 可以传一个参数绑定   selectRaw 等价于 select(DB::raw(...))

DB::table('orders')
                ->selectRaw('price * ? as price_with_tax', [1.0825])
                ->get();
$orders = DB::table('orders')
                ->whereRaw('price > IF(state = "TX", ?, 100)', [200])
                ->get();
$orders = DB::table('orders')
                ->select('department', DB::raw('SUM(price) as total_sales'))
                ->groupBy('department')
                ->havingRaw('SUM(price) > 2500')
                ->get();

$orders = DB::table('orders')
                ->orderByRaw('updated_at - created_at DESC')      // - 只能用于 timestamp 类型        
                ->get();
# inner join
$users = DB::table('users')
            ->join('contacts', 'users.id', '=', 'contacts.user_id')
            ->join('orders', 'users.id', '=', 'orders.user_id')
            ->select('users.*', 'contacts.phone', 'orders.price')
            ->get();

# left join
$users = DB::table('users')
            ->leftJoin('posts', 'users.id', '=', 'posts.user_id')
            ->get();

# cross join
$users = DB::table('sizes')
            ->crossJoin('colours')
            ->get();

# 高级 join
DB::table('users')
        ->join('contacts', function ($join) {
            $join->on('users.id', '=', 'contacts.user_id')->orOn(...);
        })
        ->get();

DB::table('users')
        ->join('contacts', function ($join) {
            $join->on('users.id', '=', 'contacts.user_id')
                 ->where('contacts.user_id', '>', 5);
        })
        ->get();

# union
$first = DB::table('users')
            ->whereNull('first_name');

$users = DB::table('users')
            ->whereNull('last_name')
            ->union($first)
            ->get();

# unionAll 和 union 参数一样
where('votes', '=', 100)
where('votes', 100)
where('votes', '>=', 100)
where('votes', '<>', 100)
where('name', 'like', 'T%')
where([
    ['status', '=', '1'],
    ['subscribed', '<>', '1'],
])
where('votes', '>', 100)->orWhere('name', 'John')
whereBetween('votes', [1, 100])
whereNotBetween('votes', [1, 100])
whereIn('id', [1, 2, 3])
whereNotIn('id', [1, 2, 3])
whereNull('last_name')
whereNotNull('updated_at')
whereDate('created_at', '2016-12-31')
whereMonth('created_at', '12')
whereDay('created_at', '31')
whereYear('created_at', '2016')
whereTime('created_at', '=', '11:20')
whereColumn('first_name', 'last_name')        // 判断两个字段 相等
whereColumn('updated_at', '>', 'created_at')

whereColumn([
    ['first_name', '=', 'last_name'],
    ['updated_at', '>', 'created_at']
])

where('name', '=', 'John')
->orWhere(function ($query) {
    $query->where('votes', '>', 100)
          ->where('title', '<>', 'Admin');
})

whereExists(function ($query) {
    $query->select(DB::raw(1))
          ->from('orders')
          ->whereRaw('orders.user_id = users.id');
})   // where exists ( select 1 from orders where orders.user_id = users.id )
  
# json
$users = DB::table('users')
                ->where('options->language', 'en')
                ->get();

$users = DB::table('users')
                ->where('preferences->dining->meal', 'salad')
                ->get();
orderBy('name', 'desc')

latest()                // === orderBy('created_at', 'desc')

inRandomOrder()

groupBy('account_id')->having('account_id', '>', 100)
skip(10)->take(5)        // === offset(10)->limit(5)
// $role 有值才会执行闭包
$role = $request->input('role');

$users = DB::table('users')
                ->when($role, function ($query) use ($role) {
                    return $query->where('role_id', $role);
                })
                ->get();

// $role 有值执行第一个闭包,否则执行第二个闭包
$sortBy = null;
$users = DB::table('users')
                ->when($sortBy, function ($query) use ($sortBy) {
                    return $query->orderBy($sortBy);
                }, function ($query) {
                    return $query->orderBy('name');
                })
                ->get();

$query = Author::query();
$query->when(request('filter_by') == 'likes', function ($q) {
    return $q->where('likes', '>', request('likes_amount', 0));
});
$query->when(request('filter_by') == 'date', function ($q) {
    return $q->orderBy('created_at', request('ordering_rule', 'desc'));
});

$query = User::query();
$query->when(request('role', false), function ($q, $role) { 
    return $q->where('role_id', $role);
});
$authors = $query->get();
Topic::with('latestPost')->get()->sortByDesc('latestPost.created_at');
function getFullNameAttribute()
{
  return $this->attributes['first_name'] . ' ' . $this->attributes['last_name'];
}

$clients = Client::orderBy('full_name')->get(); // doesn't work

$clients = Client::get()->sortBy('full_name'); // works!

$bool = DB::insert('insert into users (id, name) values (?, ?)', [1, 'Dayle']);    // 原生语句

$bool = DB::table('users')->insert(
    ['email' => 'john@example.com', 'votes' => 0]
);

$bool = DB::table('users')->insert([
    ['email' => 'taylor@example.com', 'votes' => 0],
    ['email' => 'dayle@example.com', 'votes' => 0]
]);

DB::table('users')->insertGetId(
    ['email' => 'john@example.com', 'votes' => 0]
);
# PostgreSQL 的 insertGetId 默认自增字段是 id,如果是其他的,需要传入字段名到 insertGetId 第二个参数。

$numbersOfRowsAffected = DB::update('update users set votes = 100 where name = ?', ['John']);     // 原生

$numbersOfRowsAffected = DB::table('users')
            ->where('id', 1)
            ->update(['votes' => 1]);

DB::table('users')
            ->where('id', 1)
            ->update(['options->enabled' => true]);
DB::table('users')->increment('votes');
DB::table('users')->increment('votes', 5);
DB::table('users')->decrement('votes');
DB::table('users')->decrement('votes', 5);

DB::table('users')->increment('votes', 1, ['name' => 'John']);

$numbersOfRowsAffected = DB::delete('delete from users');    // 原生

$numbersOfRowsAffected = DB::table('users')->delete();
$numbersOfRowsAffected = DB::table('users')->where('votes', '>', 100)->delete();
DB::table('users')->truncate();

共享锁

共享锁可以在事务提交前禁止当前行被修改。

DB::table('users')->where('votes', '>', 100)->sharedLock()->get();

forUpdated 锁可以阻止被修改,或者阻止被另一个共享锁使用。

DB::table('users')->where('votes', '>', 100)->lockForUpdate()->get();

三、分页

    $users = DB::table('users')->paginate(15);      # 默认 15 个元素每页

Laravel 分页暂不支持 groupBy 语句。

只显示上一页,下一页

$users = DB::table('users')->simplePaginate(15);
$users = App\User::paginate(15);

$users = User::where('votes', '>', 100)->paginate(15);

$users = User::where('votes', '>', 100)->simplePaginate(15);
<div class="container">
    @foreach ($users as $user)
        {{ $user->name }}
    @endforeach
</div>

{{ $users->links() }}
// http://example.com/custom/url?page=N
Route::get('users', function () {
    $users = App\User::paginate(15);

    $users->withPath('custom/url');
});
// &sort=votes
{{ $users->appends(['sort' => 'votes'])->links() }}
// #foo
{{ $users->fragment('foo')->links() }}

转成 json

return App\User::paginate();
{
   "total": 50,
   "per_page": 15,
   "current_page": 1,
   "last_page": 4,
   "first_page_url": "http://laravel.app?page=1",
   "last_page_url": "http://laravel.app?page=4",
   "next_page_url": "http://laravel.app?page=2",
   "prev_page_url": null,
   "path": "http://laravel.app",
   "from": 1,
   "to": 15,
   "data":[
        {
            // Result Object
        },
        {
            // Result Object
        }
   ]
}

定制分页视图

{{ $paginator->links('view.name') }}

// Passing data to the view...
{{ $paginator->links('view.name', ['foo' => 'bar']) }}
php artisan vendor:publish --tag=laravel-pagination

更多方法

$results->count()
$results->currentPage()
$results->firstItem()
$results->hasMorePages()
$results->lastItem()
$results->lastPage() (Not available when using simplePaginate)
$results->nextPageUrl()
$results->perPage()
$results->previousPageUrl()
$results->total() (Not available when using simplePaginate)
$results->url($page)

四、迁移

迁移相当于数据库的一个php版版本控制工具。迁移类中的 up 和 down 方法应当实现完全相反的操作。

创建迁移

php artisan make:migration create_users_table

php artisan make:migration create_users_table --create=users     // 等同于上面的

php artisan make:migration add_votes_to_users_table --table=users  
public function up()
{
    Schema::create('flights', function (Blueprint $table) {
        $table->increments('id');
        $table->string('name');
        $table->string('airline');
        $table->timestamps();
    });
}

public function down()
{
    Schema::drop('flights');
}

运行 migration

php artisan migrate 
php artisan migrate --force    // 强制执行,忽略提示

回滚操作

php artisan migrate:rollback   // 回滚到上一版本
php artisan migrate:rollback --step=5    // 回滚到前面五个迁移
php artisan migrate:reset    // 回滚所有
php artisan migrate:refresh   // 回滚并且 migrate
php artisan migrate:refresh --seed   // 回滚并且 migrate 再 seed
php artisan migrate:refresh --step=5    // 回滚五步,在 migrate 五步

直接删除再 migrate

php artisan migrate:fresh

php artisan migrate:fresh --seed

Schema::create('users', function (Blueprint $table) {
    $table->increments('id');
});
if (Schema::hasTable('users')) {
    //
}

if (Schema::hasColumn('users', 'email')) {
    //
}
Schema::connection('foo')->create('users', function (Blueprint $table) {
    $table->increments('id');
});
Command Description
$table->engine = 'InnoDB'; Specify the table storage engine (MySQL).
$table->charset = 'utf8'; Specify a default character set for the table (MySQL).
$table->collation = 'utf8_unicode_ci'; Specify a default collation for the table (MySQL).
$table->temporary(); Create a temporary table (except SQL Server).
Schema::rename($from, $to);

Schema::drop('users');

Schema::dropIfExists('users');

字段

Command Description
$table->bigIncrements('id'); Auto-incrementing UNSIGNED BIGINT (primary key) equivalent column.
$table->bigInteger('votes'); BIGINT equivalent column.
$table->binary('data'); BLOB equivalent column.
$table->boolean('confirmed'); BOOLEAN equivalent column.
$table->char('name', 100); CHAR equivalent column with an optional length.
$table->date('created_at'); DATE equivalent column.
$table->dateTime('created_at'); DATETIME equivalent column.
$table->dateTimeTz('created_at'); DATETIME (with timezone) equivalent column.
$table->decimal('amount', 8, 2); DECIMAL equivalent column with a precision (total digits) and scale (decimal digits).
$table->double('amount', 8, 2); DOUBLE equivalent column with a precision (total digits) and scale (decimal digits).
$table->enum('level', ['easy', 'hard']); ENUM equivalent column.
$table->float('amount', 8, 2); FLOAT equivalent column with a precision (total digits) and scale (decimal digits).
$table->geometry('positions'); GEOMETRY equivalent column.
$table->geometryCollection('positions'); GEOMETRYCOLLECTION equivalent column.
$table->increments('id'); Auto-incrementing UNSIGNED INTEGER (primary key) equivalent column.
$table->integer('votes'); INTEGER equivalent column.
$table->ipAddress('visitor'); IP address equivalent column.
$table->json('options'); JSON equivalent column.
$table->jsonb('options'); JSONB equivalent column.
$table->lineString('positions'); LINESTRING equivalent column.
$table->longText('description'); LONGTEXT equivalent column.
$table->macAddress('device'); MAC address equivalent column.
$table->mediumIncrements('id'); Auto-incrementing UNSIGNED MEDIUMINT (primary key) equivalent column.
$table->mediumInteger('votes'); MEDIUMINT equivalent column.
$table->mediumText('description'); MEDIUMTEXT equivalent column.
$table->morphs('taggable'); Adds taggable_id UNSIGNED BIGINT and taggable_type VARCHAR equivalent columns.
$table->multiLineString('positions'); MULTILINESTRING equivalent column.
$table->multiPoint('positions'); MULTIPOINT equivalent column.
$table->multiPolygon('positions'); MULTIPOLYGON equivalent column.
$table->nullableMorphs('taggable'); Adds nullable versions of morphs() columns.
$table->nullableTimestamps(); Alias of timestamps() method.
$table->point('position'); POINT equivalent column.
$table->polygon('positions'); POLYGON equivalent column.
$table->rememberToken(); Adds a nullable remember_token VARCHAR(100) equivalent column.
$table->smallIncrements('id'); Auto-incrementing UNSIGNED SMALLINT (primary key) equivalent column.
$table->smallInteger('votes'); SMALLINT equivalent column.
$table->softDeletes(); Adds a nullable deleted_at TIMESTAMP equivalent column for soft deletes.
$table->softDeletesTz(); Adds a nullable deleted_at TIMESTAMP (with timezone) equivalent column for soft deletes.
$table->string('name', 100); VARCHAR equivalent column with a optional length.
$table->text('description'); TEXT equivalent column.
$table->time('sunrise'); TIME equivalent column.
$table->timeTz('sunrise'); TIME (with timezone) equivalent column.
$table->timestamp('added_on'); TIMESTAMP equivalent column.
$table->timestampTz('added_on'); TIMESTAMP (with timezone) equivalent column.
$table->timestamps(); Adds nullable created_at and updated_at TIMESTAMP equivalent columns.
$table->timestampsTz(); Adds nullable created_at and updated_at TIMESTAMP (with timezone) equivalent columns.
$table->tinyIncrements('id'); Auto-incrementing UNSIGNED TINYINT (primary key) equivalent column.
$table->tinyInteger('votes'); TINYINT equivalent column.
$table->unsignedBigInteger('votes'); UNSIGNED BIGINT equivalent column.
$table->unsignedDecimal('amount', 8, 2); UNSIGNED DECIMAL equivalent column with a precision (total digits) and scale (decimal digits).
$table->unsignedInteger('votes'); UNSIGNED INTEGER equivalent column.
$table->unsignedMediumInteger('votes'); UNSIGNED MEDIUMINT equivalent column.
$table->unsignedSmallInteger('votes'); UNSIGNED SMALLINT equivalent column.
$table->unsignedTinyInteger('votes'); UNSIGNED TINYINT equivalent column.
$table->uuid('id'); UUID equivalent column.
$table->year('birth_year'); YEAR equivalent column.
Modifier Description
->after('column') Place the column "after" another column (MySQL)
->autoIncrement() Set INTEGER columns as auto-increment (primary key)
->charset('utf8') Specify a character set for the column (MySQL)
->collation('utf8_unicode_ci') Specify a collation for the column (MySQL/SQL Server)
->comment('my comment') Add a comment to a column (MySQL)
->default($value) Specify a "default" value for the column
->first() Place the column "first" in the table (MySQL)
->nullable($value = true) Allows (by default) NULL values to be inserted into the column
->storedAs($expression) Create a stored generated column (MySQL)
->unsigned() Set INTEGER columns as UNSIGNED (MySQL)
->useCurrent() Set TIMESTAMP columns to use CURRENT_TIMESTAMP as default value
->virtualAs($expression) Create a virtual generated column (MySQL)

修改字段需要安装拓展包

composer require doctrine/dbal
Schema::table('users', function (Blueprint $table) {
    $table->string('name', 50)->change();
});

Schema::table('users', function (Blueprint $table) {
    $table->string('name', 50)->nullable()->change();
});

Schema::table('users', function (Blueprint $table) {
    $table->renameColumn('from', 'to');
});    // enum 字段类型不被支持

可以被修改的书写:bigInteger, binary, boolean, date, dateTime, dateTimeTz, decimal, integer, json, longText, mediumText, smallInteger, string, text, time, unsignedBigInteger, unsignedInteger and unsignedSmallInteger

Schema::table('users', function (Blueprint $table) {
    $table->dropColumn('votes');
});

Schema::table('users', function (Blueprint $table) {
    $table->dropColumn(['votes', 'avatar', 'location']);
});

SQLite 不支持在一个迁移内修改或删除多个字段。

Command Description
$table->dropRememberToken(); Drop the remember_token column.
$table->dropSoftDeletes(); Drop the deleted_at column.
$table->dropSoftDeletesTz(); Alias of dropSoftDeletes() method.
$table->dropTimestamps(); Drop the created_at and updated_at columns.
$table->dropTimestampsTz(); Alias of dropTimestamps() method.

索引

$table->string('email')->unique();    // 等同于下面的

$table->string('email');
$table->unique('email');

$table->index(['account_id', 'created_at']);    // 组合索引

$table->unique('email', 'unique_email');    // 自定义索引名称

Command Description
$table->primary('id'); Adds a primary key.
$table->primary(['id', 'parent_id']); Adds composite keys.
$table->unique('email'); Adds a unique index.
$table->index('state'); Adds a plain index.
$table->spatialIndex('location'); Adds a spatial index. (except SQLite)

删除索引,laravel 自动创建索引名称:表名_字段名_索引类型

Schema::table('geo', function (Blueprint $table) {
    $table->dropIndex(['state']); // Drops index 'geo_state_index'
});

外键限制

Schema::table('posts', function (Blueprint $table) {
    $table->integer('user_id')->unsigned();

    $table->foreign('user_id')->references('id')->on('users');
});
$table->foreign('user_id')
      ->references('id')->on('users')
      ->onDelete('cascade');

类似于索引,删除外键

$table->dropForeign('posts_user_id_foreign');

$table->dropForeign(['user_id']);

启用禁用外键限制

Schema::enableForeignKeyConstraints();

Schema::disableForeignKeyConstraints();

五、填充

php artisan make:seeder UserTableSeeder

填充时候,大量赋值保护自动失效。

利用模型工厂

public function run()
{
    factory(App\User::class, 50)->create()->each(function ($u) {
        $u->posts()->save(factory(App\Post::class)->make());
    });
}

调用其他的 seeders

public function run()
{
    $this->call([
        UsersTableSeeder::class,
        PostsTableSeeder::class,
        CommentsTableSeeder::class,
    ]);
}

运行 seeders

写完你的 seeder,你需要composer dump-autoload

php artisan db:seed     // 默认运行 DatabaseSedder 类

php artisan db:seed --class=UsersTableSeeder     // 运行指定类

重建你的数据库(之前又提到过哦)

php artisan migrate:refresh --seed

六、Redis

composer require predis/predis
'redis' => [

    'client' => 'predis',

    'default' => [
        'host' => env('REDIS_HOST', 'localhost'),
        'password' => env('REDIS_PASSWORD', null),
        'port' => env('REDIS_PORT', 6379),
        'database' => 0,
    ],

],

redis 集群

'redis' => [

    'client' => 'predis',

    'clusters' => [
        'default' => [
            [
                'host' => env('REDIS_HOST', 'localhost'),
                'password' => env('REDIS_PASSWORD', null),
                'port' => env('REDIS_PORT', 6379),
                'database' => 0,
            ],
        ],
    ],

],

如果要想用原生的 redis 集群,请指定 options

'redis' => [

    'client' => 'predis',

    'options' => [
        'cluster' => 'redis',
    ],

    'clusters' => [
        // ...
    ],

],

predis

'default' => [
    'host' => env('REDIS_HOST', 'localhost'),
    'password' => env('REDIS_PASSWORD', null),
    'port' => env('REDIS_PORT', 6379),
    'database' => 0,
    'read_write_timeout' => 60,
],

phpredis

'redis' => [

    'client' => 'phpredis',

    // Rest of Redis configuration...
],
'default' => [
    'host' => env('REDIS_HOST', 'localhost'),
    'password' => env('REDIS_PASSWORD', null),
    'port' => env('REDIS_PORT', 6379),
    'database' => 0,
    'read_timeout' => 60,
],

使用

Redis::get('user:profile:'.$id);
Redis::set('name', 'Taylor');
Redis::lrange('names', 5, 10);
Redis::command('lrange', ['name', 5, 10]);

Redis::connection();
Redis::connection('my-connection');

Redis::pipeline(function ($pipe) {
    for ($i = 0; $i < 1000; $i++) {
        $pipe->set("key:$i", $i);
    }
});

publish 和 subscribe

<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use Illuminate\Support\Facades\Redis;

class RedisSubscribe extends Command
{

    protected $signature = 'redis:subscribe';


    protected $description = 'Subscribe to a Redis channel';

    public function handle()
    {
        Redis::subscribe(['test-channel'], function ($message) {
            echo $message;
        });
    }
}

Route::get('publish', function () {
    // Route logic...

    Redis::publish('test-channel', json_encode(['foo' => 'bar']));
});
Redis::psubscribe(['*'], function ($message, $channel) {
    echo $message;
});

Redis::psubscribe(['users.*'], function ($message, $channel) {
    echo $message;
});
#########################################################################################
#########################################################################################
#########################################################################################
#########################################################################################
#########################################################################################
#########################################################################################

第九章:Eloquent ORM(一、入门。二、模型关联。三、集合。四、Mutators。五、API 资源。六、序列化。)

#########################################################################################
#########################################################################################
#########################################################################################
#########################################################################################
#########################################################################################
#########################################################################################

一、入门

定义模型

php artisan make:model User

php artisan make:model User --migration # 创建一个模型,并且创建他的迁移文件
// 等同于
php artisan make:model User -m
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;

class Flight extends Model {
    protected $table = 'my_flights'; // 默认 flights 
    protected $primaryKey = 'student_id'; // 默认 id
    public $incrementing = false; // 当你的主键不是自增或不是int类型
    public $keyType = 'string'; // 当你的主键不是整型
    public $timestamps = false; // 不自动维护created_at 和 updated_at 字段
    # protected $dateFormat = 'U'; // 自定义自己的时间戳格式
    protected $connection = 'connection-name'; // 为模型指定不同的连接

    const CREATED_AT = 'creation_date'; // 自定义用于存储时间戳的字段名
    const UPDATED_AT = 'last_update'; // 同上
}

取模型

$flights = App\Flight::all();
foreach ($flights as $flight) {
    echo $flight->name;
}
# 每个 Eloquent 模型都可以当作一个 查询构造器
$flights = App\Flight::where('active', 1)
               ->orderBy('name', 'desc')
               ->take(10)
               ->get();

分块结果

$flights = $flights->reject(function ($flight) {
    return $flight->cancelled;
});

Flight::chunk(200, function ($flights) {
    foreach ($flights as $flight) {
        //
    }
});

使用游标

cursor 允许你使用游标来遍历数据库数据,一次只执行单个查询。在处理大数据量请求时 cursor 方法可以大幅度减少内存的使用:

foreach (Flight::where('foo', 'bar')->cursor() as $flight) {
    //
}

取回单个模型/集合

$flight = App\Flight::find(1);

$flight = App\Flight::where('active', 1)->first();
$flights = App\Flight::find([1, 2, 3]);

未找到异常

findOrFail 以及 firstOrFail 方法会取回查询的第一个结果。如果没有找到相应结果,则会抛出一个 Illuminate\Database\Eloquent\ModelNotFoundException

$model = App\Flight::findOrFail(1);

$model = App\Flight::where('legs', '>', 100)->firstOrFail();

如果该异常没有被捕获,则会自动返回 HTTP 404 响应给用户,因此当使用这些方法时,你没有必要明确编写检查来返回 404 响应:

Route::get('/api/flights/{id}', function ($id) {
    return App\Flight::findOrFail($id);
});
// 聚合函数返回标量值
$count = App\Flight::where('active', 1)->count();
$max = App\Flight::where('active', 1)->max('price');

添加和更新模型

public function store(Request $request)
{
    // 验证请求...

    $flight = new Flight;

    $flight->name = $request->name;

    $flight->save();
}
# 批量更新
App\Flight::where('active', 1)
          ->where('destination', 'San Diego')
          ->update(['delayed' => 1]);

当通过“Eloquent”批量更新时,saved 和 updated 模型事件将不会被更新后的模型代替。这是因为批量更新时,模型从来没有被取回。

批量赋值

要先在你的模型上定义一个 fillableguarded 属性

protected $fillable = ['name']; # name 可以赋值
$flight = App\Flight::create(['name' => 'Flight 10']);
# 或者已有实例
$flight->fill(['name' => 'Flight 22']);

$guarded 属性应该包含一个你不想要被批量赋值的属性数组

用的时候应该只选择 $fillable$guarded 中的其中一个。

protected $guarded = ['price'];
# 除了 price 所有的属性都可以被批量赋值
# 如果你想让所有的属性都可以被批量赋值,你应该定义 $guarded为空数组。
protected $guarded = [];

firstOrNew 、 firstOrCreate、updateOrCreate

firstOrCreate等价于firstOrNew + save()

$flight = App\Flight::firstOrNew(['name' => 'Flight 10']);     // 如果没找出来就返回实例。但并不存于数据库。
$flight = App\Flight::firstOrCreate(['name' => 'Flight 10']);

// Retrieve flight by name, or create it with the name and delayed attributes...
$flight = App\Flight::firstOrCreate(
    ['name' => 'Flight 10'], ['delayed' => 1]
);

$flight = App\Flight::updateOrCreate(
    ['departure' => 'Oakland', 'destination' => 'San Diego'],
    ['price' => 99]
);
// 如果有从 Oakland 到 San Diego 的航班,设置价格 99,如果没有,那就设置一个 99 元的从 Oakland 到 San Diego 的航班。

删除模型

$flight = App\Flight::find(1);  // 取回模型再删除
$flight->delete();
// 或者
App\Flight::destroy(1);     // 直接删除
App\Flight::destroy([1, 2, 3]);
App\Flight::destroy(1, 2, 3);

$deletedRows = App\Flight::where('active', 0)->delete();

当使用 Eloquent 批量删除语句时,deletingdeleted 模型事件不会在被删除模型实例上触发。因为删除语句执行时,不会检索模型实例。

软删除

use SoftDeletes;
protected $dates = ['deleted_at'];

创建 deleted_at 字段

Schema::table('flights', function ($table) {
    $table->softDeletes();
});

启用软删除的模型时,被软删除的模型将会自动从所有查询结果中排除。

要确认指定的模型实例是否已经被软删除

if ($flight->trashed()) {
     //
}

查询包含被软删除的模型

$flights = App\Flight::withTrashed()
                ->where('account_id', 1)
                ->get();

# withTrashed 方法也可以被用在 关联 查询:
$flight->history()->withTrashed()->get();

只取出软删除数据

$flights = App\Flight::onlyTrashed()
                ->where('airline_id', 1)
                ->get();

恢复软删除的模型

$flight->restore();

App\Flight::withTrashed()
        ->where('airline_id', 1)
        ->restore();

$flight->history()->restore(); // 可以被用在 关联 查询

永久删除模型

// 强制删除单个模型实例...
$flight->forceDelete();

// 强制删除所有相关模型...
$flight->history()->forceDelete();

全局作用域

# 可以自由在  app 文件夹下创建 Scopes 文件夹来存放
<?php
namespace App\Scopes;
use Illuminate\Database\Eloquent\Scope;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Builder;

class AgeScope implements Scope {
    public function apply(Builder $builder, Model $model) {
        return $builder->where('age', '>', 200);
    }
}

# 需要重写给定模型的 boot 方法并使用 addGlobalScope 方法
<?php
namespace App;
use App\Scopes\AgeScope;
use Illuminate\Database\Eloquent\Model;
class User extends Model {
    protected static function boot() {
        parent::boot();

        static::addGlobalScope(new AgeScope);
    }
}
# 添加作用域后,如果使用 User::all() 查询则会生成如下SQL语句:
select * from `users` where `age` > 200

匿名全局作用域(专门用来处理单个模型)

<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Builder;
class User extends Model {
    protected static function boot() {
        parent::boot();

        static::addGlobalScope('age', function(Builder $builder) {
            $builder->where('age', '>', 200);
        });
    }
}

移除全局作用域

# 我们还可以通过以下方式,利用 age 标识符来移除全局作用:
User::withoutGlobalScope('age')->get();

# 移除指定全局作用域
User::withoutGlobalScope(AgeScope::class)->get();

# 移除几个或全部全局作用域
User::withoutGlobalScopes([
    FirstScope::class, 
    SecondScope::class
])->get();

User::withoutGlobalScopes()->get();

本地作用域

<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class User extends Model {

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

    public function scopeActive($query) {
        return $query->where('active', 1);
    }
}
# 在进行方法调用时不需要加上 scope 前缀
$users = App\User::popular()->active()->orderBy('created_at')->get();

动态范围

<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class User extends Model {

    public function scopeOfType($query, $type) {
        return $query->where('type', $type);
    }
}

现在,你可以在范围调用时传递参数:

$users = App\User::ofType('admin')->get();

事件

retrieved, creating, created, updating, updated, 
saving, saved, deleting, deleted,  restoring, restored.

模型被取出的时候触发 retrieved。

当一个新模型被初次保存将会触发 creating 以及 created 事件。如果一个模型已经存在于数据库且调用了 save 方法,将会触发 updatingupdated 事件。在这两种情况下都会触发 savingsaved 事件。

<?php

namespace App;

use App\Events\UserSaved;
use App\Events\UserDeleted;
use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;

class User extends Authenticatable
{
    use Notifiable;

    protected $events = [
        'saved' => UserSaved::class,
        'deleted' => UserDeleted::class,
    ];
}

还可以这么玩

<?php

namespace App\Observers;

use App\User;

class UserObserver
{

    public function created(User $user)
    {
        //
    }

    public function deleting(User $user)
    {
        //
    }
}
<?php

namespace App\Providers;

use App\User;
use App\Observers\UserObserver;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{

    public function boot()
    {
        User::observe(UserObserver::class);
    }


    public function register()
    {
        //
    }
}

二、模型关联

一对一

正向关联
一个 User 模型关联一个 Phone 模型。

# class User extends Model
public function phone() {
    return $this->hasOne('App\Phone');
}

public function phone() {
    # 它会自动假设 Phone 模型拥有 user_id 外键。如果你想要重写这个约定,则可以传入第二个参数到 hasOne 方法里。
    return $this->hasOne('App\Phone', 'foreign_key');
}

public function phone() {
    # Eloquent 假设外键会和上层模型的 id 字段(或者自定义的 $primaryKey)的值相匹配。否则,请
    return $this->hasOne('App\Phone', 'foreign_key', 'local_key');
}
# 允许定义默认模型
            
# 使用
$phone = User::find(1)->phone;  # 当找不到 id=1 的 phone 记录,会查看是否有默认模型,没有则返回 null。
// select * from `by_users` where `by_users`.`id` = '1' limit 1
// select * from `by_phones` where `by_phones`.`user_id` = '1' and `by_phones`.`user_id` is not null limit 1
$email = App\Phone::where('content', '13334233434')->first()->user->email;
// select * from `by_phones` where `content` = '13334233434' limit 1
// select * from `by_users` where `by_users`.`id` = '1' limit 1

反向关联

# class Phone extends Model 
public function user() {
    return $this->belongsTo('App\User');
    // return $this->belongsTo('App\User', 'foreign_key');
    // return $this->belongsTo('App\User', 'foreign_key', 'other_key');
}

// 使用:同正向关联

关联默认模型
belongsTo,haoOne 允许定义默认模型

public function user()
{
    return $this->belongsTo('App\User')->withDefault(); // 空模型
    # 或者
    return $this->belongsTo('App\User')->withDefault([
        'name' => '游客',
    ]);
    # 或者
    return $this->belongsTo('App\User')->withDefault(function ($user) {
        $user->name = '游客';
    });
}

一对多

正向关联
一篇博客文章可能会有无限多个评论。

# class Post extends Model 
public function comments() {
    return $this->hasMany('App\Comment');
    # return $this->hasMany('App\Comment', 'foreign_key');
    # return $this->hasMany('App\Comment', 'foreign_key', 'local_key');
}

# Controller
$comments = App\Post::find(1)->comments;  // 返回满足条件的评论的集合
// select * from `by_posts` where `by_posts`.`id` = '1' limit 1
// select * from `by_comments` where `by_comments`.`post_id` = '1' and `by_comments`.`post_id` is not null
foreach ($comments as $comment) {
    //
}

$comments = App\Post::find(1)->comments()->where('title', 'foo')->first();

$comment = App\Comment::find(1);
echo $comment->post->title;

反向关联

// 同一对一的反向关联 :语义化为 ( 一条评论只属于一篇文章 )
return $this->belongsTo('App\Post');

多对多

正向关联
一个用户可能拥有多种身份,而一种身份能同时被多个用户拥有。

需要使用三个数据表:usersrolesrole_userrole_user 表命名是以相关联的两个模型数据表来依照字母顺序命名,并包含了 user_idrole_id 字段。

# class User extends Model
public function roles() {
    return $this->belongsToMany('App\Role');
}

// 使用:等同于 hasMany
$roles = App\User::find(1)->roles;
// select * from `by_users` where `by_users`.`id` = '1' limit 1
/* 
select 
`by_roles`.*, 
`by_role_user`.`user_id` as `pivot_user_id`, 
`by_role_user`.`role_id` as `pivot_role_id` 
from 
`by_roles` inner join `by_role_user` 
on 
`by_roles`.`id` = `by_role_user`.`role_id` 
where 
`by_role_user`.`user_id` = '1'
*/

如前文提到那样,Eloquent 会合并两个关联模型的名称并依照字母顺序命名。当然你也可以随意重写这个约定。可通过传递第二个参数至 belongsToMany 方法来实现:

return $this->belongsToMany('App\Role', 'role_user');

return $this->belongsToMany('App\Role', 'role_user', 'user_id', 'role_id');

反向关联

<?php
# class Role extends Model 
public function users() {
    return $this->belongsToMany('App\User'); 
}

操作中间表

获取中间表字段

$user = App\User::find(1);

foreach ($user->roles as $role) {
    echo $role->pivot->created_at;
}

# 我们取出的每个 Role 模型对象,都会被自动赋予 pivot 属性。
# 默认情况下,pivot 对象只提供模型的键。
# 如果你的 pivot 数据表包含了其它的属性,则可以在定义关联方法时指定那些字段:

return $this->belongsToMany('App\Role')->withPivot('column1', 'column2');
# 如果你想要中间表自动维护 created_at 和 updated_at 时间戳,
# 可在定义关联方法时加上  withTimestamps 方法:

return $this->belongsToMany('App\Role')->withTimestamps();

自定义 pivot 别名

return $this->belongsToMany('App\Podcast')
                ->as('subscription')
                ->withTimestamps();

$users = User::with('podcasts')->get();

foreach ($users->flatMap->podcasts as $podcast) {
    echo $podcast->subscription->created_at;
}

通过中间表过滤

return $this->belongsToMany('App\Role')->wherePivot('approved', 1);

return $this->belongsToMany('App\Role')->wherePivotIn('priority', [1, 2]);

自定义中间表模型

# 原中间表应该是 RoleUser
public function users()
{
    return $this>belongsToMany('App\User')->using('App\UserRole');
}
namespace App;

use Illuminate\Database\Eloquent\Relations\Pivot;

class UserRole extends Pivot
{
    //
}

远层一对多

一个 Country 模型可能通过中间的 Users 模型关联到多个 Posts 模型。

countries
    id - integer
    name - string

users
    id - integer
    country_id - integer
    name - string

posts
    id - integer
    user_id - integer
    title - string
class Country extends Model
{
    public function posts()
    {
        return $this->hasManyThrough(
            'App\Post',     # 最终要关联的模型
            'App\User',
            // 'country_id',   // Foreign key on users table...
            // 'user_id',      // Foreign key on posts table...
            // 'id',           // Local key on countries table...
            // 'id'            // Local key on users table...
        );
    }
}
$posts = App\Country::find(1)->posts;
// select * from `by_countries` where `by_countries`.`id` = '1' limit 1
// select `by_posts`.*, `by_users`.`country_id` from `by_posts` inner join `by_users` on `by_users`.`id` = `by_posts`.`user_id` where `by_users`.`country_id` = '1'

多态关联

用户可以「评论」文章和视频。
数据库设计

posts
    id - integer
    title - string
    body - text

videos
    id - integer
    title - string
    url - string

comments
    id - integer
    body - text
    commentable_id - integer
    commentable_type - string   // posts or videos

定义关联

namespace App;
use Illuminate\Database\Eloquent\Model;
class Comment extends Model {
    /**
     * 获取所有拥有的 commentable 模型。
     */
    public function commentable() {
        return $this->morphTo();
    }
}

class Post extends Model {
    /**
     * 获取所有文章的评论。
     */
    public function comments() {
        return $this->morphMany('App\Comment', 'commentable');
    }
}

class Video extends Model {
    /**
     * 获取所有视频的评论。
     */
    public function comments(){
        return $this->morphMany('App\Comment', 'commentable');
    }
}

使用

$post = App\Post::find(1);

foreach ($post->comments as $comment) {
    //
}
$comment = App\Comment::find(1);

$commentable = $comment->commentable;
# 返回 Post 或 Video 实例

自定义多态关联的类型字段

use Illuminate\Database\Eloquent\Relations\Relation;

Relation::morphMap([
    'posts' => App\Post::class,
    'videos' => App\Video::class,
]);
# 你需要在你自己的 AppServiceProvider 中的 boot 函数注册这个 morphMap ,或者创建一个独立且满足你要求的服务提供者。

多态多对多关联

数据库设计
博客的 PostVideo 模型可以共用多态关联至 Tag 模型。post 有多个标签,一个标签有多个 post 。

posts
    id - integer
    name - string

videos
    id - integer
    name - string

tags
    id - integer
    name - string

taggables
    tag_id - integer
    taggable_id - integer
    taggable_type - string

正向关联

namespace App;
use Illuminate\Database\Eloquent\Model;
class Post extends Model{
    /**
     * 获取该文章的所有标签。
     */
    public function tags(){
        return $this->morphToMany('App\Tag', 'taggable');
    }
}

反向关联

namespace App;
use Illuminate\Database\Eloquent\Model;
class Tag extends Model{
    /**
     * 获取所有被赋予该标签的文章。
     */
    public function posts(){
        return $this->morphedByMany('App\Post', 'taggable');
    }

    /**
     * 获取所有被赋予该标签的视频。
     */
    public function videos(){
        return $this->morphedByMany('App\Video', 'taggable');
    }
}

使用

$post = App\Post::find(1);
foreach ($post->tags as $tag) {
    //
}
$tag = App\Tag::find(1);
foreach ($tag->videos as $video) {
    //
}

查找关联

查找关联是否存在

// 获取那些至少拥有一条评论的文章...
$posts = App\Post::has('comments')->get();
// select * from `by_posts` where exists (select * from `by_comments` where `by_posts`.`id` = `by_comments`.`post_id`)
$posts = App\Post::doesntHave('comments')->get();
// select * from `by_posts` where not exists (select * from `by_comments` where `by_posts`.`id` = `by_comments`.`post_id`)
// 获取所有至少有三条评论的文章...
$posts = Post::has('comments', '>=', 3)->get();
// select * from `by_posts` where (select count(*) from `by_comments` where `by_posts`.`id` = `by_comments`.`post_id`) > 4
// 获取所有至少有一张卡的手机的用户...
$posts = User::has('phones.cards')->get(); 
// select * from `by_users` where exists (select * from `by_phones` where `by_users`.`id` = `by_phones`.`user_id` and exists (select * from `by_cards` where `by_phones`.`id` = `by_cards`.`phone_id`))
# 如果你想要更高级的用法,则可以使用 whereHas 和 orWhereHas 方法,在 has 查找里设置「where」条件。此方法可以让你增加自定义条件至关联条件中,例如对评论内容进行检查:
//  获取那些至少有一条评论包含 foo 的文章
$posts = Post::whereHas('comments', function ($query) {
    $query->where('content', 'like', 'foo%');
})->get();
// select * from `by_posts` where exists (select * from `by_comments` where `by_posts`.`id` = `by_comments`.`post_id` and `content` like 'foo%')

$posts = Post::whereDoesntHave('comments', function ($query) {
    $query->where('content', 'like', 'foo%');
})->get();

关联数据计数

# 如果你想对关联数据进行计数,请使用 withCount 方法,此方法会在你的结果集中增加一个 {relation}_count 字段
$posts = App\Post::withCount('comments')->get();

foreach ($posts as $post) {
    echo $post->comments_count;
}
$posts = Post::withCount(['votes', 'comments' => function ($query) {
    $query->where('content', 'like', 'foo%');
}])->get();

echo $posts[0]->votes_count;
echo $posts[0]->comments_count;
$posts = Post::withCount([
    'comments',
    'comments as pending_comments_count' => function ($query) {
        $query->where('approved', false);
    }
])->get();

echo $posts[0]->comments_count;

echo $posts[0]->pending_comments_count;

预加载

$books = App\Book::all();

foreach ($books as $book) {
    echo $book->author->name;
}
// 若存在着 25 本书,则循环就会执行 26 次查找:1 次是查找所有书籍,其它 25 次则是在查找每本书的作者。
$books = App\Book::with('author')->get();

foreach ($books as $book) {
    echo $book->author->name;
}
// 对于该操作则只会执行两条 SQL 语句:
# select * from books
# select * from authors where id in (1, 2, 3, 4, 5, ...)
$books = App\Book::with('author', 'publisher')->get();
$books = App\Book::with('author.contacts')->get();
# 预加载所有书籍的作者,及所有作者的个人联系方式
$users = App\Book::with('author:id,name')->get();   

with 这个方法,应该总是包含 id 字段。

预加载条件限制

$users = App\User::with(['posts' => function ($query) {
    $query->where('title', 'like', '%first%');
}])->get();
# 在这个例子里,Eloquent 只会预加载标题包含 first 的文章
$users = App\User::with(['posts' => function ($query) {
    $query->orderBy('created_at', 'desc');
}])->get();

延迟预加载

有时你可能需要在上层模型被获取后才预加载关联。

$books = App\Book::all();

if ($someCondition) {
    $books->load('author', 'publisher');
}
$books->load(['author' => function ($query) {
    $query->orderBy('published_date', 'asc');
}]);
public function format(Book $book)
{
    $book->loadMissing('author');     // 当没有加载时候,加载

    return [
        'name' => $book->name,
        'author' => $book->author->name
    ];
}

插入 & 更新关联模型

save 方法

$comment = new App\Comment(['message' => 'A new comment.']);

$post = App\Post::find(1);

$post->comments()->save($comment);
$post = App\Post::find(1);

$post->comments()->saveMany([
    new App\Comment(['message' => 'A new comment.']),
    new App\Comment(['message' => 'Another comment.']),
]);

create 方法

$post = App\Post::find(1);

$comment = $post->comments()->create([
    'message' => 'A new comment.',
]);

// create 多个
$comment = $post->comments()->create(
[
    'message' => 'A new comment.',
],[
    'message' => 'Another new comment.',
]
);

save 允许传入一个完整的 Eloquent 模型实例,但 create 只允许传入原始的 PHP 数组。

你需要先在你的模型上定义一个 fillableguarded 属性,因为所有的 Eloquent 模型都针对批量赋值(Mass-Assignment)做了保护。

更新「从属」关联

$account = App\Account::find(10);

$user->account()->associate($account);

$user->save();

删除一个 belongsTo 关联时,使用 dissociate 方法会置该关联的外键为空 (null) 。

$user->account()->dissociate();

$user->save();

多对多关联

一个用户可以拥有多个身份,且每个身份都可以被多个用户拥有。

附加一个规则至一个用户,并连接模型以及将记录写入至中间表,则可以使用 attach 方法:

$user = App\User::find(1);

$user->roles()->attach($roleId);

也可以传递一个需被写入至中间表的额外数据数组:

$user->roles()->attach($roleId, ['expires' => $expires]);
// 移除用户身上某一身份...
$user->roles()->detach($roleId);

// 移除用户身上所有身份...
$user->roles()->detach();
# 为了方便,attach 与 detach 都允许传入 ID 数组:
$user = App\User::find(1);

$user->roles()->detach([1, 2, 3]);

$user->roles()->attach([1 => ['expires' => $expires], 2, 3]);

同步关联

# sync 方法可以用数组形式的 IDs 插入中间的数据表。任何一个不存在于给定数组的 IDs 将会在中间表内被删除。操作完成之后,只有那些在给定数组内的 IDs 会被保留在中间表中。
$user->roles()->sync([1, 2, 3]);
$user->roles()->sync([1 => ['expires' => true], 2, 3]);

如果你不想删除现有的 IDs ,你可以 syncWithoutDetaching 方法:

$user->roles()->syncWithoutDetaching([1, 2, 3]);

切换关联

// 存在就删除,不存在就创建
$user->roles()->toggle([1, 2, 3]);

在中间表上保存额外数据

App\User::find(1)->roles()->save($role, ['expires' => $expires]);

更新中间表记录

# 这个方法接收中间记录的外键和属性数组进行更新
$user = App\User::find(1);

$user->roles()->updateExistingPivot($roleId, $attributes);

连动父级时间戳

当一个模型 belongsTobelongsToMany 另一个模型时,像是一个 Comment 属于一个 Post。这对于子级模型被更新时,要更新父级的时间戳相当有帮助。举例来说,当一个 Comment 模型被更新时,你可能想要「连动」更新 Post 所属的 updated_at 时间戳。

<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Comment extends Model{
    /**
     * 所有的关联将会被连动。
     *
     * @var array
     */
    protected $touches = ['post'];

    /**
     * 获取拥有此评论的文章。
     */
    public function post(){
        return $this->belongsTo('App\Post');
    }
}
$comment = App\Comment::find(1);
$comment->text = 'Edit to this comment!';
$comment->save();

三、集合

大多数 eloquent 集合方法都返回一个 eloquent 集合。pluck, keys, zip, collapse, flatten, flip方法返回基础集合。map操作不包含 eloquent 模型数据时,返回基础集合。

集合方法列表
https://laravel.com/docs/5.5/eloquent-collections#available-methods
all
average
avg
chunk
collapse
combine
concat
contains
containsStrict
count
crossJoin
dd
diff
diffAssoc
diffKeys
dump
each
eachSpread
every
except
filter
first
firstWhere
flatMap
flatten
flip
forget
forPage
get
groupBy
has
implode
intersect
intersectByKeys
isEmpty
isNotEmpty
keyBy
keys
last
macro
make
map
mapInto
mapSpread
mapToGroups
mapWithKeys
max
median
merge
min
mode
nth
only
pad
partition
pipe
pluck
pop
prepend
pull
push
put
random
reduce
reject
reverse
search
shift
shuffle
slice
sort
sortBy
sortByDesc
sortKeys
sortKeysDesc
splice
split
sum
take
tap
times
toArray
toJson
transform
union
unique
uniqueStrict
unless
unwrap
values
when
where
whereStrict
whereIn
whereInStrict
whereInstanceOf
whereNotIn
whereNotInStrict
wrap
zip

自定义集合

<?php
namespace App;

use App\CustomCollection;
use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    public function newCollection(array $models = [])
    {
        return new CustomCollection($models);
    }
}

四、Mutators

Accessor 和 Mutators 允许你在取得或者设置模型实例时格式化 eloquent 属性值。

class User extends Model
{
    public function getFirstNameAttribute($value)
    {
        return ucfirst($value);
    }
}

# $user = App\User::find(1);
# $firstName = $user->first_name;
public function getFullNameAttribute()
{
    return "{$this->first_name} {$this->last_name}";
}
class User extends Model
{
    public function setFirstNameAttribute($value)
    {
        $this->attributes['first_name'] = strtolower($value);
    }
}

# $user = App\User::find(1);
# $user->first_name = 'Sally';

日期 Mutators

protected $dates = [
     'created_at',
     'updated_at',
     'deleted_at'
];
$user = App\User::find(1);
$user->deleted_at = now();
$user->save();

$user = App\User::find(1);
return $user->deleted_at->getTimestamp();

属性类型强制转换
支持的类型:integer, real, float, double, string, boolean, object, array, collection, date, datetime, timestamp

class User extends Model
{
    protected $casts = [
        'is_admin' => 'boolean',
    ];
}
$user = App\User::find(1);

if ($user->is_admin) {     // bool
    //
}
class User extends Model
{
    protected $casts = [
        'options' => 'array',
    ];
}
$user = App\User::find(1);

$options = $user->options;    // 自动从 json 类型转化成数组

$options['key'] = 'value';

$user->options = $options;

$user->save();

五、API 资源

生产资源

php artisan make:resource UserResource

php artisan make:resource Users  --collection
php artisan make:resource UserCollection  // 同上

核心概念

resource

<?php
namespace App\Http\Resources;

use Illuminate\Http\Resources\Json\Resource;

class UserResource extends Resource
{
    public function toArray($request)
    {
        return [
            'id' => $this->id,
            'name' => $this->name,
            'email' => $this->email,
            'created_at' => $this->created_at,
            'updated_at' => $this->updated_at,
        ];
    }
}
use App\User;
use App\Http\Resources\UserResource;

Route::get('/user', function () {
    return new UserResource(User::find(1));
});

resourceCollection

use App\User;
use App\Http\Resources\UserResource;

Route::get('/user', function () {
    return UserResource::collection(User::all());
});

如果需要定制返回集合的内容

php artisan make:resource UserCollection
<?php
namespace App\Http\Resources;

use Illuminate\Http\Resources\Json\ResourceCollection;

class UserCollection extends ResourceCollection
{
    public function toArray($request)
    {
        return [
            'data' => $this->collection,
            'links' => [
                'self' => 'link-value',
            ],
        ];
    }
}
use App\User;
use App\Http\Resources\UserCollection;

Route::get('/users', function () {
    return new UserCollection(User::all());
});

编写资源

最简单的用法就是上面 resource 部分。
接下来看个关系资源

public function toArray($request)
{
    return [
        'id' => $this->id,
        'name' => $this->name,
        'email' => $this->email,
        'posts' => Post::collection($this->posts),
        'created_at' => $this->created_at,
        'updated_at' => $this->updated_at,
    ];
}

data 包裹
默认资源响应转化成 json 会被 data 键包裹。

{
    "data": [
        {
            "id": 1,
            "name": "Eladio Schroeder Sr.",
            "email": "therese28@example.com",
        },
        {
            "id": 2,
            "name": "Liliana Mayert",
            "email": "evandervort@example.com",
        }
    ]
}

禁止包裹

# AppServiceProvider
public function boot()
{
    Resource::withoutWrapping();
}

withoutWrapping 不会处理你手动添加到资源集合中的包裹。

包裹嵌套资源

class CommentsCollection extends ResourceCollection
{
    public function toArray($request)
    {
        return ['data' => $this->collection];
    }
}

数据嵌套和分页
当在资源响应返回分页集合的时候,laravel 会忽略 withoutWrapping 方法的效果,给你的资源数据加上 data 键。

{
    "data": [
        {
            "id": 1,
            "name": "Eladio Schroeder Sr.",
            "email": "therese28@example.com",
        },
        {
            "id": 2,
            "name": "Liliana Mayert",
            "email": "evandervort@example.com",
        }
    ],
    "links":{
        "first": "http://example.com/pagination?page=1",
        "last": "http://example.com/pagination?page=1",
        "prev": null,
        "next": null
    },
    "meta":{
        "current_page": 1,
        "from": 1,
        "last_page": 1,
        "path": "http://example.com/pagination",
        "per_page": 15,
        "to": 10,
        "total": 10
    }
}

分页

use App\User;
use App\Http\Resources\UserCollection;

Route::get('/users', function () {
    return new UserCollection(User::paginate());
});

条件属性

public function toArray($request)
{
    return [
        'id' => $this->id,
        'name' => $this->name,
        'email' => $this->email,
        'secret' => $this->when($this->isAdmin(), 'secret-value'),
        'created_at' => $this->created_at,
        'updated_at' => $this->updated_at,
    ];
}

'secret' => $this->when($this->isAdmin(), function () {
    return 'secret-value';
}),

多个属性取决于某个条件

public function toArray($request)
{
    return [
        'id' => $this->id,
        'name' => $this->name,
        'email' => $this->email,
        $this->mergeWhen($this->isAdmin(), [
            'first-secret' => 'value',
            'second-secret' => 'value',
        ]),
        'created_at' => $this->created_at,
        'updated_at' => $this->updated_at,
    ];
}

mergeWhen 里的数组不能又包含字符串作为索引,又包含数字作为索引。也不能存在乱序的数字索引。

条件关联
whenLoaded 的参数是关系的名称,而不是关系本身。避免了 n+1 的问题。

public function toArray($request)
{
    return [
        'id' => $this->id,
        'name' => $this->name,
        'email' => $this->email,
        'posts' => Post::collection($this->whenLoaded('posts')),
        # 如果没有加载 posts,响应将移除 posts 键
        'created_at' => $this->created_at,
        'updated_at' => $this->updated_at,
    ];
}

中间表数据

return [
    'id' => $this->id,
    'name' => $this->name,
    'expires_at' => $this->whenPivotLoaded('role_users', function () {
        return $this->pivot->expires_at;
    }),
];

添加 meta 数据

return [
    'data' => $this->collection,
    'links' => [
        'self' => 'link-value',
    ],
];

顶层 meta 数据

# UserCollection 
public function toArray($request)
{
    return parent::toArray($request);
}


public function with($request)
{
    return [
        'meta' => [
            'key' => 'value',
        ],
    ];
}

添加顶层 meta 数据(控制器、路由中)

return (new UserCollection(User::all()->load('roles')))
            ->additional(['meta' => [
                'key' => 'value',
            ]]);

资源响应

如之前提到的,可以在路由和控制器中直接返回资源。
可以通过 reponse 自定义响应

Route::get('/user', function () {
    return (new UserResource(User::find(1)))
                ->response()
                ->header('X-Value', 'True');
});

还可以在 UserResource 中统一自定义响应

public function toArray($request)
{
    return [
        'id' => $this->id,
    ];
}

public function withResponse($request, $response)
{
    $response->header('X-Value', 'True');
}

六、序列化

序列化模型和集合

toArray 是递归的转化为数组的,即使里面又关联模型数据。

$user = App\User::with('roles')->first();
return $user->toArray();

toJson 也是递归的

$user = App\User::find(1);
return $user->toJson();

隐式调用 toJson

$user = App\User::find(1);
return (string) $user;
# 或者直接 return
return App\User::all();

在 json 中隐藏属性

class User extends Model
{
    protected $hidden = ['password'];     # 隐藏关联模型时,使用关联模型的方法名
}
class User extends Model
{
    protected $visible = ['first_name', 'last_name'];
}

临时修改属性的可见性

return $user->makeVisible('attribute')->toArray();
return $user->makeHidden('attribute')->toArray();

在 json 中添加值

class User extends Model
{
    public function getIsAdminAttribute()
    {
        return $this->attributes['admin'] == 'yes';
    }
}

# 还需要添加属性 $appends
class User extends Model
{
    protected $appends = ['is_admin'];
}

实时添加

return $user->append('is_admin')->toArray();

return $user->setAppends(['is_admin'])->toArray();    // 覆盖 appends 数组

日期序列化

# AppServiceProvider 
public function boot()
{
    Carbon::serializeUsing(function ($carbon) {
        return $carbon->format('U');
    });
}
#########################################################################################
#########################################################################################
#########################################################################################
#########################################################################################
#########################################################################################
#########################################################################################

第九章:测试(一、入门。二、HTTP 测试。三、浏览器测试。四、数据库。五、模拟。)

#########################################################################################
#########################################################################################
#########################################################################################
#########################################################################################
#########################################################################################
#########################################################################################

一、入门

tests 目录包含两个目录:Feature 和 Unit 。Unit 主要是针对简单的、独立的代码,大多数时候是一个单独的方法。Feature 针对的是多一点的代码,包括几个对象交互甚至是整个 http 请求到 json 终端。

// Create a test in the Feature directory...
php artisan make:test UserTest

// Create a test in the Unit directory...
php artisan make:test UserTest --unit

如果你定义自己的 setUp 方法,记得调用 parent::setUp() 。

二、HTTP 测试

简单介绍

public function testBasicExample()
{
    $response = $this->withHeaders([
        'X-Header' => 'Value',
    ])->json('POST', '/user', ['name' => 'Sally']);

    $response
        ->assertStatus(200)
        ->assertJson([
            'created' => true,
        ]);
}

Session 和 认证

$response = $this->withSession(['foo' => 'bar'])
                         ->get('/');
    public function testApplication()
    {
        $user = factory(User::class)->create();

        $response = $this->actingAs($user)
                         ->withSession(['foo' => 'bar'])
                         ->get('/');
    }
$this->actingAs($user, 'api')
$response = $this->json('POST', '/user', ['name' => 'Sally']);

$response
    ->assertStatus(200)
    ->assertJson([
        'created' => true,
    ]);

assertJson 方法会将响应转换为数组并且利用 PHPUnit::assertArraySubset 方法来验证传入的数组是否在应用返回的 JSON 中。也就是说,即使有其它的属性存在于该 JSON 响应中,但是只要指定的片段存在,此测试仍然会通过。

数组与返回的 JSON 完全匹配,使用 assertExactJson 方法。

$response = $this->json('POST', '/user', ['name' => 'Sally']);

$response
    ->assertStatus(200)
    ->assertExactJson([
        'created' => true,
    ]);

测试文件上传

Storage::fake('avatars');

$response = $this->json('POST', '/avatar', [
    'avatar' => UploadedFile::fake()->image('avatar.jpg')
]);

// Assert the file was stored...
Storage::disk('avatars')->assertExists('avatar.jpg');

// Assert a file does not exist...
Storage::disk('avatars')->assertMissing('missing.jpg');
UploadedFile::fake()->image('avatar.jpg', $width, $height)->size(100);

UploadedFile::fake()->create('document.pdf', $sizeInKilobytes);

assert 相关方法

assert 方法官方文档
https://laravel.com/docs/5.5/http-tests#available-assertions
assertCookie
assertCookieExpired
assertCookieMissing
assertDontSee
assertDontSeeText
assertExactJson
assertHeader
assertHeaderMissing
assertJson
assertJsonFragment
assertJsonMissing
assertJsonMissingExact
assertJsonStructure
assertJsonValidationErrors
assertPlainCookie
assertRedirect
assertSee
assertSeeText
assertSessionHas
assertSessionHasAll
assertSessionHasErrors
assertSessionHasErrorsIn
assertSessionMissing
assertStatus
assertSuccessful
assertViewHas
assertViewHasAll
assertViewIs
assertViewMissing

三、浏览器测试

介绍

Laravel Dusk 只需要使用一个单独的 ChromeDriver,不再需要在你的机器中安装 JDK 或者 Selenium。不过,依然可以按照你自己的需要安装其他 Selenium 兼容的驱动引擎。

安装

composer require --dev laravel/dusk:"^2.0"

不要在生产环境中注册 Dusk 。

php artisan dusk:install

设置 .env 中的 APP_URL

APP_URL=http://my-app.com

运行测试

php artisan dusk    // 接收任何 phpunit 可以接受的参数

使用其他的浏览器

# tests/DuskTestCase.php
# 删除 startChromeDriver 方法
public static function prepare()
{
    // static::startChromeDriver();
}

修改 driver 方法来连接到你指定的 URL 和端口。同时,你要修改传递给 WebDriver 的「desired capabilities」:

protected function driver()
{
    return RemoteWebDriver::create(
        'http://localhost:4444/wd/hub', DesiredCapabilities::phantomjs()
    );
}

入门

php artisan dusk:make LoginTest

php artisan dusk    // 运行测试

Dusk 会自动启动 ChromeDriver ,有些系统不能正常启动,你需要重新启动。你需要做如下操作:

# 注释这行
public static function prepare()
{
    // static::startChromeDriver();
}

如果你的端口不是 9515

protected function driver()
{
    return RemoteWebDriver::create(
        'http://localhost:9515', DesiredCapabilities::chrome()
    );
}

环境处理
在你项目的根目录创建 .env.dusk.{environment} 文件来强制 Dusk 使用自己的的环境文件来运行测试。简单来说,如果你想要以 local 环境来运行 dusk 命令,你需要创建一个 .env.dusk.local 文件。

运行测试的时候,Dusk 会备份你的 .env 文件,然后重命名你的 Dusk 环境文件为 .env。一旦测试结束之后,将会恢复你的 .env 文件。

创建浏览器

<?php

namespace Tests\Browser;

use App\User;
use Tests\DuskTestCase;
use Laravel\Dusk\Chrome;
use Illuminate\Foundation\Testing\DatabaseMigrations;

class ExampleTest extends DuskTestCase
{
    use DatabaseMigrations;

    public function testBasicExample()
    {
        $user = factory(User::class)->create([
            'email' => 'taylor@laravel.com',
        ]);

        $this->browse(function ($browser) use ($user) {
            $browser->visit('/login')
                    ->type('email', $user->email)
                    ->type('password', 'secret')
                    ->press('Login')
                    ->assertPathIs('/home');
        });
    }
}

多浏览器

$this->browse(function ($first, $second) {
    $first->loginAs(User::find(1))
          ->visit('/home')
          ->waitForText('Message');

    $second->loginAs(User::find(2))
           ->visit('/home')
           ->waitForText('Message')
           ->type('message', 'Hey Taylor')
           ->press('Send');

    $first->waitForText('Hey Taylor')
          ->assertSee('Jeffrey Way');
});

调整浏览器窗口大小

$browser->resize(1920, 1080);
$browser->maximize();

认证

$this->browse(function ($first, $second) {
    $first->loginAs(User::find(1))
          ->visit('/home');
});

使用 loginAs 方法后, 该用户的 session 将会持久化供改文件其他测试用例使用。

当你想用迁移,你应该用 DatabaseMigrations,而不是 RefreshDatabase 。RefreshDatabase 无法跨 http 请求。

class ExampleTest extends DuskTestCase
{
    use DatabaseMigrations;
}

与元素互动

<button>Login</button>

// Test...
$browser->click('.login-page .container div > button');

上面这种测试非常不好,因为前段变化的概率较大,变化了,你的测试就没用了,取而代之的是下面这种测试方法。

// HTML...
<button dusk="login-button">Login</button>

// Test...
$browser->click('@login-button');

点击链接

$browser->clickLink($linkText);

这个方法用到了 jquery,如果你的文件中没用到 jquery,dusk 会自动注入 jquery。

文本、值、属性

// Retrieve the value...
$value = $browser->value('selector');

// Set the value...
$browser->value('selector', 'value');
$text = $browser->text('selector');
$attribute = $browser->attribute('selector', 'value');

表单相关操作

$browser->type('email', 'taylor@laravel.com');

注意:虽然 type 方法可以传递 CSS 选择器作为第一个参数,但这并不是强制要求。如果传入的不是 CSS 选择器,Dusk 会尝试匹配传入值与 name 属性相符的 input 框,如果没找到,最后 Dusk 会尝试查找匹配传入值与 name 属性相符的 textarea。

添加更多的值

$browser->type('tags', 'foo')
    ->append('tags', ', bar, baz');

清除值

$browser->clear('email');

selector 传入的应该是 option 的值

$browser->select('size', 'Large');

随机传值,那就忽略第二个参数

 $browser->select('size');

check

$browser->check('terms');

$browser->uncheck('terms');

附件

$browser->attach('photo', __DIR__.'/photos/me.png');

键盘

$browser->keys('selector', ['{shift}', 'taylor'], 'otwell');
// 数组代表一起按,上面是按住 shift 键,然后输入 taylor
$browser->keys('.app', ['{command}', 'j']);

热键列表:facebook/php-webdriver

鼠标

$browser->click('.selector');
$browser->mouseover('.selector');
$browser->drag('.from-selector', '.to-selector');
$browser->dragLeft('.selector', 10);
$browser->dragRight('.selector', 10);
$browser->dragUp('.selector', 10);
$browser->dragDown('.selector', 10);

范围指定

$browser->with('.table', function ($table) {
    $table->assertSee('Hello World')
          ->clickLink('Delete');
});

等待

$browser->pause(1000);    // 等待整个 test 1000 秒
# 时间一到,抛出异常

// Wait a maximum of five seconds for the selector...
$browser->waitFor('.selector');

// Wait a maximum of one second for the selector...
$browser->waitFor('.selector', 1);

$browser->waitUntilMissing('.selector');

$browser->waitUntilMissing('.selector', 1);
# 等待到可用
$browser->whenAvailable('.modal', function ($modal) {
    $modal->assertSee('Hello World')
          ->press('OK');
});
// Wait a maximum of five seconds for the text...
$browser->waitForText('Hello World');

// Wait a maximum of one second for the text...
$browser->waitForText('Hello World', 1);
// Wait a maximum of five seconds for the link text...
$browser->waitForLink('Create');

// Wait a maximum of one second for the link text...
$browser->waitForLink('Create', 1);
$browser->waitForLocation('/secret');
$browser->click('.some-action')
        ->waitForReload()
        ->assertSee('something');
# 等待 js 表达式
// Wait a maximum of five seconds for the expression to be true...
$browser->waitUntil('App.dataLoaded');

$browser->waitUntil('App.data.servers.length > 0');

// Wait a maximum of one second for the expression to be true...
$browser->waitUntil('App.data.servers.length > 0', 1);
# Dusk 中的许多「等待」方法依赖于 waitUsing 方法。该方法可以等待一个回调返回 true。waitUsing 接受的参数为最大等待秒数、闭包的执行间隔、闭包以及一个可选的错误信息。
$browser->waitUsing(10, 1, function () use ($something) {
    return $something->isReady();
}, "Something wasn't ready in time.");

assert Vue

// HTML...

<profile dusk="profile-component"></profile>

// Component Definition...

Vue.component('profile', {
    template: '<div>{{ user.name }}</div>',

    data: function () {
        return {
            user: {
              name: 'Taylor'
            }
        };
    }
});
public function testVue()
{
    $this->browse(function (Browser $browser) {
        $browser->visit('/')
                ->assertVue('user.name', 'Taylor', '@profile-component');
    });
}

assert 相关方法

Assertion Description
$browser->assertTitle($title) Assert the page title matches the given text.
$browser->assertTitleContains($title) Assert the page title contains the given text.
$browser->assertUrlIs($url) Assert that the current URL (without the query string) matches the given string.
$browser->assertPathBeginsWith($path) Assert that the current URL path begins with given path.
$browser->assertPathIs('/home') Assert the current path matches the given path.
$browser->assertPathIsNot('/home') Assert the current path does not match the given path.
$browser->assertRouteIs($name, $parameters) Assert the current URL matches the given named route's URL.
$browser->assertQueryStringHas($name, $value) Assert the given query string parameter is present and has a given value.
$browser->assertQueryStringMissing($name) Assert the given query string parameter is missing.
$browser->assertHasQueryStringParameter($name) Assert that the given query string parameter is present.
$browser->assertHasCookie($name) Assert the given cookie is present.
$browser->assertCookieMissing($name) Assert that the given cookie is not present.
$browser->assertCookieValue($name, $value) Assert a cookie has a given value.
$browser->assertPlainCookieValue($name, $value) Assert an unencrypted cookie has a given value.
$browser->assertSee($text) Assert the given text is present on the page.
$browser->assertDontSee($text) Assert the given text is not present on the page.
$browser->assertSeeIn($selector, $text) Assert the given text is present within the selector.
$browser->assertDontSeeIn($selector, $text) Assert the given text is not present within the selector.
$browser->assertSourceHas($code) Assert that the given source code is present on the page.
$browser->assertSourceMissing($code) Assert that the given source code is not present on the page.
$browser->assertSeeLink($linkText) Assert the given link is present on the page.
$browser->assertDontSeeLink($linkText) Assert the given link is not present on the page.
$browser->assertInputValue($field, $value) Assert the given input field has the given value.
$browser->assertInputValueIsNot($field, $value) Assert the given input field does not have the given value.
$browser->assertChecked($field) Assert the given checkbox is checked.
$browser->assertNotChecked($field) Assert the given checkbox is not checked.
$browser->assertRadioSelected($field, $value) Assert the given radio field is selected.
$browser->assertRadioNotSelected($field, $value) Assert the given radio field is not selected.
$browser->assertSelected($field, $value) Assert the given dropdown has the given value selected.
$browser->assertNotSelected($field, $value) Assert the given dropdown does not have the given value selected.
$browser->assertSelectHasOptions($field, $values) Assert that the given array of values are available to be selected.
$browser->assertSelectMissingOptions($field, $values) Assert that the given array of values are not available to be selected.
$browser->assertSelectHasOption($field, $value) Assert that the given value is available to be selected on the given field.
$browser->assertValue($selector, $value) Assert the element matching the given selector has the given value.
$browser->assertVisible($selector) Assert the element matching the given selector is visible.
$browser->assertMissing($selector) Assert the element matching the given selector is not visible.
$browser->assertDialogOpened($message) Assert that a JavaScript dialog with given message has been opened.
$browser->assertVue($property, $value, $component) Assert that a given Vue component data property matches the given value.
$browser->assertVueIsNot($property, $value, $component) Assert that a given Vue component data property does not match the given value.

页面

php artisan dusk:page Login
public function url()
{
    return '/login';
}

public function assert(Browser $browser)   // 不是必须的
{
    $browser->assertPathIs($this->url());
}


use Tests\Browser\Pages\Login;

$browser->visit(new Login);
use Tests\Browser\Pages\CreatePlaylist;

$browser->visit('/dashboard')
        ->clickLink('Create Playlist')
        ->on(new CreatePlaylist)
        ->assertSee('@create');

快捷方式

public function elements()
{
    return [
        '@email' => 'input[name=email]',
    ];
} 

# $browser->type('@email', 'taylor@laravel.com');

全局快捷方式

# Class Page
public static function siteElements()
{
    return [
        '@element' => '#selector',
    ];
}

页面方法

public function createPlaylist(Browser $browser, $name)
{
    $browser->type('name', $name)
            ->check('share')
            ->press('Create Playlist');
}
use Tests\Browser\Pages\Dashboard;

$browser->visit(new Dashboard)
        ->createPlaylist('My Playlist')
        ->assertSee('My Playlist');

组件

生成组件

php artisan dusk:component DatePicker
<?php

namespace Tests\Browser\Components;

use Laravel\Dusk\Browser;
use Laravel\Dusk\Component as BaseComponent;

class DatePicker extends BaseComponent
{

    public function selector()
    {
        return '.date-picker';
    }

    /**
     * Assert that the browser page contains the component.
     *
     * @param  Browser  $browser
     * @return void
     */
    public function assert(Browser $browser)
    {
        $browser->assertVisible($this->selector());
    }

    public function elements()
    {
        return [
            '@date-field' => 'input.datepicker-input',
            '@month-list' => 'div > div.datepicker-months',
            '@day-list' => 'div > div.datepicker-days',
        ];
    }

    public function selectDate($browser, $month, $year)
    {
        $browser->click('@date-field')
                ->within('@month-list', function ($browser) use ($month) {
                    $browser->click($month);
                })
                ->within('@day-list', function ($browser) use ($day) {
                    $browser->click($day);
                });
    }
}

使用 component

public function testBasicExample()
{
    $this->browse(function (Browser $browser) {
        $browser->visit('/')
                ->within(new DatePicker, function ($browser) {
                    $browser->selectDate(1, 2018);
                })
                ->assertSee('January');
    });
}

连续集成

Travis CI
在 Travis CI 中运行 Dusk 时需要「sudo-enabled」的 Ubuntu 14.04 (Trusty) 环境。由于 Travis CI 不是图形环境,我们需要一些额外的步骤去启动 Chrome 浏览器,另外,我们需要使用 php artisan serve 命令去启动 PHP 的内置服务器。

sudo: required
dist: trusty

addons:
   chrome: stable

install:
   - cp .env.testing .env
   - travis_retry composer install --no-interaction --prefer-dist --no-suggest

before_script:
   - google-chrome-stable --headless --disable-gpu --remote-debugging-port=9222 http://localhost &
   - php artisan serve &

script:
   - php artisan dusk

CircleCI 1.0
在 CircleCI 1.0 中运行 Dusk 时需要使用以下配置进行启动。与 TravisCI 相同,我们需要使用 php artisan serve 命令去启动 PHP 的内置服务器。

dependencies:
  pre:
      - curl -L -o google-chrome.deb https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
      - sudo dpkg -i google-chrome.deb
      - sudo sed -i 's|HERE/chrome\"|HERE/chrome\" --disable-setuid-sandbox|g' /opt/google/chrome/google-chrome
      - rm google-chrome.deb

test:
    pre:
        - "./vendor/laravel/dusk/bin/chromedriver-linux":
            background: true
        - cp .env.testing .env
        - "php artisan serve":
            background: true

    override:
        - php artisan dusk

CircleCI 2.0
在 CircleCI 2.0 中运行 Dusk 时需要将以下 steps 添加至 build:

 version: 2
 jobs:
     build:
         steps:
            - run: sudo apt-get install -y libsqlite3-dev
            - run: cp .env.testing .env
            - run: composer install -n --ignore-platform-reqs
            - run: npm install
            - run: npm run production
            - run: vendor/bin/phpunit

            - run:
               name: Start Chrome Driver
               command: ./vendor/laravel/dusk/bin/chromedriver-linux
               background: true

            - run:
               name: Run Laravel Server
               command: php artisan serve
               background: true

            - run:
               name: Run Laravel Dusk Tests
               command: php artisan dusk

Codeship

phpenv local 7.1
cp .env.testing .env
composer install --no-interaction
nohup bash -c "./vendor/laravel/dusk/bin/chromedriver-linux 2>&1 &"
nohup bash -c "php artisan serve 2>&1 &" && sleep 5
php artisan dusk

四、数据库测试

生成工厂

php artisan make:factory PostFactory
php artisan make:factory PostFactory --model=Post

每次测试后重置数据库

use RefreshDatabase;

编写工厂

use Faker\Generator as Faker;

$factory->define(App\User::class, function (Faker $faker) {
    return [
        'name' => $faker->name,
        'email' => $faker->unique()->safeEmail,
        'password' => '$2y$10$TKh8H1.PfQx37YgCzwiKb.KjNyWgaHb9cbcoQgdIVFlYg7B77UdFm', // secret
        'remember_token' => str_random(10),
    ];
});

工厂状态

$factory->state(App\User::class, 'delinquent', [
    'account_status' => 'delinquent',
]);

$factory->state(App\User::class, 'address', function ($faker) {
    return [
        'address' => $faker->address,
    ];
});

使用工厂

public function testDatabase()
{
    $user = factory(App\User::class)->make();
    $users = factory(App\User::class, 3)->make();
    // Use model in tests...
}
$users = factory(App\User::class, 5)->states('delinquent')->make();

$users = factory(App\User::class, 5)->states('premium', 'delinquent')->make();

覆盖属性

user = factory(App\User::class)->make([
    'name' => 'Abigail',
]);

create === make + save()

public function testDatabase()
{
    // Create a single App\User instance...
    $user = factory(App\User::class)->create();

    // Create three App\User instances...
    $users = factory(App\User::class, 3)->create();

    // Use model in tests...
}
$user = factory(App\User::class)->create([
    'name' => 'Abigail',
]);

关联

$users = factory(App\User::class, 3)
           ->create()
           ->each(function ($u) {
                $u->posts()->save(factory(App\Post::class)->make());
            });
$factory->define(App\Post::class, function ($faker) {
    return [
        'title' => $faker->title,
        'content' => $faker->paragraph,
        'user_id' => function () {
            return factory(App\User::class)->create()->id;
        }
    ];
});
$factory->define(App\Post::class, function ($faker) {
    return [
        'title' => $faker->title,
        'content' => $faker->paragraph,
        'user_id' => function () {
            return factory(App\User::class)->create()->id;
        }
    ];
});

assert 方法

$this->assertDatabaseHas($table, array $data);  
$this->assertDatabaseMissing($table, array $data);  
$this->assertSoftDeleted($table, array $data);

五、模拟

当测试某个控制器的时候,也许你并不想去测试他所触发的事件,因为该事件已经有了自己的测试方法。这时候你就可用在测试中模拟该时间了。

Bus 模拟

你可以使用 Bus facade 的 fake 方法来模拟任务执行,测试的时候任务不会被真实执行。使用 fakes 的时候,断言一般出现在测试代码的后面:

<?php

namespace Tests\Feature;

use Tests\TestCase;
use App\Jobs\ShipOrder;
use Illuminate\Support\Facades\Bus;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithoutMiddleware;

class ExampleTest extends TestCase
{
    public function testOrderShipping()
    {
        Bus::fake();

        // Perform order shipping...

        Bus::assertDispatched(ShipOrder::class, function ($job) use ($order) {
            return $job->order->id === $order->id;
        });

        // Assert a job was not dispatched...
        Bus::assertNotDispatched(AnotherJob::class);
    }
}

事件模拟

测试的时候不会触发事件监听器运行。然后你就可以断言事件运行了,甚至可以检查它们收到的数据。

<?php

namespace Tests\Feature;

use Tests\TestCase;
use App\Events\OrderShipped;
use App\Events\OrderFailedToShip;
use Illuminate\Support\Facades\Event;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithoutMiddleware;

class ExampleTest extends TestCase
{
    /**
     * Test order shipping.
     */
    public function testOrderShipping()
    {
        Event::fake();

        // Perform order shipping...

        Event::assertDispatched(OrderShipped::class, function ($e) use ($order) {
            return $e->order->id === $order->id;
        });

        Event::assertNotDispatched(OrderFailedToShip::class);
    }
}

邮件模拟

测试时不会真的发送邮件。然后你可以断言发送给了用户,甚至可以检查他们收到的数据。

class ExampleTest extends TestCase
{
    public function testOrderShipping()
    {
        Mail::fake();

        // Perform order shipping...

        Mail::assertSent(OrderShipped::class, function ($mail) use ($order) {
            return $mail->order->id === $order->id;
        });

        // Assert a message was sent to the given users...
        Mail::assertSent(OrderShipped::class, function ($mail) use ($user) {
            return $mail->hasTo($user->email) &&
                   $mail->hasCc('...') &&
                   $mail->hasBcc('...');
        });

        // Assert a mailable was sent twice...
        Mail::assertSent(OrderShipped::class, 2);

        // Assert a mailable was not sent...
        Mail::assertNotSent(AnotherMailable::class);
    }
}

如果你是用后台任务队执行 mailables 的发送,你应该用 assertQueued 方法来代替 assertSent:

Mail::assertQueued(...);
Mail::assertNotQueued(...);

通知模拟

测试的时候并不会真的发送通知。然后你可以断言已经发送给你的用户,甚至可以检查他们收到的数据。

public function testOrderShipping()
{
    Notification::fake();

    // Perform order shipping...

    Notification::assertSentTo(
        $user,
        OrderShipped::class,
        function ($notification, $channels) use ($order) {
            return $notification->order->id === $order->id;
        }
    );

    // Assert a notification was sent to the given users...
    Notification::assertSentTo(
        [$user], OrderShipped::class
    );

    // Assert a notification was not sent...
    Notification::assertNotSentTo(
        [$user], AnotherNotification::class
    );
}

队列模拟

测试的时候并不会真的把任务放入队列。然后你可以断言任务被放进了队列,甚至可以检查它们收到的数据。

public function testOrderShipping()
{
    Queue::fake();

    // Perform order shipping...

    Queue::assertPushed(ShipOrder::class, function ($job) use ($order) {
        return $job->order->id === $order->id;
    });

    // Assert a job was pushed to a given queue...
    Queue::assertPushedOn('queue-name', ShipOrder::class);

    // Assert a job was pushed twice...
    Queue::assertPushed(ShipOrder::class, 2);

    // Assert a job was not pushed...
    Queue::assertNotPushed(AnotherJob::class);
}

存储模拟

你可以轻松地生成一个模拟的磁盘,结合 UploadedFile 类的文件生成工具,极大地简化了文件上传测试。

    public function testAvatarUpload()
    {
        Storage::fake('avatars');

        $response = $this->json('POST', '/avatar', [
            'avatar' => UploadedFile::fake()->image('avatar.jpg')
        ]);

        // Assert the file was stored...
        Storage::disk('avatars')->assertExists('avatar.jpg');

        // Assert a file does not exist...
        Storage::disk('avatars')->assertMissing('missing.jpg');
    }

Facades

public function testGetIndex()
{
    Cache::shouldReceive('get')
                ->once()
                ->with('key')
                ->andReturn('value');

    $response = $this->get('/users');

    // ...
}

你不应该模拟 Request Facades。应该使用 HTTP 辅助函数 get, post 等来运行测试。模拟 Config Facades,应该使用 Config::set 方法。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容