2017年4月

laravel摘要

Laravel 使用 Vance Lucas 的 DotEnv PHP函数库来实现项目内环境变量的控制

生命周期:

  • 载入 Composer 生成的自动加载器定义; 从 bootstrap/app.php 文件获取到 Laravel 应用实例
  • HTTP 内核继承自 Illuminate\Foundation\Http\Kernel 类,它定义了一个 bootstrappers 数组,数组中的类在请求真正执行前进行前置执行(Illuminate\Foundation\Http的bootstrap方法)
  • 中间件的处理:Pipeline 参考
    $destination = function ($passable) {
        return $passable;
    };
    $pipes = array(
        function ($passable, $stack) {
            $passable[] = 1;
            return $stack($passable);
        },
        function ($passable, $stack) {
            $passable[] = 2;
            return $stack($passable);
        }
    );
    $pipeline = array_reduce(
        array_reverse($pipes),,
        function ($stack, $pipe) {
            return function ($passable) use ($stack, $pipe) {
                if ( $pipe instanceof Closure ) {
                    return $pipe($passable,$stack);
                }
            };
        },
        function ($passable) use ($destination) {
            return $destination($passable);
        }
    );

    $passable = [999];
    $result = $pipeline($passable);
    var_dump($result);

laravel容器:

// 简单绑定
$this->app->bind('HelpSpot\API', function ($app) {
    return new HelpSpot\API($app->make('HttpClient'));
});
// 绑定一个单例
$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);
// 绑定接口和实现, 当类依赖EventPusher的时候, RedisEventPusher会被自动注入
$this->app->bind(
    'App\Contracts\EventPusher',
    'App\Services\RedisEventPusher'
);
// 标记绑定
$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')); // tagged方法把标记为reports的abstract解析返回
});
// 解析
$api = $this->app->make('HelpSpot\API');
$api = resolve('HelpSpot\API');
// 容器事件(每当服务容器解析一个对象时就会触发一个事件。你可以使用 resolving 方法监听这个事件)
$this->app->resolving(function ($object, $app) {
    // 解析任何类型的对象时都会调用该方法...
});

$this->app->resolving(HelpSpot\API::class, function ($api, $app) {
    // 解析「HelpSpot\API」类型的对象时调用...
});

laravel服务提供者

  • 生成:php artisan make:provider RiakServiceProvider
  • 服务提供者的 boot 方法设置类型提示。服务容器 会自动注入您需要的任何依赖
  • 注册服务提供者:所有服务提供者都在 config/app.php 配置文件中providers数组中注册

路由

// 可选路由参数
Route::get('user/{name?}', function ($name = null) {
    return $name;
});
// 路由正则约束
Route::get('user/{name}', function ($name) {
    //
})->where('name', '[A-Za-z]+');
// RouteServiceProvider 的 boot 方法里定义全局约束
public function boot()
{
    Route::pattern('id', '[0-9]+');

    parent::boot();
}
// 命名路由(使用全局辅助函数 route 来生成 URL 或者重定向到该条路由)
Route::get('user/profile/{id?}', function ($id = null) {
    //
})->name('profile');
// 生成 URL...
$url = route('profile', ['id' => 1]);
// 生成重定向...
return redirect()->route('profile');
// 路由组:
Route::group(['middleware' => 'auth'], function () {
    // 组内应用auth中间件
    Route::get('user/{id}' , function ($id) {

    });
});
Route::group(['namespace' => 'admin'], function () {
    // 路由到 App\Http\Controllers\Admin\Aticle的index方法
    Route::get('article/{id}', 'article@index');
});
Route::group(['domain' => '{account}.zhorz.pw'], function () {
    Route::get('user/{account}/{id?}', function ($account, $id) {
        // 允许把捕获的子域名一部分用于我们的路由或控制器, 即$account
    });
});
Route::group(['prefix' => 'admin'], function () {
    Route::get('manage', function () {
        // 匹配 'admin/manage' uri
    });
    Route::get('account', function () {
        // 匹配 'admin/account' uri
    });    
});
// 路由隐式绑定模型
Route::get('api/users/{user}', function (App\User $user) {
    return $user->email;
    /*
        找不到对应的模型实例,将会自动生成产生一个 404 HTTP 响应
        默认使用id作为键名寻找, 修改模型的getRouteKeyName指定键名
        public function getRouteKeyName()
        {
            return 'slug';
        }
     */
});
// 路由显式绑定模型(RouteServiceProvider的boot中定义)
public function boot()
{
    // 路由显示绑定模型
    Route::model('user', \App\User::class);
    // 自定义解析逻辑
    Route::bind('user', function ($value) {
        return \App\User::where('name', $value)->first();
    });
    parent::boot();
}

