Laravel 关联模型由于名称一致性导致的问题

1. 定义关联模型

在Laravel里面,我们可以通过定义以下Model来完成关联查询。

class MyPost extends Eloquent {
    public function myPostInfo () {
        return $this->hasOne('MyPostInfo');
    }
}

class MyPostInfo extends Eloquent {}

图片 1laravel原文链接

您在此之前可能就已经缓存过模型数据,但是我将向您展示一个使用动态记录模型的更精细的Laravel模型缓存技术,这是我一开始在RailsCasts学习到的技术。

2. 使用关联模型

这里myPostInfo()用的是Camel命名规则,但是我们在读取某一个PostInfo的时候可以用Snake规则。如下面代码都是可行的:

$post = MyPost::find(1);
$post_info = $post->myPostInfo; // example 1
$post_info = $post->my_post_info; // example 2

Laravel允许上述两种方法,但是没有合理的处理使用两种命名造成的冲突。

配置

Laravel的主配置文件将经常用到的文件集中到了根目录下的.env目录下,这样更高效更安全。其内容如下:

# 这里配置APP_ENV=localAPP_DEBUG=trueAPP_KEY=YboBwsQ0ymhwABoeRgtlPE6ScqSzeWZG# 这里配置数据库DB_HOST=localhostDB_DATABASE=testDB_USERNAME=rootDB_PASSWORD=mysqlCACHE_DRIVER=fileSESSION_DRIVER=fileQUEUE_DRIVER=syncMAIL_DRIVER=smtpMAIL_HOST=mailtrap.ioMAIL_PORT=2525MAIL_USERNAME=nullMAIL_PASSWORD=nullEXAMPLE_PUBLIC_KEY=abcndef # 要在配置里面换行,目前只有这种方式了,在读取的时候这样子读取: str_replace("\n", "n", env('MSGCENTER_PUBLIC_KEY'))

还可以在该文件里配置其它的变量,然后在其它地方使用env(name, default)即可访问。例如,读取数据库可以用config('database.redis.default.timeout', -1)来。全局配置文件.env仅仅是一些常量的配置,而真正具体到哪个模块的配置则是在config目录之下.同样,也可以动态改变配置:Config::set('database.redis.default.timeout')

另外,可以通过帮助函数来获取当前的应用环境:

# 获取的是.env里面APP_ENV的值$environment = App::environment();App::environment // true/falseapp()->environment()

使用模型的唯一缓存键,您可以缓存模型的模型上的属性和关联,一个好处是访问缓存的数据比在控制器中缓存的数据更具可复用性,因为它在模型上而不是在单个控制器方法中。

3. 缓存失效

如果我们同时使用了上述两个例子,就会使其中一个缓存失效。在Model的relations变量中,缓存了已经读取过的关联Model,但是当我们用不同规则的名字去读取的时候,却会使得前一个缓存失效。例如

$post_info = $post->myPostInfo; 
// $post->relations = [‘myPostInfo’ => ..];

$post_info = $post->my_post_info;
// $post->relations = [‘myPostInfo’ => …, ‘my_post_info’ => …];

所以如果不希望缓存失效,得在项目中只使用一种命名方法去读取关系模型。Laravel推荐的是Camel
Case.

控制器

laravel可以直接通过命令创建一个控制器:php artisan make:controller HomeController,然后就会有这么一个控制器文件了:app/Http/Controllers/HomeController.php

