Laravel指南 Laravel项目基本结构
十分推荐安装laravel-ide-helper
配置
.env
文件中,如果有空格,那么值需要用双引号包围,并且里面如果用\n
,那么必须转义\\n
如果.env
不起作用,可以尝试清理缓存php artisan cache:clear
laravel可以根据不同的系统环境自动选择不同的配置文件,例如,如果APP_ENV=testing
,那么会自动选择读取.env.testing
中的配置,如果有.env
则会被覆盖,特别是单元测试和artisan
命令中
项目目录文件最正确的配置
1 2 3 4 5 6 7 8 9 10 cd /var/www/html/laravel-project-root sudo chown -R $USER:www-data . sudo find . -type f -exec chmod 664 {} \; sudo find . -type d -exec chmod 775 {} \; sudo find . -type d -exec chmod g+s {} \; sudo chgrp -R www-data storage bootstrap/cache sudo chmod -R ug+rwx storage bootstrap/cache
Laravel的主配置文件将经常用到的文件集中到了根目录下的.env
目录下,这样更高效更安全。其内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 # 这里配置APP_ ENV=local APP_ DEBUG=true APP_ KEY=YboBwsQ0ymhwABoeRgtlPE6ScqSzeWZG # 这里配置数据库DB_ HOST=localhost DB_ DATABASE=test DB_ USERNAME=root DB_ PASSWORD=mysql CACHE_ DRIVER=file SESSION_ DRIVER=file QUEUE_ DRIVER=sync MAIL_ DRIVER=smtp MAIL_ HOST=mailtrap.io MAIL_ PORT=2525 MAIL_ USERNAME=null MAIL_ PASSWORD=null EXAMPLE_ PUBLIC_ KEY=abc\ndef # 要在配置里面换行,目前只有这种方式了,在读取的时候这样子读取: str_ replace("\\ n", "\n ", env('MSGCENTER_ PUBLIC_ KEY')) ARRAY={"slave":[{"host":"127.0.0.1","port":3306},{"host":"127.0.0.1","port":3307}]} # 目前配置文件也不支持数据,也只能这样,然后在使用的时候用json转换一下了json_ decode($ a, true)['slave']
还可以在该文件里配置其它的变量,然后在其它地方使用env(name, default)
即可访问。例如,读取数据库可以用config('database.redis.default.timeout', -1)
来。 全局配置文件.env
仅仅是一些常量的配置,而真正具体到哪个模块的配置则是在config
目录之下.同样,也可以动态改变配置:Config::set('database.redis.default.timeout')
另外,可以通过帮助函数来获取当前的应用环境:
1 2 3 4 $environment = App ::environment ();App ::environment ('local' ) app ()->environment ()
控制器
Restful资源控制器
资源控制器可以让你快捷的创建 RESTful 控制器
通过命令php artisan make:controller PhotoController --resource
创建一个资源控制器,这样会在控制器PhotoController.php
里面包含预定义的一些Restful的方法Route::resource('photo', 'PhotoController')
;
通过命令php artisan make:controller PhotoController --resource --model=Photo
可以直接将其与Model绑定
1 2 3 4 5 6 7 8 9 10 11 12 13 Route ::resource ('photo' , 'PhotoController' , ['only' => ['index' , 'show' ]]); Route ::resource ('photo' , 'PhotoController' , ['except' => ['create' , 'store' ]]); Route ::apiResource ('photo' , 'PhotoController' ); Route ::resource ('photo' , 'PhotoController' , ['names' => [ 'create' => 'photo.createa' // 资源路由命名 ]]) Route ::resource ('photos.comments' , 'PhotoCommentController' );public function show ($photoId , $commentId )
资源控制器对应的路由
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
Resources目录 resource
目录包含了视图views
和未编译的资源文件(如LESS、SASS或javascript),还包括语言文件lang
路由url 路由缓存:laravel里面使用route:cache Artisan
,可以加速控制器的路由表,而且性能提升非常显著。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 Route ::group (['namespace' => 'Cron' , 'middleware' => ['foo' , 'bar' ]], function(){ Route ::get ('/' , function() { }); Route ::get ('user/profile' , function() { }); }); Route ::resource ('wei/{who}' , 'WeixinController' );Route ::resource ('wei/{path?}' , 'TestController' )->where ('path' , '(.*)' ); public function index ($who ) {}Route ::resource ('photos.comments' , 'PhotoCommentController' );public function show ($photoId , $commentId )# 如果要获取嵌套资源的url ,可以这样子: route ('post.comment.store' , ['id' => 12 ] ) # 这样子就获取到id 为12的post 的comment 的创建接口地址 # 通配路由 Route ::get ('/{abc}' , 'TestController@test' ) ; public function boot ( ) { Route ::pattern ('abc' , '^(?backend|nova-api|nova|nova-vendor).[a-zA-Z0-9-_\/]+$]' ); parent ::route (); }
路由相关方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 URL::full (); url ()->full (); URL::current (); url ()->current ();Request ::url (); $request ->url ();Request ::path (); $request ->path ();Request ::getRequestUri (); $request ->getRequestUri ();Request ::getUri (); $request ->getUri ();Route ::currentRouteName () === 'businessEditView' URL::previous () url ()->previous ();Request ::url ();
分页 Larvel的分页主要靠Eloquent来实现,如果要获取所有的,那么直接把参数写成PHP_INT_MAX
就行了嘛
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 $request ->merge (['page' => 2 ]); Paginator ::currentPagesolver (function () use ($currentPage ) {return $currentPage }); $users = User ::where ('age' , 20 )->paginate (20 ); { 'total' : 50 , 'per_page' : 20 , 'current_page' : 1 , 'last_page' : 3 , 'next_page_url' : '...' , 'prev_page_url' : null , 'from' : 1 , 'to' : 15 , 'data' : [{}, {}] } public function ...( ) { return $this ->posts ()->paginate (20 ); } User ::paginate (20 )
数据库Model Laravel提供了migration和seeding为数据库的迁移和填充提供了方便,可以让团队在修改数据库的同时,保持彼此的进度,将建表语句及填充操作写在laravel框架文件里面并,使用migration来控制数据库版本,再配合Artisan命令,比单独管理数据库要方便得多。
配置文件 config/database.php
里面进行数据库引擎的选择,数据库能通过prefix
变量统一进行前缀的配置
数据库读写分离的配置(Laravel的读写分离仅仅是读写分离,在主库故障以后,程序无论是读写都会报连接错误,因为在程序启动的时候建立数据库连接默认都会建立一个写连接,又是一个坑),如果要解决这个问题,可以使用zara-4/laravel-lazy-mysql
,它原本是为解决主库连接慢而创造的,但是也能填这个坑。不过,另一方面,DBA必须保证主库的高可用,开发人员是可以不用考虑这一层面的,这是DBA的责任,而不是开发人员的责任,主库挂了,DBA应该立马用新的主库代替。
对于多slave
的配置,网上的教程感觉都有问题,看了下源码,正确的配置多读库并且几个读库的配置不一样,那么需要这样配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 'mysql' => [ 'driver' => 'mysql' , 'database' => 'test' , 'username' => 'root' , 'password' => 'password' , 'charset' => 'utf8' , 'collation' => 'utf8_unicode_ci' , 'strict' => false , 'read' => [ [ 'host' => '127.0.0.2' , 'port' => 3307 ], [ 'host' => '127.0.0.3' , 'port' => 3308 ] ], 'write' => [ 'host' => '127.0.0.1' , 'port' => 3306 , ] ],
建表操作 生成一个model: php artisan make:model user -m
,这样会在app
目录下新建一个和user表对应的model文件
1 2 3 4 5 6 7 <?php namespace App ;use Illuminate \Database \Eloquent \Model ;class Flight extends Model { }
加上-m
参数是为了直接在database/migrations
目录下生成其迁移文件,对数据库表结构的修改都在此文件里面,命名类似2016_07_04_051936_create_users_table
,对数据表的定义也在这个地方,默认会按照复数来定义表名:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 <?php use Illuminate \Database \Schema \Blueprint ;use Illuminate \Database \Migrations \Migration ;class CreateApplicationsTable extends Migration { public function up ( ) { Schema ::create ('applications' , function (Blueprint $table ) { $table ->increments ('id' ); $table ->timestamps (); }); DB::statement ('ALTER TABLE `' .DB::getTablePrefix ().'applications` comment "这里写表的备注"' ); DB::table ('users' ->insert ([])); } public function down ( ) { Schema ::drop ('applications' ); } }
当数据表定义完成过后,执行php artisan migrate
即可在真的数据库建表了
1 2 3 4 5 6 7 php artisan make:migration 操作名 # 生成迁移文件 php artisan schema:dump # 从数据库已有的表生成迁移文件 php artisan migrate # 建表操作,运行未提交的迁移 php artisan migrate --path=databases/migrations/ # 运行指定目录下的迁移,这里无法指定具体文件,只能指定文件夹 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函数里面:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 public function up ( ) { Schema ::table ('users' , function (Blueprint $table ) { $table ->string ('mobile' , 20 ) ->nullable () ->after ('user_face' ) ->comment ('电话号码' ) ->default ('' ) ->change (); $table ->renameColumn ('from' , 'to' ); $table ->dropColumn ('votes' ); $table ->dropColumn (['votes' , 'from' ]); $table ->string ('email' )->unique (); $table ->unique ('email' ); $table ->unique ('email' , 'nickname' ); $table ->index (['email' , 'name' ]); $table ->dropPrimary ('users_id_primary' ); $table ->dropUnique ('users_email_unique' ); $table ->dropIndex ('geo_state_index' ); $table ->json ('movies' )->default (new Expression ('(JSON_ARRAY())' )); $table ->timestamp ('created_at' )->useCurrent (); $table ->timestamp ('updated_at' )->useCurrentOnUpdate (); $table ->timestamp ('deleted_at' )->nullable (); }); Schema ::hasTable ('users' ); Schema ::hasColumn ('users' , 'email' ); Shcema ::rename ($from , $to ); Schema ::drop ('users' ); Schema ::dropIfExists ('users' ); }
表/Model的定义 1 2 3 4 5 6 7 8 9 10 11 12 class User extends Model { public $timestamps = false ; protected $primaryKey = 'typeid' protected $primaryKey = null ; public $incrementing = false ; protected $connection = 'second' ; protected $fillable = ['id' , 'name' ]; protected $table = 'my_flights' ; protected $appends = ['id2' ]; protected $visible = ['id' ]; protected $hidden = ['password' ]; }
字段的定义 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 $table ->increments ('id' ) $table ->string ('name' , 45 )->comment ('名字' ) $table ->boolean ('type' ) $table ->softDeletes () $table ->bigInteger ('' ) $table ->integer () $table ->integer ()->uninsign () $table ->integer ()->unsigned () $table ->mediumInteger ('' ) $table ->mediumInteger ('' )->unsign () $table ->mediumInteger ('' )->unsigned ()$table ->smallInteger ('' ) $table ->smallInteger ('' )->unsign () $table ->smallInteger ('' )->unsigned () $table ->tinyInteger ('' ) $table ->tinyInteger ('' )->unsign () $table ->tinyInteger ('' )->unsigned () $table ->float ('' ) $table ->text ('' ) $table ->dateTime ('created_at' ) ->nullable () ->unsigned () ->unsign () ->default ('' ) $table ->index ('user_id' )$table ->primary ('id' ) $table ->primary (array ('id' , 'name' )) $table ->integer ('user_id' )->unsigned (); $table ->foreign ('user_id' )->references ('id' )->on ('users' );
定义表之间的关系
直接在ORM里面进行表关系的定义,可以方便查询操作
5.4开始新增了withDefault
方法,在定义关系的时候,如果对象找不到那么返回一个空对象,而不是一个null
一对多hasMany 1 2 3 4 5 6 7 8 public function posts ( ) { return $this ->hasMany ('App\Post' ); } Users ::find (1 )->posts$this ->hasMany ('App\Post' , 'foreign_key' , 'local_key' )
一对一hasOne 1 2 3 4 5 public function father ( ) { return $this ->hasOne ('App\Father' ); } $this ->hasOne ('App\Father' , 'id' , 'father' );
相对关联belongsTo(多对一) 1 2 3 4 5 6 public function user ( ) { return $this ->belongsTo ('App\User' ); } Posts ::find (1 )->user
多对多关系belongsToMany 如果有三张表,users,roles,role_user其中,role_user表示users和roles之间的多对多关系。如果要通过user直接查出来其roles,那么可以这样子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 class User extends Model { public funciton roles () { return $this ->belongsToMany ('App\Role' , 'user_roles' , 'user_id' , 'foo_id' ); } } $roles = User ::find (1 )->roles; foreach ($user ->roles as $role ) { echo $role ->pivot->created_at; } return $this ->belongsToMany (Role ::class )->withPivot ('field1' , 'field2' )->withTimestamps ();$user ->posts ()->sync ([1 , 2 , 3 ]) $user ->posts ()->sync ([ # 如果要更新关联表的pivot字段,需要传入这样的数据结构 1 => ['abc' => 'def' ], 2 => ['def' => 'ghi' ] ])
多态关联 一个模型同时与多种模型相关联,可以一对多(morphMany)、一对一(morphOne)、多对多(mar)
例如: 三个实例,文章、评论、点赞,其中点赞可以针对文章和评论,点赞表里面有两个特殊的字段target_id
、target_type
,其中target_type
表示对应的表的Model,target_id
表示对应的表的主键值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 class Like extends Model { public function target ( ) { return $this ->morphTo (); } } class Post extends Model { public function likes ( ) { return $this ->morphMany ('App\Like' , 'target' ); } } class Comment extends Model { public function likes ( ) { return $this ->morphMany ('App\Like' , 'target' ); } } $comment ->likes;$comment ->likes;$this ->morphedByMany ('App\Models\Posts' , 'target' , 'table_name' );
数据库填充 Laravel使用数据填充类来填充数据,在app/database/seeds/DatabaseSeeder.php
中定义。可以在其中自定义一个填充类,但最 好以形式命名,如(默认填充类为DatabaseSeeder,只需要在该文件新建类即可,不是新建文件):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 class DatabaseSeeder extends Seeder { public function run ( ) { $this ->call (UsersTableSeeder ::class ); } } class UsersTableSeeder extends Seeder { public function run ( ) { DB::table ('users' )->delete (); App\User ::create ([ 'email' => 'admin@haofly.net' , 'name' => '系统管理员' , ]); } }
然后在Composer的命令行里执行填充命令
1 2 3 php artisan db:seed php artisan db:seed --class=UserTableSeeder # 执行指定的seed php artisan migrate:refresh --seed # 回滚数据库并重新运行所有的填充
ORM操作 Laravel 查询构建器使用 PDO 参数绑定来避免 SQL 注入攻击,不再需要过滤以绑定方式传递的字符串。但是需要注意的是当使用whereRaw/selectRaw
等能嵌入原生语句的时候,要么用bind的方式(即将用户输入作为第二个参数传入)要么就对输入的字符进行严格的过滤
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 DB::statement ('drop table xxx' ); DB::select ('select xxx' ); DB::connection ('default' )->enableQueryLog (); ... dd (DB::connection ('statistics' )->getQueryLog ()); DB::getTablePrefix (); $user ->getTable (); User ::all (); User ::all (array ('id' , 'name' )); User ::find (1 ); User ::find ([1 ,2 ,3 ]); optional (User ::find (1 ))->id; User ::findOrFail (1 ); User ::where ([ ['id' , 1 ], ['name' , 'haofly' ] ); User ::where (); User ::where ('field' , 'like' , '%test%' ); User ::where ('field' , 'like' , '%{$keyword}%' ); User ::where ('field' , 'regexp' , 'abc' ); User ::where ()->limit (2 ); User ::where ()->exists (); User ::whereIn ('name' , ['hao' , 'fly' ]); User ::whereNull ('name' ); User ::whereNotNull ('name' ); User ::whereBetween ('score' , [1 , 100 ]); User ::whereNotBetween ('score' , [1 , 100 ]); User ::whereDate ('created_at' , '2017-05-17' );User ::whereMonth ('created_at' , '5' );User ::whereDay ('created_at' , '17' );User ::whereYear ('created_at' , '2017' );User ::whereRaw ('name="wang" and LENGT(name) > 1' ); User ::whereColumn ('first_field' , 'second_field' ); User ::where (...)->orWhere (); User ::where (...)->where (function($query ) { $query ->where (...)->orWhere (...); }) User ::where ('...' )->orWhere (['a' =>1 , 'b' =>2 ]); User ::where ()->firstOrFail () User ::where ('user_id' , 1 )->get ()User ::where (...)->first () User ::find (1 )->logs->where (...) User ::->where ('updated_at' , '>=' , date ('Y-m-d H:i' ).':00' )->where ('updated_at' , '<=' , date ('Y-m-d H:i' ).':59' ) User ::find (1 )->sum ('money' ) User ::where (...)->get ()->pluck ('name' ) User ::where (DB::raw ('YEAR(created_at)' ), $year ); User ::modelKeys () User ::select ('name' )->where () User ::where ()->get (['id' , 'name' ])User ::where (...)->pluck ('name' ) User ::withTrashed ()->where () User ::onlyTrashed ()->where () User ::find (1 )->posts User ::find (1 )->posts () User ::find (1 )->posts->count () User ::all ()->orderBy ('name' , 'desc' ) User ::all ()->latest () User ::all ()->oldest () User ::all ()->inRandomOrder ()->first (); User ::select ('name' )->distinct ()->get () User ::select ('name' )->join ('posts' , 'users.id' , '=' , 'posts.user_id' )->where (...); User ::select ('name' )->leftJoin ('posts' , 'users.id' , '=' , 'posts.user_id' )->where (...);User ::select ('name' )->leftJoin ('posts' , 'users.id' , '=' , DB::raw ('posts.user_id AND users.type=xxx' ))->where (...); User ::select ('name' )->leftJoin ('posts' , function($join ) { $join ->on ('users.id' , '=' , 'posts.user_id' ) ->on ('users.type' , '=' , 'xxx' ) }) $posts = Post ::has ('comments' )->get (); $posts = Post ::has ('comments' , '>=' , 3 )->get (); $posts = Post ::has ('comments.votes.user' )->get (); $posts = Post ::whereHas ('comments' , function($query ) { $query ->where ('content' , 'like' , 'foo%' )->whereHas ('user' ); }); public function getNameAttribute ( ) { return $this ->firstname.$this ->lastname; } 那么在外部可以直接$user ->name进行访问 Model ::firstOrCreate () Model ::firstOrNew () Model ::updateOrCreate (array (), array ())$User ::find (1 )->phones ()->create ([]) $author ->posts ()->save ($post ); $author ->posts ()->associate ($post ); $author ->posts ()->saveMany ([$post1 , $post2 ]) $post ->author ()->save (Author ::find (1 )) $author ->posts ()->detach ([1 ,2 ,3 ])$author ->posts ()->attach ([1 ,2 ,3 =>['expires' =>$expires ]])$datas = [ ['field1' => 'value1' , 'field2' => 'value2' ], ['field1' => 'value3' , 'field2' => 'value4' ], ]; Post ::insert ($datas ); $user ->restore (); $user ->fill (['name' => 'wang' ])->save () $user ->update (['name' => 'wang' ]) $user ->increment ('age' , 5 ) $user ->decrement ('age' , 5 ) $user ->save (['timestamps' =>false ]); $user ->replicate () ->fill (['name' => $newName ]) ->save () $user ->delete () $user ->forceDelete () DB::beginTransaction (); DB::connection ('another' )->begintransaction (); DB::rollback (); DB::commit (); $newUser = $user ->replicate ();$newUser ->save (); User ::whereRaw ('FIND_IN_SET(region_id, abc)' )
查询缓存 With/load(预加载/渴求式加载/eager load)
with/load在laravel的ORM中被称为预加载,作用与关联查询上,能有效缓解N+1查询问题
通常的做法是在一次请求开始处理的时候一次性把所有需要用到的关联关系取出来,例如: Auth::user()->load('detail', 'posts:name', 'posts.comments')
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 $posts = App\Post ::all ();foreach ($posts as $post ) { var_dump ($post ->user->name); } $posts = App\Post ::with ('user' )->get ();foreach ( $books as $book ) { var_dump ($post ->user->name); } App\Post ::with ('user' , 'author' )->get ();App\Post ::with ('user.phone' )->get (); App\Post ::with ('user:name,nickname' )->get ();$users = User ::with (['posts' => function ($query ) { $query ->where ('title' , '=' , 'test' )->orderBy ('id' , 'desc' ); }])->get (); $posts = Post ::all ();$posts ->laod ('user' , 'category' );
Cache 缓存的是结果
ORM对象方法 1 2 3 4 $posts = User ::find (1 )->posts () $posts = User ::find (1 )->posts $posts ->get ()
Collection对象 1 2 3 4 $obj ->count () $obj ->first () $obj ->last () $obj ->isEmpty ()
Model对象的事件 可以在任何的ServiceProvinder
的boot
方法中针对model
级别进行类似事件的回调,例如
1 Post ::updated (function ($post ) {})
可供监听的事件有updating/created/updating/updated/deleting/deleted/saving/saved/restoring/restored
。其中updated
仅仅是字段的值真的变化了才会去更新。
DatabaseServiceProvider Laravel自带一个特殊的DatabaseServiceProvider
,用于管理数据库的连接,在config/app.php
里面进行声明。
1 Model ::setConnectionResolver ($this ->app['db' ]);
认证相关
认证相关路由直接用Auth:routes()
就注册了所有的路由了
可以通过php artisan route:list
查看到系统自带的跟auth认证有关的一些路由
可以在AppServiceProvider.php -> boot
中使用ResetPassword::toMailUsing(function($notifiable, $token) {})
来修改发送重置密码邮件的逻辑
用户系统 1 2 Auth ::loginUsingId (123 , TRUE ); Auth ::login ($user );
授权Policy Policy主要用于对用户的某个动作添加权限控制,这里的Policy
并不是对Controller
的权限控制.
权限的注册在app/Providers/AuthServiceProvider.php
里面,权限的注册有两种:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 class AuthServiceProvider extends ServiceProvider { public function boot (GateContract $gate ) { $this ->registerPolicies ($gate ); $gate ->define ('update-post' , function($user , $post ) { return $user ->id === $post ->user_id; } ) $gate ->define ('update-post' , 'Class@method' ); $gate ->before (function ($user , $ability ) { if ($user ->isSuperAdmin ()) return true ; }); $gate ->after (function() {}) } } protected $policies = [ Post ::class => PostPolicy ::class , ]; class PostPolicy { public function before ($user , $ability ) { if ($user ->isSuperAdmin ()) {return true } } public function update (User $user , Post $post ) { return true ; } }
权限的使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 use Gate ;if (Gate ::denies ('update-post' , $post )) {abort (403 , 'Unauthorized action' )}Gate ::forUser ($user )->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 ) $user ->can ('create' , Post ::class ) @can ('update-post' , $post ) <html> @endcan @can ('update-post' , $post ) <html1> @else <html2> @endcan @can ('create' , \App\Post ::class ) @can ('create' , [\App\Comment ::class , $post ])
Session/Cookie 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 public function index ( ) { Cookie ::queue ('test' , 'value' , 10 ); return view ('index' ); return view ('index' )->withCookie ( cookie ('test' , 'value' , 10 ); // 或者Cookie ::make ('test' , 'value' , 10 ) ); } use Illuminate \Cookie \Middleware \EncryptCookies as BaseEncrypter ;class EncryptCookies extends BaseEncrypter { protected $except = [ 'userid' , ]; } public function index (Request $request ) { $cookie = $request ->cookie ('test' ); $cookies = $request ->cookie (); } $cookie = Cookie ::forget ('test' );return view ('index' )->withCookie ($cookie );
中间件 Authenticate中间件
任务队列Job
通过php artisan make:job CronJob
新建队列任务,会在app/Jobs
下新建一个任务
队列超时自动重试的配置在config->queue.php->retry_after
中,最好设置成300,否则设置小了即使会成功也可能会超时生成一个失败的任务
失败的job默认会保存在数据库中的failed_jobs
中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 public function __construct (ResourceService $resourceService ) { $this ->resourceService = $resourceService ; } dispatch (new App\Jobs\PerformTask );$jog = (new App\Jobs \..)->onQueue ('name' );dispatch ($jog );$job = (new App\Jobs \..)->delay (60 );Redis ::zcard (sprintf ('queues:%s:delayed' , JobClass ::NAME )); public function failed ( ) { echo '失败了' ; } php artisan queue:retry all php artisan queue:retry 5 php artisan queue:forget 5 php artisan queue:flush
队列消费
queue:work
: 最推荐使用这种方式,它比queue:listen
占用的资源少得多,不需要每次启动框架。但是代码如果更新就需要用queue:restart
来重启
如果要使用crontab
来管理队列,可以php artisan queue:work --stop-when-empty
可以设置每分钟执行一次,只要队列为空就能停掉,当然,这个目测并不会导致进程太多的情况,因为进程多消费就快了,我反而觉得这是动态增减消费者数量的好方法
需要注意的是
不要在Jobs
的构造函数里面使用数据库操作,最多在那里面定义一些传递过来的常量,否则队列会出错或者无响应
job如果有异常,是不能被catch的,job只会重新尝试执行该任务,并且默认会不断尝试,可以在监听的时候指定最大尝试次数--tries=3
不要将太大的对象放到队列里面去,否则会超占内存,有的对象本身就有几兆大小
一个很大的坑是在5.4及以前,由于queue:work
没有timeout参数,所以当它超过了队列配置中的expire
时间后,会自动重试,但是不会销毁以前的进程,默认是60s,所以如果有耗时任务超过60s,那么队列很有可能在刚好1分钟的时候自动新建一条一模一样的任务,这就导致数据重复的情况。
如果是使用redis作为队列,那么队列任务默认是是Job的NAME命名,例如
queues:NAME
,是一个列表,过期时间为-1,没有消费者的情况是会一直存在于队列中。而如果是延迟执行的任务,则是单独放在一个有序集合中,其key为queues:NAME:delayed
,其score
值就是其执行的时间点。另外,queues:NAME
存储的是未处理的任务,queue:default:reserved
存储的是正在处理的任务,这是个有序集合,用ZRANGE queues:NAME:reserved 0 -1 WITHSCORES
查看其元素。
缓存Cache Laravel
虽然默认也是用的redis
,但是和redis
直接存取相比,方便多了。Cache
能够直接将一个对象序列化后直接以key-value
的形式存放到redis
中。缓存的配置文件在config/cache.php
,可以指定redis
的连接。
用到缓存,我的建议是,从model
层入手,仅仅基于model
的增删该查进行缓存,而不是直接缓存最上层控制器的结果,如果缓存控制器结果,那么下面相关的所有model
在变化的时候都得进行改变,这样就会相当复杂。当然具体业务具体分析,如果你仅仅是返回一个静态的页面呢。
1 2 3 4 5 6 Cache ::remember ('redis-key' , 10 , function () { return User ::find (1 ); }); Cache ::get ('key' , 'default' );Cache ::put ('key' , 'value' , $minutes );
如果想在Model
进行什么更改以后让缓存消失或者更新缓存,那么可以用Model
的事件去监听。
如何缓存关联关系 由于关系的定义函数并没有直接查询数据库而是一个pdo对象,所以不能直接对关系进行缓存,折衷方法是可以添加一个方法,例如
1 2 3 4 5 6 7 8 9 10 class User extends Model {} public function post ( ) { return $this ->hasMany (...); } public function getPost ( ) { Cache ::remember (..., 10 , funciton () { return $this ->post; })) } }
数据库为null的时候的缓存问题以及数据库事务的处理方法。 在5.4以前,remember
和get
在获取的时候,无论是没有该key
还是value
为null
,得到的结果都是一样的,这样remember
在每次都会先从redis
读一次,没找到再在回调函数里面读数据库,然后把null值返回,最后再把null值写入缓存。虽然缓存多次读写没毛病,但是这里数据库也执行了很多次无效查询。我的解决办法用Redis
去查询key是否真的存在。
如果当前在数据库的事务里面,并且事务进行了回滚,那么依赖于Redis
的Cache
并不会自动回滚,可能导致数据不一致。我的解决办法是当有事务发生的时候,不进行缓存的读操作,并且在查询的时候直接将该key进行删除,无论事务里面对该model进行了什么操作,保证数据一致性。
两个问题的解决办法是这样的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 static public function remember ($key , $minute , Closure $callback ) { if (DB::transactionLevel () === 0 ) { $value = Redis ::connection (self ::REDIS_CACHE )->get ($key ); if ($value == 'N;' ) return null ; if (is_null ($value )) return Cache ::remember ($key , $minute , $callback ); return is_numeric ($value ) ? $value : unserialize ($value ); } else { Cache ::forget ($key ); return $callback (); } } static public function existsInCache ($key ) { return Redis ::connection (self ::REDIS_CACHE )->exists ( sprintf ('%s:' . $key , config ('cache.prefix' )) ); }
事件 就是实现了简单的观察者模式,允许订阅和监听应用中的事件。用法基本上和队列一致,并且如果用上队列,那么代码执行上也和队列一致了。
事件的注册 事件的定义 事件监听器 事件的触发 服务容器 Laravel核心有个非常非常高级的功能,那就是服务容器,用于管理类的依赖,可实现自动的依赖注入。比如,经常会在laravel的控制器的构造函数中看到这样的代码:
1 2 3 function function __construct (Mailer $mailer ) { $this ->mailer = $mailer }
但是我们却从来不用自己写代码去实例化Mailer,其实是由Laravel的服务容器自动去提供类的实例化了。
1 2 3 4 5 6 7 8 9 10 11 12 13 $this ->app->bind ('Mailer' , function($app ){ return new Mailer ('一些构造参数' ) }); $this ->app->singleton ('Mailer' , function($app ){ return new Mailer ('一些构造参数' ) }) $this ->app->instance ('Mailer' , $mailer ) $mailer = $this ->app->make ('Mailer' ) $this ->app['Mailer' ] public function __construct (Mailer $mailer ) # 在控制器、事件监听器、队列任务、过滤器中进行注册
事件Event 应用场景:
1.缓存机制的松散耦合,比如在获取一个资源时先看是否有缓存,有则直接读缓存,没有则走后端数据库,此时,通常做法是在原代码里面直接用if...else...
进行判断,但有了缓存后,我们可以用事件来进行触发。
2.Illuminate\\Database\\Events\\QueryExecuted
监听数据库相关事件来进行后续处理
Service Provider Laravel提供了很方便的注入服务的方法,那就是service provider
,当写完一个service provider
以后,在config/app.php
的provider里面添加该类名称即可实现注入。最重要的两个方法:绑定(Binding)
和解析(Resolving)
。
1 2 3 4 5 6 7 8 9 App ()->getLoadedProviders (); $this ->app->make ('Foo' );$foo = $this ->app['Foo' ];$this ->app->resolving (function ($object , $container ) {}); $this ->app->resolving ('db' , function () {});
这样就可以给laravel编写第三方扩展包了,例如
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 <?php use Illuminate \Support \Facades \Config ;use Illuminate \Support \ServiceProvider ;class TestServiceProvider extends ServiceProvider { protected $defer = true ; public function boot ( ) { $this ->publishes ([realpath (__DIR__ .'/../../config/api.php' ) => config_path ('api.php' )]); $this ->mergeConfigFrom (__DIR__ .'/config/test.php' , 'database' ); } public function register ( ) { Config ::set ('database.redis' , []); } public function provides ( ) { return [Connection ::class ]; } }
Facades外观 使用外观模式提供静态的接口去访问注册到IoC容器中的类,并且配以别名,这样的好处是,使用起来简单一些,不用写很长的类名。
重要对象 Route
Mail
发送邮件相关功能
to
: 邮件接收人,cc
: 抄送对象,bcc
: 暗抄送对象
1 2 3 4 5 6 7 8 9 10 11 Mail ::to ($email ) ->cc (['admin@haofly.net' ,'admin1@haofly.net' ]) ->send ('document' ); class MyEmailSender extends Mailable () {}$sender = new MyEmailSender ();foreach ($emails as $email ) { Mail ::to ($email )->queue ($sender ); }
Crypt Laravel
通过Mcrypt PHP
扩展提供AES加密功能。一定要设置config/app.php
中的key
1 2 $encrypted = Crypt ::encrypt ('password' );$decrypted = Crypt ::decrypt ($encrypted );
错误和日志
日志模式: single
表示输出到storage/log/laravel.log
中,daily
表示按天输出到storage/log/
目录下,syslog
会输出到系统日志中/var/log/message
,errorlog
跟PHP的error_log
函数一样输出到php的错误日志中
logger
用于直接输出DEBUG
级别的日志,更好的是使用use Illuminate\Support\Facades\Log;
,如果storage/laravel.log
下面找不到日志,那么可能是重定向到apache
或者nginx
下面去了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 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 ();Log ::useDailyFiles ('路径' , 30 , 'debug' ) $monolog = Log ::getMonolog (); $monogo ->getHandlers (); $handler ->setFormatter (new CustomFormatter ()); class CustomFormatter extends Monolog \Formatter \LineFOrmatter { public function format (array $record ) { $msg = [ $record ['datetime' ]->format ('Y-m-d H:m:s.u' ), '[TxId : ' . '' . ' , SpanId : ' . '' . ']' , '[' . $record ['level_name' ] .']' , $record ['message' ], "\n" , ]; return implode (' ' , $msg ); } } $logStreamHandler = new StreamHandler ('路径' , Logger ::DEBUG );$logStreamHandler ->setFormatter (new CustomFormatter ());Log ::getMonolog ()->pushHandler ($logStreamHandler );
自定义错误处理类 Laravel里面所有的异常默认都由App\Exceptions\Handler
类处理,这个类包含report
(用于记录异常或将其发送到外部服务)和render
(负责将异常转换成HTTP响应发送给浏览器)方法。render是不会处理非HTTP异常的,这点要十分注意。
自定义未认证/未登陆的错误信息或重定向 1 2 3 4 5 6 7 8 9 10 class Handler extends ExceptionHandler { protected function unauthenticated ($request , AuthenticationException $exception ) { return $request ->expectsJson () ? response ()->json (['message' => 'Unauthenticated.' ], 401 ) : redirect ()->guest (route ('authentication.index' )); }
统一的异常处理 Laravel可以在app/Exceptions/Handler.php
里面自定义统一处理异常,需要注意的是,验证异常是不会到这个Handler里面的,验证失败的时候,会抛出HttpResponseException
,它并不继承于HttpException
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public function report (Exception $e ) { if ($e instanceoof NotFoundException) { throw new NotFoundHttpException ; } if ($e instanceof HttpException) return parent ::report ($e ); $request = request (); $log = [ 'msg' => $e ->getMessage (), 'file' => $e ->getFile (), 'line' => $e ->getLine (), 'request_path' => $request ->getPathInfo (), 'request_body' => $request ->all (), ]; Log ::error (json_encode ($log )); throw new ResourceException ('System Exception' ); }
Artisan Console
php artisan serve --port=80
: 运行内置的服务器
php artisna config:cache
: 把所有的配置文件组合成一个单一的文件,让框架能够更快地去加载。
queue:work
从5.3开始默认就是daemon
,不需要加—daemon
参数了
queue:work
和queue:listen
的区别是,前者不用每次消费都重启整个框架,但是代码变更后前者必须手动重启命令
使用命令的方式执行脚本,这时候如果要打印一些日志信息,可以直接用预定义的方法,还能显示特定的颜色:
1 2 3 4 5 $ this->info('' ) $ this->line('' ) $ this->comment('' ) $ this->question('' ) $ this->error('' )
Command添加参数
1 2 3 protected $signature = 'test:test {field?} {--debug}' ; $this ->argument ('field' ); $this ->option ('debug' );
命令行直接调用
1 2 3 4 5 6 7 8 use Illuminate \Support \Facades \Artisan ;$exitCode = Artisan ::call ('email:send' , [ 'user' => 1 , '--queue' => 'default' ]); php artisan test:test
定时任务 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 crontab -u www -e * * * * * php /data/www/html/furion/artisan schedule:run >> /dev/null 2 >&1 class Kernel extends ConsoleKernel { protected $commands = [ ]; protected function schedule (Schedule $schedule ) { $schedule ->command ('test' ) ->everyMinute () ->days ([0 , 3 ]) ->days ([Schedule ::SUNDAY , Schedule ::WEDNESDAY ]) ->between ('7:00' , '22:00' ) ->unlessBetween ('23:00' , '4:00' ) ->when (function () {return true ;}) ->skip (function () {return true ;}) ->environments (['staging' , 'production' ]) ->at ('2:00' ) ->onOneServer () ->before (function(){}) ->after (function () {}) ->onSuccess (function () {}) ->onFailure (function () {}) ->pingBefore ($url ) ->thenPing ($url ) ->pingBeforeIf ($condition , $url ) ->thenPingIf ($condition , $url ) ->pingOnSuccess ($url ) ->pingOnFailure () ->emailOutputTo ('haoflynet@gmail.com' ) ->sendOutputTo ($filePath ) ->appendOutputTo ($fileCronLog ); $schedule ->call (function () { DB::table ('recent_users' )->delete (); })->daily (); $schedule ->job (new Heartbeat )->everyFiveMinutes (); $shcedule ->exec ('node /home/forge/script.js' )->daily (); $schedule ->call ('App\Http\Controllers\MyController@test' )->everyMinute (); } }
缓存清理
1 2 3 4 php artisan cache:clear php artisan route:clear php artisan config:clear php artisan view:clear
单元测试 1 2 php artisan make:test OrderTest --unit ./vender/bin/phpunit tests/unit
框架扩展/管理者/工厂 Laravel有几个”Manager”类,用于管理一些基本的驱动组件,例如数据库管理类、缓存管理类、会话管理类、用户验证管理、队列管理。这种类负责根据配置来创建一个驱动。
1 2 3 4 $this ->app->resolving ('db' , function ($db ) { return new Connection ($config ); });
测试 PHP的phpunit提供了很好的测试方式,Laravel对其进行了封装,使得易用性更高更方便。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 $this ->visit ('/' )->click ('About' )->seePageIs ('/about-us' ) $this ->seePageIs ('/next' ) $this ->visit ('/' )->see ('Laravel 5' )->dontSee ('Rails' ) $user = User ::find (1 )$this ->be ($user ) Auth ::check () $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' ]) $factory ->define (App\User ::class , function (Faker\Generator $faker ) { return [ 'name' => $faker ->name, 'password' => bcrypt (str_random (10 )), 'remember_token' => str_random (10 ), ] }); factory (App\User ::class , 50 )->create ()->each (function($u ) { $u ->posts ()->save (factory (App\Post ::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 ()); } ./vendor/bin/phpunit tests/xxxTest.php
在实际的测试过程中,我有这样的几点体会:
测试类本身就不应该继承的,因为单元测试本身就应该独立开来
直接对控制器测试是一种简单直接有效的测试方法,而无需再单独给service或者model层进行测试
性能优化 1 2 3 php artisan clear-compiled && php artisan cache:clear && php artisan config:clear && php artisan route:clear php artisan optimize --force && php artisan config:cache && php artisan api:cache
相关文章
用Laravel拥抱异常
将SQL语句直接转换成Laravel的语法
Laravel请求生命周期
如何少写PHP”烂”代码 : 更好的MVC分层实践
打造 Laravel 优美架构 谈可维护性与弹性设计
laravelio/portal : 一个很好的参考项目,连测试都写得非常好
octobercms : laravel写的cms系统
[Laravel 从学徒到工匠系列] 目录结构篇 : 详细解释了laravel为何没有Model目录
老司机带你深入理解 Laravel 之 Facade
Laravel项目深度优化指南
如何在Laravel中使用PHP的装饰器模式 : 这篇文章中的仓库模式也是十分有用的
laravel-query-builder : 一个查询构造器,可以直接从API请求中快速构建Eloquent查询,看起来简单,但是也有一定的学习成本,我还是懒得去弄