中间件

// 创建中间件
php artisan make:middleware 
// App\Http\Middleware\CheckAge的handle方法处理传入请求
public function handle($request, $next) 
{
    // 请求被处理前执行...
    $response = $next($request);
    // 请求被处理后执行...
    return $response;
}
// 注册中间件(每个请求都会运行; 只需将该中间件类列入 app/Http/Kernel.php 类里的 $middleware 属性)
// 命名中间件(只需将该中间件类列入 app/Http/Kernel.php 类里的 $routeMiddleware属性)

// 路由指定命名中间件
Route::get('admin/profile', function () {
    //
})->middleware('middleware1','middleware2');

// 路由指定中间件类名
Route::get('admin/profile', function () {
    //
})->middleware(\App\Http\Middleware\CheckAge::class);

// 路由指定中间件组
Route::get('/', function () {
    //
})->middleware('web');
Route::group(['middleware' => ['web']], function () {
    //
});

// 为中间件指定参数
Route::put('post/{id}', function ($id) {
    // 使用role中间件, 参数是editor
})->middleware('role:editor');

// Terminable 中间件, terminate方法会在响应返回浏览器后执行
// 一旦定义了 terminable 中间件,你便需要将它增加到 HTTP kernel 文件的全局中间件清单列表中
public function terminate($request, $response)
{
    // Store the session data...
}

csrf

  • VerifyCsrfToken中间件处理csrf
  • 辅助函数 csrf_field 可以用来生成令牌字段
<form method="POST" action="/profile">
    {{ csrf_field() }}
    ...
</form>
  • csrf白名单
    • 比如第三方支付平台的支付回调使用post方法, 无法获取csrf的token, 需要将这类路由排除csrf保护
    • 将URI 添加到 VerifyCsrfToken 中间件中的 $except 属性来排除对这类路由的 CSRF 保护
  • VerifyCsrfToken 中间件还会检查 X-CSRF-TOKEN 请求头
// 1.表单请求指定meta头
<meta name="csrf-token" content="{{ csrf_token() }}">
// 2.使用类似 jQuery 的库将令牌自动添加到所有ajax请求的头信息中
$.ajaxSetup({
    headers: {
        'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
    }
});

HTTP控制器

  • 控制器中间件
class UserController extend Controller
{
    public function __construct()
    {
        $this->middleware('auth');
        $this->middleware('log')->only('index');// index操作使用中间件log
        $this->middleware('subscribed')->execept('stroe');// stroe操作使用中间件subscribed
        $this->middleware(function ($request, $next) {
            // 使用闭包函数作为中间件
        });
    }
}
  • 资源路由(略...)

请求REQUEST


// 获取请求路径
echo $request->path();
// 请求路径匹配
if ( $request->is('admin/*') ) {
    echo 1;
}
// 获取请求url
echo $request->url();
echo $request->fullUrl();

// 获取请求方法
echo $request->method();
if ( $request->isMethod('post') ) {
    echo 1;
}
// 获取所有输入数据
var_dump($input = $request->all());
// 获取输入数据
echo $request->input('field','default');
// 通过动态属性获取数据[先从请求数据中找, 没有再从路由参数中找]
echo $request->name;
// 获取部分输入数据
var_dump($request->only(['email']));
var_dump($request->expect(['email']));
// 判断数据是否存在
if ( $request->has('key') ) {
    echo 1;
}
// 获取cookie
echo $request->cookie('filed');
// 获取上传文件
$file = $request->file('photo');
$file = $request->photo;
// 文件是否存在
if ( $request->file('photo')->isValid() ) {
    echo 1;
}
// 文件扩展和路径
$path = $request->file('photo')->path();
$extension = $request->file('photo')->extension();
// 保存
$path = $request->photo->store('images');

响应response