# 通过Validator进行校验,第一个参数是一个key-value的数组$validation = Validator::make($request->all(), [ 'ip' => 'required|ip' # 校验key=ip的值是否真的是ip 'arr.*.field' => 'required|' # 验证数组内部的字段,5.1不支持])# 常用框架自带的认证类型active_url # 该url一定能访问array # 仅允许为数组between:min,max # 介于最小值和最大值之间,两边都是闭区间,如果是数字,一定要先声明当前字段为integerboolean # 必须是true,false,1,0,"1","0"date # 必须是时间类型exists:table,column # 判断字段的值是否存在于某张表的某一列里面exists:table,column1,column2,value # 判断字段的值是否存在于某张表的某一列里面,并且另一列的值为多少exists:table,column1,column2,!value # 判断字段的值是否存在于某张表的某一列里面,并且另一列的值不为多少exists:table,column1,column2,{$field}# 判断字段的值是否存在于某张表的某一列里面,并且另一列的值和前面的某个字段提供的值一样in:value1,value2,...# 字段值必须是这些值中的一个,枚举值not_in:value1,value2,... # 字段值不为这其中的任何一个integer # 必须是整数ip # 必须是IP字符串json # 必须是JSON字符串max:value # 规定最大值min:value # 规定最小值numeric # 是数字required # 必填required_with:字段名 # 当某个字段存在的时候当前字段必填required_if:anotherfield,value # 当某个字段的某个值为多少的时候,当前字段为必填string # 必须是字符串url # 必须是合法的urlregex # 必须符合这个正则表达式,例如regex:/^[a-z]{1}[a-z0-9-]+$/,需要注意的是,如果正则表达式中用了|符号,必须用数组的方式来写正则表达式,否则会报错,例如['required', 'regex:/[0-9]([0-9]|-+/']# 自定义错误提示的消息,可以通过传递进去,不过也可以直接在语言包文件resources/lang/xx/validation.php文件的的custom数组中进行设置# 验证数组里面的字段用这样的方式'person.email' => 'email|unique:users''person.first_name' => 'required_with:person.*.last_name'# 将表单的验证提取出来作为单独的表单请求验证Form Request Validation# 使用php artisan make:request BlogPostRequest创建一个表单请求验证类,会在app/Http/Requests里面生成相应的类,之后表单验证逻辑就只需要在这里写上就行了,例如<?phpnamespace AppHttpRequests;use Route;use IlluminateSupportFacadesAuth;class BlogPost extends Request{ // 这个方法验证用户是否有权限访问当前的控制器 public function authorize() { $id = Route::current()->getParameter; // 如果是resource的东西,要获取id,在这里是这样子获取,不能直接用id,而是相对应的资源名 switch($this->method{ # 我这里,姑且卸载一起 case 'POST':{ return Auth::user()->can('create', Project::class); } case 'PUT':{ return Auth::user()->can('update', Project::find; } } } /** * 这里则是返回验证规则 */ public function rules(){ switch($this->method{ case 'POST': { return [ 'name' => 'required|string|max:100', ]; } case 'PUT':{ return [ 'name' => 'required|string|max:100', ]; } } } // 自定义返回格式 public function response(array $errors){ return redirect()->back()->withInput()->withErrors; }}

资源控制器可以让你快捷的创建 RESTful
控制器。通过命令php artisan make:controller PhotoController创建一个资源控制器,这样会在控制器PhotoController.php里面包含预定义的一些Restful的方法Route::resource(‘photo’,
‘PhotoController’);

# 嵌套资源控制器# 例如Route::resource('photos.comments', 'PhotoCommentController');# 这样可以直接通过这样的URL进行访问photos/{photos}/comments/{comments}# 控制器只需要这样子定义即可public function show($photoId, $commentId)

假设你有很多个 CommentArticle模型,给定下面的Laravel blade
模板,你就可以像下面这样访问/article/:id路由时得到评论的数量:

4. toArray() 方法失效

如果同时使用了两者,另外一个问题就是导致Model::toArray()失效。因为toArray()方法首先去relations中查找Snake
Case命名的关联模型,没有的话才去看Camel Case的。

所以如果用到了toArray()方法来转换Model,切忌同时使用两者。

资源控制器对应的路由
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
$article->comments->count() {{ str_plural('Comment', $article->comments->count

5. 容易犯错的位置

最容易犯错的代码是这样的:

MyPost::with(‘myPostInfo’)->get();

在使用With去eagerLoad关联模型时,必须使用和定义方法同名的key去读取,那么这样读取出来的方法只能是Camel
Case的key。其他地方就只能用

$my_post->myPostInfo;

来保证不出问题。

路由url

路由缓存:laravel里面使用route:cache Artisan,可以加速控制器的路由表,而且性能提升非常显著。

# 路由分组,第一个属性则是下面所有路由共有的属性Route::group(['namespace' => 'Cron', 'middleware' => ['foo', 'bar']], function(){ Route::get('/', function() { // AppHttpControllersCron }); Route::get('user/profile', function() { // Has Foo And Bar Middleware });});# 通过url向控制器传递参数这样定义urlRoute::resource('wei/{who}', 'WeixinController');然后在控制器里这样定义public function index# 嵌套资源控制器# 例如Route::resource('photos.comments', 'PhotoCommentController');# 这样可以直接通过这样的URL进行访问photos/{photos}/comments/{comments}# 控制器只需要这样子定义即可public function show($photoId, $commentId)# 如果要获取嵌套资源的url,可以这样子:route('post.comment.store', ['id'=> 12]) # 这样子就获取到id为12的post的comment的创建接口地址

您可以在控制器中缓存评论的计数,但是当您有多个需要缓存的一次性查询和数据时,控制器会变得非常臃肿难看。使用控制器,访问缓存的数据也不是很方便。

视图/静态资源

return response()->download($pathToFile); # 直接提供文件下载return response()->download($pathToFile, $name, $headers); # 设置文件名和响应头return response()->download($pathToFile)->deleteFileAfterSend; # 设置为下载后删除

我们可以构建一个模板,它仅在文章更新时访问数据库,并且访问该模型的所有代码都可以获取缓存值:

模板Template

# 转义{!! $name !!}# if else@if()@else@endif# 需要注意的是,if else是不能写在一行的如果非要写在同一行,建议使用这样的方法{!! isset && $a['a'] == 'a' ? 'disabled': '' !!}

Larval的分页主要靠Eloquent来实现,如果要获取所有的,那么直接把参数写成PHP_INT_MAX就行了嘛

$users = User::where('age', 20)->paginate; // 表示每页为20条,不用去获取页面是第几页,laravel会自动在url后面添加page参数,并且paginate能自动获取,最后的结果,用json格式显示就是{ 'total': 50, 'per_page': 20, 'current_page': 1, 'last_page': 3, 'next_page_url': '...', 'prev_page_url': null, 'from': 1, 'to': 15, 'data': [{}, {}]}# 如果是在数据库关系中进行分页可以直接在Model里面鞋public function ...(){ return $this->posts()->paginate;}# 获取all的分页数据,不用::all(),而是User::paginate # 直接用paginate
$article->cached_comments_count {{ str_plural('Comment', $article->cached_comments_count)

数据库Model

Laravel提供了migration和seeding为数据库的迁移和填充提供了方便,可以让团队在修改数据库的同时,保持彼此的进度,将建表语句及填充操作写在laravel框架文件里面并,使用migration来控制数据库版本,再配合Artisan命令,比单独管理数据库要方便得多。

config/database.php里面进行数据库引擎的选择,数据库能通过prefix变量统一进行前缀的配置

生成一个model:
php artisan make:model user -m,这样会在app目录下新建一个和user表对应的model文件

<?phpnamespace App;use IlluminateDatabaseEloquentModel;class Flight extends Model{ //}

加上-m参数是为了直接在database/migrations目录下生成其迁移文件,对数据库表结构的修改都在此文件里面,命名类似2016_07_04_051936_create_users_table,对数据表的定义也在这个地方,默认会按照复数来定义表名:

<?phpuse IlluminateDatabaseSchemaBlueprint;use IlluminateDatabaseMigrationsMigration;class CreateApplicationsTable extends Migration{ /** * Run the migrations. * * @return void */ public function up() { Schema::create('applications', function (Blueprint $table) { $table->increments; $table->timestamps; DB::statement('ALTER TABLE `'.DB::getTablePrefix().'applications` comment "这里写表的备注"'); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::drop('applications'); }}

当数据表定义完成过后,执行php artisan migrate即可在真的数据库建表了

php artisan migrate // 建表操作,运行未提交的迁移php artisan migrate:rollback // 回滚最后一次的迁移php artisan migrate:reset // 回滚所有迁移php artisan migrate:refresh // 回滚所有迁移并重新运行所有迁移

如果要修改原有model,不能直接在原来的migrate文件上面改动,而是应该新建修改migration,例如,执行php artisan make:migration add_abc_to_user_table这样会新建一个迁移文件,修改语句写在up函数里面:

public function up(){ Schema::table('users', function (Blueprint $table) { $table->string('mobile', 20)->nullable()->after('user_face')->comment->change(); // 将mobile字段修改为nullable并且放在user_face字段后面,主要就是在后面加上change()方法 $table->renameColumn('from', 'to'); // 重命名字段 $table->dropColumn; // 删除字段 $table->dropColumn(['votes', 'from']);// 删除多个字段 $table->string->unique(); // 创建索引字段 $table->unique; // 创建唯一索引 $table->unique('email', 'nickname'); // 联合唯一索引 $table->index(['email', 'name']); // 创建复合索引 $table->dropPrimary('users_id_primary'); // 移除主键 $table->dropUnique('users_email_unique'); // 移除唯一索引 $table->dropIndex('geo_state_index'); // 移除基本索引 });}

class User extends Model{ public $timestamps = false; // 设置该表不需要使用时间戳,updated_at和created_at字段 protected $primaryKey = 'typeid' // 不以id为主键的时候需要单独设置 protected $primaryKey = null; // 没有主键的情况 protected $incrementing = false; // 不使用自增主键 protected $connection = 'second'; // 设置为非默认的那个数据库连接 protected $fillable = ['id', 'name']; // 设置可直接通过->访问或者直接提交保存的字段 protected $table = 'my_flights'; // 自定义表明,默认的表明会以model的复数形式,需要注意的是,英语单词复数的变化有所不同,如果取错了表明活着以中文拼音作为表明,有时候就需要明确表的名称了}

# 字段定义$table->increments # 默认都有的自增的主键$table->string('name', 45)->comment # 字符串类型,添加注释,长度可指明也可不指名$table->boolean # 相当于tinyint$table->softDeletes() # 软删除,名为deleted_at类型为timestamp的软删除字段$table->bigInteger # bigint,加不加sign都是20$table->integer() # int$table->integer()->uninsign() # int$table->integer()->unsigned() # int$table->mediumInteger # int$table->mediumInteger->unsign() # int$table->mediumInteger->unsigned()# 相当于int$table->smallInteger # smallint$table->smallInteger->unsign() # smallint$table->smallInteger->unsigned() # smallint$table->tinyInteger # tinyint$table->tinyInteger->unsign() # tinyint$table->tinyInteger->unsigned() # tinyint$table->float # 相当于DOUBLE$table->text # text()$table->dateTime('created_at') # DATETIME类型# 字段属性->nullable() # 允许null->unsigned() # 无符号,如果是integer就是int->unsign() # 无符号,如果是integer就是int->default # 默认值 # 索引定义$table->index('user_id')# 主键定义$table->primary # 默认不用写这个$table->primary(array('id', 'name')) # 多个主键的情况# 外键定义$table->integer('user_id')->unsigned(); # 先要有一个字段,而且必须是unsigned的integer$table->foreign('user_id')->references->on; # 关联到users表的id字段

直接在ORM里面进行表关系的定义,可以方便查询操作。

通过使用模型访问器,我们可以缓存基于最后一次文章更新的评论计数值。

一对多hasMany
public function posts(){ return $this->hasMany('AppPost');}# 可以这样使用Users::find->posts# 指定外键$this->hasMany('AppPost', 'foreign_key', 'local_key')

因此,在评论新增或删除时我们该怎么更新文章的updated_at列值呢?

一对一hasOne
public function father(){ return $this->hasOne('AppFather');}$this->hasOne('AppFather', 'id', 'father'); # 表示father的id对应本表的father

先进入 touch 方法看看。

相对关联belongsTo
public function user(){ return $this->belongsTo('AppUser')}Posts::find->user # 可以找到作者

模型的触发

多对多关系belongsToMany

如果有三张表,users,roles,role_user其中,role_user表示users和roles之间的多对多关系。如果要通过user直接查出来其roles,那么可以这样子

class User extends Model { public funciton roles() { return $this->belongsToMany('AppRole', 'user_roles', 'user_id', 'foo_id'); # 其中user_roles是自定义的关联表表名,user_id是关联表里面的user_id,foo_id是关联表里面的role_id }}$roles = User::find->roles; # 这样可以直接查出来,如果想查出来roles也需要在roles里面进行定义

可以通过使用模型的touch()方法来更新文章的updated_at 列值:

多态关联

一个模型同时与多种模型相关联,可以一对多(morphMany)、一对一、多对多

例如:
三个实例,文章、评论、点赞,其中点赞可以针对文章和评论,点赞表里面有两个特殊的字段target_idtarget_type,其中target_type表示对应的表的Model,target_id表示对应的表的主键值

# 点赞Modelclass Like extends Model { public function target() { return $this->morphTo(); // 如果主键不叫id,那么可以指定morphTo(null, null, 'target_uuid')最后这个参数是字段名哟 }}// 文章Modelclass Post extends Model { public function likes(){ # 获取文章所有的点赞 return $this->morphMany('AppLike', 'target'); }}// 评论Modelclass Comment extends Model { public function likes() { # 获取评论所有的点赞 return $this->morphMany('AppLike', 'target'); }}$comment->likes;$comment->likes;$this->morphedByMany('AppModelsPosts', 'target', 'table_name'); // 一种多对多关联的morphedby

Laravel使用数据填充类来填充数据,在app/database/seeds/DatabaseSeeder.php中定义。可以在其中自定义一个填充类,但最好以形式命名,如(默认填充类为DatabaseSeeder,只需要在该文件新建类即可,不是新建文件):

class DatabaseSeeder extends Seeder{ /** * Run the database seeds. */ public function run() { $this->call(UsersTableSeeder::class); }}class UsersTableSeeder extends Seeder{ /** * Run the user seeds. */ public function run() { DB::table->delete(); AppUser::create([ 'email' => 'admin@haofly.net', 'name' => '系统管理员', ]); }}

然后在Composer的命令行里执行填充命令

php artisan db:seedphp artisan migrate:refresh --seed //回滚数据库并重新运行所有的填充

# 获取查询SQLDB::connection('default')->enableQueryLog() # 如果不指定连接可以直接DB::enableQueryLog()... # ORM操作 dd(DB::connection('statistics')->getQueryLog # 打印sql# 查询User::all() # 取出所有记录User::all(array('id', 'name')) # 取出某几个字段User::find # 根据主键取出一条数据User::findOrFail # 根据主键取出一条数据或者抛出异常User::where([ ['id', 1], ['name', 'haofly']) # where语句能够传递一个数组User::where() # 如果不加->get()或者其他的是不会真正查询数据库的,所以可以用这种方式来拼接,例如$a_where=User::where();$result =$a_where->where()->get();User::whereIn('name', ['hao', 'fly']) # in查询User::whereNull # is nullUser::whereNotNull # is not nullUser::whereBetween('score', [1, 100]) # where betweenUser::whereNotBetween('score', [1, 100]) # where not betweenUser::whereDate('created_at', '2017-05-17')User::whereMonth('created_at', '5')User::whereDay('created_at', '17')User::whereYear('created_at', '2017')User::whereColumn('first_field', 'second_field') # 判断两个字段是否相等User::where->orWhere() # or whereUser::where()->firstOrFail() # 查找第一个,找不到就抛异常User::where('user_id', 1)->get()# 返回一个Collection对象User::where->first() # 只取出第一个model对象User::find->logs->where # 关系中的结果也能用where等字句User::->where('updated_at', '>=', date('Y-m-d H:i').':00')->where('updated_at', '<=', date('Y-m-d H:i').':59') # 按分钟数查询User::find->sum # 求和SUMUser::where->get()->pluck # 只取某个字段的值,而不是每条记录取那一个字段,这是平铺的,这里的pluck针对的是一个Collection,注意,这里只能针对Collection,千万不要直接针对一个Model,这样只会取出那张表的第一条数据的那一列User::select->where() # 也是只取出某个字段,但是这里不是平铺的User::where->pluck # 这是取出单独的一个行的一个列,不再需要firstUser::withTrashed()->where() # 包括软删除了的一起查询User::onlyTrashed()->where() # 仅查找软删除了的User::find->posts # 取出一对多关联,返回值为CollectionUser::find->posts() # 取出一对多关联,返回值为hasManyUser::find->posts->count() # 判断关联属性是否存在stackoverflow上面用的这种方法 User::all()->orderBy('name', 'desc') # 按降序排序User::all()->latest() # 按created_at排序User::all()->oldest() # 按created_at排序User::all()->inRandomOrder()->first() # 随机顺序# 访问器,如果在Model里面有定义这样的方法public function getNameAttribute(){ return $this->firstname.$this->lastname;}那么在外部可以直接$user->name进行访问# 新增Model::firstOrCreate() # firstOrCreate的第二个参数是5.3才开始的Model::firstOrNew() # 与上面一句不同的是不会立马添加到数据库里,可以通过$object->new来判断是否是新添加的,如果该方法不存在那就用$object->exists判断是否已经存在于数据库中,这个方法是没有第二个参数的Model::updateOrCreate, array$User::find->phones()->create # 存在着关联的model可以直接新建,而且可以不指定那个字段,比如这里创建phone的时候不用指定user_id$author->posts()->save; # 添加hasone或者hasmany,不过这是针对新建的$author->posts()->associate; # 这是直接将外键设置为已经存在的一个posts$author->posts()->saveMany([$post1, $post2]) # 添加hasmany$post->author()->save(Author::find # 设置外键$author->posts()->detach$author->posts()->attach([1,2,3=>['expires'=>$expires]])# 修改$user->fill(['name' => 'wang'])->save() # fill必须save过后才会更新到数据库$user->update(['name' => 'wang']) # update会立即更新数据库$user->increment # 如果是数字类型,可以直接相加,不带5就表示之内加1$user->decrement # 或者减# 删除$user->delete() # 删除,如果设置了软删除则是软删除$user->forceDelete() # 无论是否设置软删除都物理删除 # 事务,注意数据库的连接问题DB::beginTransaction();DB::connection('another')->begintransaction();DB::rollback(); # 5.1之前用的都是rollBackDB::commit();
$ php artisan tinker>>> $article = AppArticle::first();=> AppArticle {#746 id: 1, title: "Hello World", body: "The Body", created_at: "2018-01-11 05:16:51", updated_at: "2018-01-11 05:51:07", }>>> $article->updated_at->timestamp=> 1515649867>>> $article->touch();=> true>>> $article->updated_at->timestamp=> 1515650910
With

with在laravel的ORM中被称为预加载,作用与关联查询上

# 例如要查询所有文章的作者的名字,可以这样子做$posts = AppPost::all();foreach($posts as $post) { var_dump($post->user->name);}# 但是,这样做的话,每一篇文章都会查询一次用户,而如果这些文章的用户都是一个人,那岂不是要查询n次了。这时候预加载就有用了。$posts = AppPost::with->get();foreach ( $books as $book) { var_dump($post->user->name);}# 这样子做,所有的数据在foreach前就都读取出来了,后面循环的时候并没有查询数据库,总共只需要查询2次数据库。# with还可以一次多加几张关联表AppPost::wth('user', 'author')->get();# 嵌套使用AppPost::with('user.phone')->get(); # 取出用户并且取出其电话# 也可以不用全部取出来$users = User::with(['posts' => function  { $query->where('title', '=', 'test');}])->get();

我们可以用更新的 timestamp
值使缓存失效。不过在新增或删除一个评论时,我们怎么触发修改文章的updated_at字段呢?

Cache

缓存的是结果

# hasMany对象的查询$posts = User::find->posts() # 返回hasMany对象,并未真正查询数据库$posts = User::find->posts # 返回Collection对象,数据库的查询结果集$posts->get() # 返回Collection对象,数据库的查询结果集

碰巧 Eloquent 模型中有一个属性就叫$touches
。下面是我们的评论模型的大概样子:

Collection对象
$obj->count() # 计数$obj->first() # 取出第一个对象$obj->last() # 取出最后一个对象$obj->isEmpty() # 是否为空
belongsTo; }}

认证相关

Policy主要用于对用户的某个动作添加权限控制,这里的Policy并不是对Controller的权限控制.

权限的注册在app/Providers/AuthServiceProvider.php里面,权限的注册有两种:

# 一种是直接在boot方法里面进行定义class AuthServiceProvider extends ServiceProvider{ public function boot(GateContract $gate) { $this->registerPolicies; $gate->define('update-post', function($user, $post) { return $user->id === $post->user_id; # 这样就添加了一个名为update-post的权限 } ) $gate->define('update-post', 'Class@method'); # 也可以这样指定回调函数 $gate->before(function ($user, $ability) { # before方法可以凌驾于所有的权限判断之上,如果它说可以就可以 if ($user->isSuperAdmin return true; }); $gate->after(function }}# 第二种是创建Policy类,可以用命令php artisan make:policy PostPolicy进行创建,会在Policies里面生成对应的权限类,当然,权限类创建完了后同样也需要将该类注册到AuthServiceProvider里面去,只需要在其$policies属性中定义就好了,例如protected $policies = [ Post::class => PostPolicy::class, # 将权限类绑定到某个Model];# 权限类的定义:class PostPolicy{ public function before($user, $ability){ // 类似的before方法 if ($user->isSuperAdmin {return true} } public function update(User $user, Post $post){ return true; }}

权限的使用

# 控制器中使用use Gate;if (Gate::denies('update-post', $post)) {abort(403, 'Unauthorized action')}Gate::forUser->allows('update-post', $post) {}Gate::define('delete-comment', function($user, $post, $comment){}) # 传递多个参数Gate::allows('delete-comment', [$post, $comment]) # 也可这样传递多个参数$user->cannot('update-post', $post)$user->can('update-post', $post)$user->can('update', $post) # 无论你有好多个Policy,因为权限类是根据Model创建的,系统会自动定位到PostPolicy的update中去判断$user->can('create', Post::class) # 自动定位到某个model@can('update-post', $post) # 在模版中使用,如果是create可以这样@can('create', AppPost::class) <html>@endcan@can('update-post', $post) <html1>@else <html2>@endcan@can('create', AppPost::class) # Post的创建,针对PostPolicy@can('create', [AppComment::class, $post]) # Comment的创建,针对CommentPolicy,并且应该这样子定义:public function create(User $user, $commentClassName, Project $project)

这里的$touches属性是个数组,包含了在评论的创建、保存和删除时会引起“触发”的关联信息。

任务队列Job

通过php artisan make:job CronJob新建队列任务,会在app/Jobs下新建一个任务.

# 队列里能够直接在构造函数进行注入,例如public function __construct(ResourceService $resourceService){ $this->resourceService = $resourceService;}# 任意地方使用队列dispatch(new AppJobsPerformTask);# 指定队列名称$jog = (new AppJobs..)->onQueue;dispatch;# 指定延迟时间$job = (new AppJobs..)->delay;# 任务出错执行public function failed(){ echo '失败了';}
  • queue:work:
    最推荐使用这种方式,它比queue:listen占用的资源少得多,不需要每次启动框架。但是代码如果更新就需要用queue:restart来重启

需要注意的是

  1. 不要在Jobs的构造函数里面使用数据库操作,最多在那里面定义一些传递过来的常量,否则队列会出错或者无响应
  2. job如果有异常,是不能被catch的,job只会重新尝试执行该任务,并且默认会不断尝试,可以在监听的时候指定最大尝试次数--tries=3
  3. 不要将太大的对象放到队列里面去,否则会超占内存,有的对象本身就有几兆大小
  4. 一个很大的坑是在5.4及以前,由于queue:work没有timeout参数,所以当它超过了队列配置中的expire时间后,会自动重试,但是不会销毁以前的进程,默认是60s,所以如果有耗时任务超过60s,那么队列很有可能在刚好1分钟的时候自动新建一条一模一样的任务,这就导致数据重复的情况。

缓存的属性

事件

就是实现了简单的观察者模式,允许订阅和监听应用中的事件。用法基本上和队列一致,并且如果用上队列,那么代码执行上也和队列一致了。

我们先回到$article->cached_comments_count访问器。该方法的实现可能象AppArticle模型中的样子:

服务容器

Laravel核心有个非常非常高级的功能,那就是服务容器,用于管理类的依赖,可实现自动的依赖注入。比如,经常会在laravel的控制器的构造函数中看到这样的代码:

function function __construct(Mailer $mailer){ $this->mailer = $mailer }

但是我们却从来不用自己写代码去实例化Mailer,其实是由Laravel的服务容器自动去提供类的实例化了。

# 注册进容器$this->app->bind('Mailer', function{ return new Mailer});$this->app->singleton('Mailer', function{ # 直接返回的是单例 return new Mailer})$this->app->instance('Mailer', $mailer) # 如果已经有一个实例化了的对象,那么可以通过这种方式将它绑定到服务容器中去 # 从容器解析出来$mailer = $this->app->make # 返回一个实例$this->app['Mailer'] # 这样也可以public function __construct(Mailer $mailer) # 在控制器、事件监听器、队列任务、过滤器中进行注册
public function getCachedCommentsCountAttribute(){ return Cache::remember . ':comments_count', 15, function () { return $this->comments->count;}

事件Event

应用场景:
1.缓存机制的松散耦合,比如在获取一个资源时先看是否有缓存,有则直接读缓存,没有则走后短数据库,此时,通常做法是在原代码里面直接用if...else...进行判断,但有了缓存后,我们可以用事件来进行触发

我们使用唯一键值的cacheKey()方法缓存模型 15
分钟,然后简单地在闭包方法中返回评论计数值。

重要对象

$request->route() # 通过request获取Route对象

$route->parameters() # 获取路由上的参数,即不是GET和POST之外的,定义在路由上面的参数

注意,我们也用到了Cache::rememberForever()方法,靠着缓存机制的垃圾回收策略以删除过期的键值。我设置了一个定时器,以便在每隔
15 分钟的缓存刷新间隔里,缓存可在该时间的多数范围内有最高的命中率。

帮助函数

# intersect 获取request的字段来更新字段$record->update($request->intersect([ 'title', 'label', 'year', 'type']));str_contains('Hello foo bar.', 'foo'); # 判断给定字符串是否包含指定内容str_random; # 产生给定长度的随机字符串

cacheKey()方法要用到模型的唯一键值,并且在模型更新时对应缓存失效。下面是我的cacheKey实现代码:

错误和日志

logger用于直接输出DEBUG级别的日志,更好的是使用use IlluminateSupportFacadesLog;,如果storage/laravel.log下面找不到日志,那么可能是重定向到apache或者nginx下面去了

# 日志的用法Log::useFiles(storage_path().'/logs/laravel.log') # 如果发现无论什么都不输入到日志里面去,一是检查日志文件的权限,而是添加这个,直接指名日志文件Log::emergency;Log::alert;Log::critical;Log::error;Log::warning;Log::notice;Log::info('This is some useful information.');Log::debug();
public function cacheKey(){ return sprintf( "%s/%s-%s", $this->getTable, $this->updated_at->timestamp );}

Artisan Console

  • php artisna config:cache:
    把所有的配置文件组合成一个单一的文件,让框架能够更快地去加载。
  • 使用命令的方式执行脚本,这时候如果要打印一些日志信息,可以直接用预定义的方法,还能显示特定的颜色:

$this->info # 绿色$this->line # 黑色$this->comment # 黄色$this->question # 绿色背景$this->error # 红色背景

模型的cacheKey() 方法示例输出结果可能返回下面的字串信息:

测试

PHP的phpunit提供了很好的测试方式,Laravel对其进行了封装,使得易用性更高更方便。

# 访问页面$this->visit->click->seePageIs('/about-us') # 直接点击按钮并察看页面$this->seePageIs # 验证当前url的后缀是不是这个$this->visit->see('Laravel 5')->dontSee # 查看页面是否存在某个字符串或者不存在 # 用户登录$user = User::find$this->be # 直接在测试用例添加这个即可Auth::check() # 用户是否登录,如果已经登录返回true # 表单填写$this->type($text, $elementName) # 输入文本$this->select($value, $elementName) # 选择一个单选框或者下拉式菜单的区域$this->check($elementName) # 勾选复选框$this->attach($pathtofile, $elementName) # 添加一个文件$this->press($buttonTextOrElementName) # 按下按钮# 如果是复杂的表单,特别是包含了数组的表单,可以这样子<input name="multi[]" type="checkbox" value="1"><input name="multi[]" type="checkbox" value="2">这种的,就不能直接使用上面的方法了,只能怪上面的方法不够智能呀,解决方法是直接提交一个数组$this->submitForm('提交按钮', [ 'name' => 'name', 'multi' => [1, 2]]);# 测试$this->seeInDatabase('users', ['email' => 'hehe@example.com']) # 断言数据库中存在 # 模型工厂Model Factories,database/factories/ModelFactory.php,可以不用插入数据库,就能直接得到一个完整的Model对象,define指定一个模型,然后把字段拿出来填上想要生成的数据,例如$factory->define(AppUser::class, function (FakerGenerator $faker) { return [ 'name' => $faker->name, 'password' => bcrypt(str_random, 'remember_token' => str_random;// 使用的时候,直接这样,50表示生成50个模型对象factory(AppUser::class, 50)->create()->each(function { $u->posts()->save(factory(AppPost::class)->make;# 直接对控制器进行测试可以这样做public function setUp(){ $this->xxxController = new xxxController()}public function testIndex{ $re = $this->xxxController->index(new Request; var_dump($re->content); var_dump($re->isSuccessful;}

在实际的测试过程中,我有这样的几点体会:

  • 测试类本身就不应该继承的,因为单元测试本身就应该独立开来
  • 直接对控制器测试是一种简单直接有效的测试方法,而无需再单独给service或者model层进行测试
articles/1-1515650910

这个键值是由表名、模型id值及当前updated_at 的 timestamp
值组成。一旦我们触发这个模型,timestamp
值就会更新,并且我们的模型缓存就会相应地失效。

以下是Article模型的完整代码:

getTable, $this->updated_at->timestamp ); } public function comments() { return $this->hasMany; } public function getCachedCommentsCountAttribute() { return Cache::remember . ':comments_count', 15, function () { return $this->comments->count; }}

然后是关联的Comment模型:

belongsTo; }}

接下来做什么?

我已经向你展示了如何缓存一个简单的评论计数,但是如何缓存所有的评论呢?

public function getCachedCommentsAttribute(){ return Cache::remember . ':comments', 15, function () { return $this->comments; });}

你也可以选择将评论转换为数组替代序列化模型,只允许在前端对数据进行简单的数组访问:

public function getCachedCommentsAttribute(){ return Cache::remember . ':comments', 15, function () { return $this->comments->toArray;}

最后,
我在Article模型中定义了cacheKey()方法,但是你可能想要通过一个名为ProvidesModelCacheKey的trait来定义这个方法以便你可以在复合模型中使用或者在一个基础模型中定义所有模型扩展的方法。
你甚至可能想要为实现cacheKey()方法的模型使用使用契约。

我希望你已经发现这个简单的技术是十分有用的!

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持脚本之家。

发表评论

电子邮件地址不会被公开。 必填项已用*标注