Route::get('gg', function () {
    // 返回字符串
    return 'hello world';
    
    // 数组自动转换为json返回
    return ['a' => 1, 'b' => 2];
    
    // 返回构建响应对象
    return response('hello world', 200)->header('Content-Type', 'text/plain');
    // 多个响应头信息
    return response('hello world', 200)
                ->withHeaders(
                    array(
                        'Content-Type' => 'text/plain',
                        'X-Header-One' => 'value'
                    )
                );

    // 将一个cookie附加到响应
    return response('hello world')->cookie('name', 'value', $minutes, $path, $domain, $secure, $httpOnly);
    // 将一个cookie实例附加到响应
    $cookieObj = cookie('name', 'value');
    return response('hello world')->cookie($cookieObj);
    // 重定向路由
    return redirect('home');
    // 返回上一页, 并把输入数据保存到session
    return back()->withInput(
        $request->except(['email'])// $request->only(['email'])
    );
    // 获取旧的输入数据
    $username = $request->old('email'); // 模版上使用 {{ old('username') }}
    // 重定向并附加数据到session中
    return back()->with('status', 'success');
    // 重定向到命名路由
    return redirect()->route('login', ['id' => 1]);
    // 重定向到控制器
    return redirect()->action('HomeController@index', ['id' => 1]);

    // 视图响应
    return response()->view($template, $data, 200, $header);// 或使用全局辅助函数view
    // json响应
    return response()->json(['a' => 1,'b' => 2]);
    // 文件下载响应
    return response()->download($pathToFile, $name, $headers);
});

视图

Route::get('view', function () {
    // 视图位于 /resources/view/admin/profile.blade.php
    view('admin.profile', $data);

    view('admin.profile')->with('name', 'zhorz');

    // 把数据共享到所有视图(AppServiceProvider中配置)
    View::share('author', 'zhorz');

    // 判断模版文件是否存在
    if (View::exists('admin.profile')) {
        echo 1;
    }
});

SESSION管理

// session获取
$value = $request->session()->get('key'); // 第二个参数作为默认值(甚至闭包)
// 获取所有session数据
$all = $request->session()->all();
// 判断某个session值存在且不为null
if ( $request->session()->has('key') ) {
    echo 1;
}
// 判断某个session值存在可能为null
if ( $request->session()->exists('key') ) {
    echo 1;
}        

// 存储值
$request->session()->put('key', 'value');
// 存储值到数组中
$request->session()->push('team', 'value');

// 取出并删除
$value = $request->session()->pull('key', 'value');

// 删除数据到session, 只能将数据保留到下个 HTTP 请求, 然后就会被自动删除
$request->session()->flash('status', 'Task was successful!');

// 删除session数据
$request->session()->forget('key');
$request->session()->flush();

// 重新生成session id
$request->session()->regenerate();

// 全局的session函数
session('key', 'default'); // 获取
session(['key' => 'value']); // 存储

表单验证机制

public function store(StoreBlogPost $request)
{
    // 表单验证 法一
    $this->validate($request, [
        'title' => 'required|unique:posts|max:255',
        'body' => 'required',
    ]);

    /*
        视图中处理错误信息 $errors 变量(Illuminate\Support\ViewErrorBag 的实例)
        $errors变量通过中间件 \Illuminate\View\Middleware\ShareErrorsFromSession 绑定到视图
    */
    
    ###

    // 表单验证 法二 type hit 表单请求
    
    /*
        php artisan make:request StoreBlogPost
        1.添加一些验证规则到 StoreBlogPost 的 rules 方法
        2.添加表单请求后钩子 StoreBlogPost 的 withValidator 方法( 也可以控制器中使用 Validator::make(...)->after()方法 )
        3.请求授权 StoreBlogPost 的 authorize 方法
        4.自定义错误格式 StoreBlogPost 的 formatErrors 方法
        5.自定义错误消息 StoreBlogPost 的 messages 方法( 也可以控制器中使用 Validator::make($input, $rules, $messages) 第三个参数指定错误消息 )
     */

    ###

    // 表单验证 法三 Validate::make 生成validate实例
    $validator = Validator::make($request->all(), [
        'title' => 'required|unique:posts|max:255',
        'body' => 'required',
    ]);

    // 自动重定向
    $validator->validate();
    // 手动重定向
    if ($validator->fails()) {
        return redirect('post/create')
                    ->withErrors($validator) // withErrors 方法接收 validator、MessageBag,或 PHP array
                    ->withInput();
    }       

    ###

    // 命名错误包 withErrors 的第二个参数指定名称 ( 模版中使用 {{ $errors->login->first('email') }} )
    return redirect('register')
        ->withErrors($validator, 'login'); 

    ### 
    
    // 语言文件中指定错误消息 resources/lang/xx/validation.php
    
    /*
        'custom' => [
            'email' => [
                'required' => 'We need to know your e-mail address!',
            ],
        ],
     */
}

语言包

  • 语言包存放在 resources/lang 目录下的文件里
  • 切换语言:
    • config/app.php的locale
    • App::setLocale($locale);
  • 获取翻译语句:
    • 全局trans方法:trans('文件名'.'键名')
    • echo __('文件名'.'键名');
    • 模版中使用{{ __('文件名'.'键名') }}

用户认证系统逻辑

Route::get('auth', function (Request $request) {
    // laravel自带的用户验证系统(由guard和provider组成)
    
    // LoginController,RegisterController 和 ResetPasswordController 中设置 redirectTo 属性或方法设置用户操作成功后跳转
        
    // 自定义用户名 LoginController 默认使用email字段验证, 通过设置 username 方法修改
    
    // LoginController,RegisterController 和 ResetPasswordController 中定义 guard 方法 返回 自定义guard实例
    
    // 要修改新用户注册所必需的表单字段,或者自定义新用户字段如何存储到数据库,你可以修改 RegisterController 类。该类负责为应用验证输入参数和创建新用户
    
    // 获取已经验证的用户信息
    Auth::user();
    Auth::id();
    $request->user(); 

    // 检查用户是否登录Auth::check方法或使用Authenticate中间件
    // 如果使用 控制器类,可以在构造器中调用 middleware 方法,来代替在路由中直接定义
    // $this->middleware('auth:api') 中间件指定使用api的guard, guard在config/auth.php文件中guard数组
    if (Auth::check()) {
        echo 1; // check方法动态调用Auth对象下的guard对象的check( is_null($this->user) )
    }

    // 登录次数限制实现: Illuminate\Foundation\Auth\ThrottlesLogins
    
    // 手动验证用户
    if (Auth::attempt($request->only(['email', 'password']), false)) {
        return redirect()->intended('dashboard');
        // intended 返回用户未登录前的访问页面
        // 可以指定guard
        // Auth::guard($customDriver)->attempt(...);
    }

    // 检查用户是否登录中间件:
    // RedirectIfAuthenticated中间件和auth中间件
    // RedirectIfAuthenticated使用Auth::check()方法判断用户是否登录, 如果是跳转到/home
    // auth动态调用Auth对象下的guard对象的authenticate( 同样判断is_null($this->user), 如果没登录则抛出异常AuthenticationException, 异常处理器使用unauthenticated方法处理->调用redirect的guest方法, 把url放入session中, intended方法可以取出来  )

    // 关键在 guard的user() 方法, SessionGuard的user()方法从session获取用户id 或 从cookie获取token凭证
    // session的键名  'login_'. `guard的名称` . '_' . sha1(static::class) ; 值为`用户id`
    // cookie的键名  'remember_'. `guard的名称` . '_' . sha1(static::class) ;  值为 `用户id` . `保存在users表的remember_token`
    // 注销用户
    Auth::logout();

    // Auth::attempt方法第三个参数remember me , 把rember_token通过AddQueuedCookiesToResponse中间件写入cookie

    // viaRemember检查用户是否通过 rember me 的 cookie 来做认证
    if (Auth::viaRemember()) {
        //
    }

    // 可以使用 once 方法来针对一次性认证用户,没有任何的 session 或 cookie 会被使用, 参数和attempt参数一样
    if (Auth::once($request->only(['email', 'password']), false)) {
        //
    }

    // 添加自定义guard 的 "driver", AuthServiceProvider中调用Auth::extend();
    
    // 添加自定义user provider 的 "driver", AuthServiceProvider中调用Auth::provider();
    
    // Laravel 提供了在认证过程中的各种 事件。你可以在 EventServiceProvider 中对这些事件做监听

    /*
    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',
        ],
    ];
    */
})->middleware('auth');

Migrations迁移

  • 程序如何读取Migrations文件的路径呢?
    • 手动添加路径到$migrator:
      • Illuminate\Support\ServiceProvider的loadMigrationsFrom函数, 调用 $migrator->path($path), 保存路径到$migrator对象
    protected function loadMigrationsFrom($paths)
    {
        $this->app->afterResolving('migrator', function ($migrator) use ($paths) {
            foreach ((array) $paths as $path) {
                $migrator->path($path);
            }
        });
    }
    
    • 命令行读取路径:
      • Illuminate\Database\Console\Migrations\BaseCommand的getMigrationPaths方法
    protected function getMigrationPaths()
    {
        // Here, we will check to see if a path option has been defined. If it has we will
        // use the path relative to the root of the installation folder so our database
        // migrations may be run for any customized path from within the application.
        if ($this->input->hasOption('path') && $this->option('path')) {
            return [$this->laravel->basePath().'/'.$this->option('path')];
        }
    
        return array_merge(
            [$this->getMigrationPath()], $this->migrator->paths()
        );
    }
    

service provider的注册和延迟加载流程

  • Illuminate\Foundation\Http\Kernel的bootstrap()方法调用Illuminate\Foundation\Bootstrap\RegisterProviders的bootstrap方法注册服务

  • 然后调用$app->registerConfiguredProviders方法实例化ProviderRepository类(负责读取config/app.php的providers数组, 实例化每个providers类, 分析是否延迟加载, 分析provider对象的provides方法, 获取延迟加载的对象别名, 保存到bootstrap\cache\service.php)

  • 对应不用延迟加载的provider对象, 调用$app->register()方法

  • 延迟加载的服务如何加载?

    • Illuminate\Container\Container类的offsetGet方法, 当访问一个延迟服务提供的对象的时候, 比如'queue', $app->make方法判断是否延迟加载的对象, 如果是则注册该对象对应的服务提供者provider

api验证

Route::get('api', function () {
    // api验证

    // 1.发放私人令牌(不用经过跳转流程)
    $user = App\User::find(1);

    // Creating a token without scopes...
    $token = $user->createToken('Token Name')->accessToken;
    echo $token;
    // Creating a token with scopes...
    // $token = $user->createToken('My Token', ['place-orders'])->accessToken;

    /*
    
    2.通过用户密钥获取令牌(不用经过跳转流程)
    $http = new GuzzleHttp\Client;

    $response = $http->post('http://laravel.cn/oauth/token', [
        'form_params' => [
            'grant_type' => 'password',
            'client_id' => '1',
            'client_secret' => 'Wc51roPhGq7FOdJMs7oIyBLmeUjSN8DPpJEz3T9f',
            'username' => '1181747659@qq.com',
            'password' => 'zhou5566',
            'scope' => '*',
        ],
    ]);

    return json_decode((string) $response->getBody(), true);
    */
   
   // 3.通过中间件验证授权
   
   // 4.传递访问令牌 需要将访问令牌作为 Bearer 令牌放在请求头 Authorization 中
    $response = $client->request('GET', '/api/user', [
        'headers' => [
            'Accept' => 'application/json',
            'Authorization' => 'Bearer '.$accessToken,
        ],
    ]);
})->middleware('auth:api');;

Facade

  • 在Illuminate\Foundation\Bootstrap\RegisterFacades.php中为Facade静态对象设置$app
  • 调用某个Facade, 比如Gate::allows, Facade会先从$app容器中返回Gate对象, __callStatic()动态调用Gate对象的方法

用户授权之gate和策略

  • 用户的授权方式主要有两种:gates(类似路由, 使用闭包作为限制规则)和策略(类似控制器, 使用类控制)
// AuthServiceProvider中注册gate规则
// 实际是把闭包函数保存到Gate对象$abiliyies数组
Gate::define('update-post', function($user, $post) {
    return $user->id == $post->user_id;
});
// gate判断是否授权
// 调用allows方法时会先检查$post是否被注册到Gate对象$policies数组( '模型' => '策略模型' )
// 如果是则调用该策略模型PostPolicy下对应$ability的驼峰名方法(updatePost)
// 否则查看该$ability是否被保存到Gate对象$abiliyies数组, 是则调用对应的闭包函数
if (Gate::allows('update-post', $post)) {
    // allows do somthing;
}

if (Gate::forUser($user)->allows('update-post', $post)) {
    // allows user to do somthing;
}

if (Gate::forUser($user)->denies('update-post', $post)) {
    // denies user to do somthing;
}

// 策略
// AuthServiceProvider中定义模型映射策略
    protected $policies = [
        'App\Model' => 'App\Policies\ModelPolicy',
        'App\Post' => 'App\Policies\PostPolicy' // 指定模型对应的策略
    ];
if ($user->can('update', $post)) {
    // 实际是调用Gate::check方法, 会自动检查$post是否定义了策略映射, 是则调用该策略对象的update方法
}

// 策略过滤器
// 策略对象中定义before方法, before接受两个参数$user $ability, 这个功能最常见的场景是授权应用的管理员可以访问所有动作
// 如果你想拒绝用户所有的授权,你应该在 before 方法中返回 false。如果返回的是 null,则通过其他的策略方法来决定授权与否

// 通过控制器中使用辅助函数实现授权访问
$this->authorize($ability, $model);

加密解密和hash加密

Route::get('encrypt', function () {
    // return encrypt('abc');
    $encrypted = Illuminate\Support\Facades\Crypt::encryptString('hello world');
    $decrypted = Illuminate\Support\Facades\Crypt::decryptString($encrypted);
    echo $decrypted;
});
Route::get('hash', function () {
    $hashed = Hash::make('hello');
    if (Hash::needsRehash($hashed)) {
        // 检查hash是否被篡改
        echo 'yes';
    } else {
        echo 'no';
    }

    echo PHP_EOL;

    if (Hash::check('hello', $hashed)) {
        echo 'success';
    } else {
        echo 'fail';
    }
});