1、路由命名——给路由起个名字 1.1 基本使用 我们使用as关键字来为路由命名:
|
1 2 3 |
<a title="View all posts in Route" href="http://laravelacademy.org/tags/route" target="_blank">Route</a>::get('/hello/laravelacademy',['as'=>'academy',function(){ return 'Hello <a title="View all posts in Laravel" href="http://laravelacademy.org/tags/laravel" target="_blank">Laravel</a>Academy!'; }]); |
路由命名可以让我们在使用route函数生成指向该路由的URL或者生成跳转到该路由的重定向链接时更加方便:
|
1 2 3 |
Route::get('/testNamedRoute',function(){ return route('academy'); }); |
我们在浏览器中访问http://laravel.app:8000/testNamedRoute时输出http://laravel.app:8000/hello/laravelacademy,然后我们修改上述闭包内代码:
|
1 2 3 |
Route::get('/testNamedRoute',function(){ return redirect()->route('academy'); }); |
再次在浏览器中访问http://laravel.app:8000/testNamedRoute时会跳转到http://laravel.app:8000/hello/laravelacademy。 我们甚至还可以在使用带参数的路由命名:
|
1 2 3 |
Route::get('/hello/laravelacademy/{id}',['as'=>'academy',function($id){ return 'Hello LaravelAcademy '.$id.'!'; }]); |
对应的测试路由定义如下:
|
1 2 3 |
Route::get('/testNamedRoute',function(){ return redirect()->route('academy',['id'=>1]); }); |
这样,当我们在浏览器中访问http://laravel.app:8000/testNamedRoute时会跳转到http://laravel.app:8000/hello/laravelacademy/1 1.2 路由分组时路由命名方式 再来看一个更复杂的例子,使用路由分组时如何定义路由命名?官网文档提供的例子如下:
|
1 2 3 4 5 |
Route::group(['as' => 'admin::'], function () { Route::get('dashboard', ['as' => 'dashboard', function () { // }]); }); |
在Route门面的group方法中使用一个as关键字来指定该路由群组中所有路由的公共前缀,然后再在里面每个路由中使用as关键字为该路由命名。 这样我们可以通过如下方式来生成该路由URL:
|
1 2 3 |
Route::get('/testNamedRoute',function(){ return route('admin::dashboard'); }); |
2、路由分组 路由分组就是将一组拥有相同属性(中间件、命名空间、子域名、路由前缀等)的路由使用Route门面的group方法聚合起来。 2.1 中间件 首先我们在应用根目录下运行如下Artisan命令生成一个测试用的中间件TestMiddleware:
|
1 |
php artisan make:middleware TestMiddleware |
这样会在/app/Http/Middleware目录下生成一个TestMiddleware.php文件,打开该文件编辑TestMiddleware类的handle方法如下:
|
1 2 3 4 5 6 |
public function handle($request, Closure $next) { if($request->input('age')<18) return redirect()->route('refuse'); return $next($request); } |
我们在中间件中定义这段业务逻辑的目的是年龄18岁以下的未成年人不能访问。 然后我们打开/app/Http/Kernal.php文件,新增TestMiddleware到Kernel的$routeMiddleware属性:
|
1 2 3 4 5 6 |
protected $routeMiddleware = [ 'auth' => \App\Http\Middleware\Authenticate::class, 'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class, 'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class, 'test' => \App\Http\Middleware\TestMiddleware::class, ]; |
接下来我们在routes.php中定义路由如下:
|
1 2 3 4 5 6 7 8 9 10 11 12 |
Route::group(['middleware'=>'test'],function(){ Route::get('/write/laravelacademy',function(){ //使用Test中间件 }); Route::get('/update/laravelacademy',function(){ //使用Test中间件 }); }); Route::get('/age/refuse',['as'=>'refuse',function(){ return "未成年人禁止入内!"; }]); |
这样当我们在浏览器中访问http://laravel.app:8000/write/laravelacademy?age=15或者http://laravel.app:8000/update/laravelacademy?age=15时就会跳转到http://laravel.app:8000/age/refuse,并显示:
|
1 |
未成年人禁止入内! |
2.2 命名空间 默认情况下,routes.php中的定义的控制器位于App\Http\Controllers命名空间下,所以如果要指定命名空间,只需指定App\Http\Controllers之后的部分即可:
|
1 2 3 4 5 6 7 8 |
Route::group(['namespace' => 'LaravelAcademy'], function(){ // 控制器在 "App\Http\Controllers\LaravelAcademy" 命名空间下 Route::group(['namespace' => 'DOCS'], function() { // 控制器在 "App\Http\Controllers\LaravelAcademy\DOCS" 命名空间下 }); }); |
2.3 子域名 子域名可以通过domain关键字来设置:
|
1 2 3 4 5 6 7 8 |
Route::group(['domain'=>'{service}.laravel.app'],function(){ Route::get('/write/laravelacademy',function($service){ return "Write FROM {$service}.laravel.app"; }); Route::get('/update/laravelacademy',function($service){ return "Update FROM {$service}.laravel.app"; }); }); |
这样我们在浏览器中访问http://write.laravel.app:8000/write/laravelacademy,则输出
|
1 |
Write FROM write.laravel.app |
访问http://update.laravel.app:8000/write/laravelacademy时,则输出:
|
1 |
Write FROM update.laravel.app |
注意:要想让子域名解析生效,需要在hosts中绑定IP地址 2.4 路由前缀 如果路由群组中的所有路由包含统一前缀,则我们可以通过在group方法中设置prefix属性来指定该前缀:
|
1 2 3 4 5 6 7 8 |
Route::group(['prefix'=>'laravelacademy'],function(){ Route::get('write',function(){ return "Write LaravelAcademy"; }); Route::get('update',function(){ return "Update LaravelAcademy"; }); }); |
这样我们就可以通过http://laravel.app:8000/laravelacademy/write或者http://laravel.app:8000/laravelacademy/update来访问对应的操作。 我们甚至还可以在路由前缀中指定参数:
|
1 2 3 4 5 6 7 8 |
Route::group(['prefix'=>'laravelacademy/{version}'],function(){ Route::get('write',function($version){ return "Write LaravelAcademy {$version}"; }); Route::get('update',function($version){ return "Update LaravelAcademy {$version}"; }); }); |
这样我们在浏览器中访问http://laravel.app:8000/laravelacademy/5.1/write,则对应会输出:
|
1 2 3 4 |
Write LaravelAcademy 5.1 from:<a href="http://laravelacademy.org/post/417.html">http://laravelacademy.org/post/417.html</a> |
1、路由基本使用示例 1.1 默认示例 Laravel中所有路由定义在/app/Http/routes.php文件中,该文件默认定义了应用的首页路由:
|
1 2 3 |
<a title="View all posts in Route" href="http://laravelacademy.org/tags/route" target="_blank">Route</a>::get('/', function () { return view('welcome'); }); |
这段代码的意思是:当访问应用首页http://laravel.app:8000(使用Homestead虚拟机作为开发环境)的时候,返回/resources/views/welcome.blade.php视图中的内容并渲染到浏览器页面中: 以上是应用自带的路由示例,下面我们来自定义一些示例来演示路由的基本使用。 1.2 GET请求路由定义 对页面常见的请求方式有GET和POST,上面这个例子就是使用GET路由的例子,接下里来我们自定义一个/hello请求:
|
1 2 3 |
Route::get('/hello',function(){ return "Hello Laravel[GET]!"; }); |
我们在浏览器中输入http://laravel.app:8000/hello,以上代码在浏览器中输出:
|
1 |
Hello Laravel[GET]! |
1.3 POST请求路由示例 然后我们来演示一个POST请求的例子:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
Route::get('/testPost',function(){ $csrf_token = csrf_token(); $form = <<<FORM <form action="/hello" method="POST"> <input type="hidden" name="_token" value="{$csrf_token}"> <input type="submit" value="Test"/> </form> FORM; return $form; }); Route::post('/hello',function(){ return "Hello Laravel[POST]!"; }); |
首先我们定义一个/testPost页面用于提交POST请求表单,在http://laravel.app:8000/testPost页面点击“Test”按钮,页面跳转到http://laravel.app:8000/hello并显示:
|
1 |
Hello Laravel[POST]! |
表明这是通过POST请求访问而非GET请求。 1.4 其它便捷路由定义 还可以使用Route门面上的match方法匹配多种请求方式:
|
1 2 3 |
Route::match(['get','post'],'/hello',function(){ return "Hello Laravel!"; }); |
当然还使用更方便的any方法匹配所有请求方式:
|
1 2 3 |
Route::any('/hello',function(){ return "Hello Laravel!"; }); |
效果都一样。 2、路由参数使用示例 2.1 必选参数
|
1 2 3 |
Route::get('/hello/{name}',function($name){ return "Hello {$name}!"; }); |
在浏览器中访问http://laravel.app:8000/hello/Laravel输出:
|
1 |
Hello Laravel! |
当然还可以指定多个参数:
|
1 2 3 |
Route::get('/hello/{name}/by/{user}',function($name,$user){ return "Hello {$name} by {$user}!"; }); |
这样在浏览器中访问http://laravel.app:8000/hello/Laravel/by/Laravel学院则会输出:
|
1 |
Hello Laravel by Laravel学院! |
注意以上参数是必选的,如果没有输入参数会抛出MethodNotAllowedHttpException或NotFoundHttpException异常。 此外闭包函数中的参数与路由参数一一对应。 2.2 可选参数 有时候我们并不总是想要输入对应参数,也就是说,我们期望参数是可有可无的,我们通过这种方式来定义:
|
1 2 3 |
Route::get('/hello/{name?}',function($name="Laravel"){ return "Hello {$name}!"; }); |
我们同时为可选参数指定了默认值,这样当我们访问http://laravel.app:8000/hello时输出:
|
1 |
Hello Laravel! |
当我们访问http://laravel.app:8000/hello/Laravel学院的时候输出:
|
1 |
Hello Laravel学院! |
2.3 正则约束 有时候我们希望对路由有更加灵活的条件约束,可以通过正则表达式来实现:
|
1 2 3 |
Route::get('/hello/{name?}',function($name="Laravel"){ return "Hello {$name}!"; })->where('name','[A-Za-z]+'); |
该条件约束意味着$name参数只能包含大小写字母,如果包含数字或中文就会抛出NotFoundHttpException异常。 如果我们想要在全局范围内对参数进行条件约束,可以在RouteServiceProvider的boot方法中做如下定义:
|
1 2 3 4 5 |
public function boot(Router $router) { $router->pattern('name','[A-Za-z]+'); parent::boot($router); } |
我们访问http://laravel.app:8000/hello/Laravel123/by/Laravel学院时一样会抛出NotFoundHttpException异常。这意味着boot方法定义的参数条件约束将会应用到所有包含该参数的路由中。 此外,服务提供者的boot方法在所有服务提供者的register方法执行完毕后开始执行,也就是说,我们可以在boot方法对任意服务容器中的对象进行依赖注入。 from:http://laravelacademy.org/post/398.html
View Details昨天折腾了1个小时,把5.2升级到5.3,目前是成功了,遇见了几个小坑。但是之后的坑不可预见,分享一下遇见的小坑。 前提是必须仔细阅读的官方文档。 刚开始的时候,改了composer文件,直接更新,出现了错误 Declaration of App\Providers\EventServiceProvider::boot() should be compatible with Illuminate\Foundation\Support\Providers\EventServiceProvider::boot(Illuminate\Contracts\Events\Dispatcher $events) 然后我去看了下官方文档,具体忘记在哪里了,主要是bootstrap里面的cache文件没有清理,所以必须得先清理,运行下面的命令 php artisan route:cache php artisan view:clear php artisan config:clear php artisan cache:clear php artisan clear-compiled php artisan optimize php artisan route:cache 更新完之后会报错, ErrorException in EventServiceProvider.php line 8: 按照官方文档解释,就是这些功能已经改变,不需要参数,所以,我们找到相应的位置,把参数去掉即可 public function boot() { parent::boot(); } from:https://laravel-china.org/topics/3767/upgrade-laravel-52-to-53
View Details这是一个Web Server的时代,apache2与nginx共舞,在追求极致性能的路上,没有最高,只有更高。但这又是一个追求个性化的时代,有些Web Server并没有去挤“Performance提升”这一独木桥,而是有着自己的定位,Caddy就是这样一个开源Web Server。 Caddy的作者Matt Holt在caddy官网以及FAQ中对caddy的目标阐释如下: 其他Web Server为Web而设计,Caddy为human设计。功能定位上,与经常充当最前端反向代理的nginx不同,caddy致力于成为一个易用的静态 文件Web Server。可以看出Caddy主打易用性,使用配置简单。并且得益于Go的跨平台特性,caddy很容易的支持了三大主流平台:Windows、 Linux、Mac。在Caddy开发者文档中,我们可以看到caddy还可以在Android(linux arm)上运行。caddy目前版本为0.7.1,还不稳定,且后续版本可能变化较大,甚至与前期版本不兼容,因此作者目前不推荐caddy在生产环境被 重度使用。 关注caddy,是因为caddy填补了go在通用web server这块的空白(也许有其他,但我还不知道),同时Web server in go也“响应”了近期Golang去C化的趋势(Go 1.5中C is gone!),即便caddy作者提到caddy的目标并非如nginx那样。但未来谁知道呢?一旦Go性能足够高时,一旦caddy足够稳定时,自然而 然的就会有人将其用在某些应用的生产环境中替代nginx或apache2了。一套全Go的系统,在部署、运维方面也是有优势的。 一、安装和运行caddy 和诸多go应用一样,我们可以直接从caddy的github.com releases页中找到最新发布版(目前是0.7.1)的二进制包。这里使用的是caddy_darwin_amd64.zip。 下载解压后,进入目录,直接执行./caddy即可将caddy运行起来。 $caddy 0.0.0.0:2015 在浏览器里访问localhost:2015,页面上没有预期显示的类似"caddy works!”之类的默认Welcome页面,而是“404 Not Found"。虽然这说明caddy已经work了,但没有一个default welcome page毕竟对于caddy beginer来说并不友好。这里已经向作者提了一个sugguestion issue。 二、caddy原理 Go的net/http标准库已经提供了http server的实现,大多数场合这个http server都能满足你的需要,无论是功能还是性能。Caddy实质上也是一个Go web app,它也import net/http,嵌入*http.Server,并通过handler的ServeHTTP方法为每个请求提供服务。caddy使用 http.FileServer作为处理 静态文件的基础。caddy的诱人之处在于其middleware,将诸多middleware串成一个middleware chain以提供了灵活的web服务。另外caddy中的middleware还可以独立于caddy之外使用。 caddy从当前目录的Caddyfile(默认)文件中读取配置,当然你也可以通过-conf指定配置文件路径。Caddyfile的配置格式 的确非常easy,这也符合caddy的目标。 Caddyfile总是以站点的Addr开始的。 单一站点的Caddyfile样例如下: //Caddyfile localhost:2015 gzip log ./2015.log Caddy也支持配置多个站点,类似virtualhost的 配置(80端口多路复用): //Caddyfile foo.com:80 { log ./foo.log gzip } bar.com:80 { log ./bar.log gzip } 为了实现风格上的统一,单一站点也最好配置为如下这种格式(代码内部称之为 Server Block): localhost:2015 { gzip log ./2015.log } 这样Caddyfile的配置文件模板样式类似于下面这样: host1:port { middleware1 middleware2 { … … } […]
View DetailsNeo4j是一个高性能的,NOSQL图形数据库,它将结构化数据存储在网络上而不是表中。它是一个嵌入式的、基于磁盘的、具备完全的事务特性的Java持久化引擎,但是它将结构化数据存储在网络(从数学角度叫做图)上而不是表中。Neo4j也可以被看作是一个高性能的图引擎,该引擎具有成熟数据库的所有特性。程序员工作在一个面向对象的、灵活的网络结构下而不是严格、静态的表中——但是他们可以享受到具备完全的事务特性、企业级的数据库的所有好处。 Neo4j因其嵌入式、高性能、轻量级等优势,越来越受到关注.
View DetailsBeanstalk,一个高性能、轻量级的分布式内存队列系统,最初设计的目的是想通过后台异步执行耗时的任务来降低高容量Web应用系统的页面访问延迟,支持过有9.5 million用户的Facebook Causes应用。 后来开源,现在有PostRank大规模部署和使用,每天处理百万级任务。Beanstalkd是典型的类Memcached设计,协议和使用方式都是同样的风格,所以使用过memcached的用户会觉得Beanstalkd似曾相识。
View DetailsMarkdown是一种可以使用普通文本编辑器编写的标记语言,通过简单的标记语法,它可以使普通文本内容具有一定的格式。 Markdown具有一系列衍生版本,用于扩展Markdown的功能(如表格、脚注、内嵌HTML等等),这些功能原初的Markdown尚不具备,它们能让Markdown转换成更多的格式,例如LaTeX,Docbook。Markdown增强版中比较有名的有Markdown Extra、MultiMarkdown、 Maruku等。这些衍生版本要么基于工具,如Pandoc;要么基于网站,如GitHub和Wikipedia,在语法上基本兼容,但在一些语法和渲染效果上有改动。
View Details从 PHP 5.4.0 起,PHP内置了Web服务器,这对于认为需要Apache或Nginx才能预览PHP应用的开发者来说又是一个隐藏功能。这个内置的Web服务器不应该用于生产环境,但对于本地开发来说是个极好的工具。Laravel Valet 起初就是使用这个内置的服务器,但是在1.1.0版本后将其替换为Caddy(查看相关新闻)。 1、启动 这个内置的Web服务器很容易启动,打开终端(Windows下对应是cmd命令行),进入项目根目录,执行如下命令即可:
|
1 |
php -S localhost:8000 |
上述命令会新启动一个PHP Web服务器,地址是localhost,监听的端口是8000,当前所在目录就是这个Web服务器的根目录。 现在,打开浏览器,访问http://localhost:8000就可以预览应用了。在Web浏览器中浏览应用时,每个HTTP请求的信息都会记录到终端的标准输出中,因此我们可以查看应用是否抛出了404或500响应: 有时候我们需要在同一局域网中的另一台设备中访问这个服务器(例如iPad或本地虚拟机),为此,我们可以把localhost换成0.0.0.0,让PHP Web服务器监听所有接口:
|
1 |
php -S 0.0.0.0:8000 |
要想停止Web服务器,可以关闭终端,也可以按Ctrl+C快捷键。 2、配置 应用常常需要使用专属的PHP配置文件,尤其是对内存使用、文件上传、分析或对字节码缓存有特殊要求时,一定要单独配置,我们可以使用-c选项,让PHP内置的服务器使用指定的配置文件:
|
1 |
php -S localhost:8000 -c app/config/php.ini |
3、路由脚本 PHP内置服务器明显遗漏了一个功能:与Apache和Nginx不同,它不支持.htaccess文件,因此,这个服务器很难使用多数流行的PHP框架中常见的前端控制器(单一入口文件index.php,用于转发所有HTTP请求,现在主流PHP框架如Laravel、Symfony都是这样)。 PHP内置服务器使用路由脚本弥补了这一缺憾,处理每个HTTP请求前,会先执行这个路由脚本,如果结果为false,返回当前HTTP请求中引用的静态资源URI,否则会把路由脚本的执行结果当做HTTP响应主体返回。换句话说,路由脚本的作用其实和.htaccess一样。 路由脚本的用法很简单,只需要在启动PHP内置服务器时指定这个PHP脚本文件的路径即可:
|
1 |
php -S localhost:8000 router.php |
关于路由脚本,有兴趣的同学可以研究下Laravel Valet底层的server.php(https://github.com/laravel/valet/blob/master/server.php)。 4、判断函数 有时候需要知道PHP脚本使用的是PHP内置的Web服务器还是使用传统的Web服务器,这样方便我们为不同服务器设定不同的响应头。我们可以使用php_sapi_name()函数检查使用的是哪个PHP Web服务器,如果当前脚本使用的是PHP内置服务器,则该函数返回字符串cli-server:
|
1 2 3 4 5 6 |
<?php if (php_sapi_name() == ‘cli-server') { // PHP 内置 Web 服务器 } else { // 其他Web服务器 } |
5、缺点 PHP内置的Web服务器不能在生成环境使用,只能在本地开发环境中使用,这是因为其相比Apache或Nginx有诸多不足: 性能不佳。一次只能处理一个请求,其他请求会受到阻塞。如果某个进程耗时较长(数据库查询、远程API调用),则整个Web应用会陷入停顿状态。 支持媒体类型较少(这一点PHP 5.5.7以后有较大改进)。 路由脚本仅支持少量的URL重写,更高级则还是需要Apache或Nginx。 from:http://laravelacademy.org/post/4422.html
View Details1、概述 字节码缓存不是PHP的新特性,有很多独立的扩展可以实现,比如APC、eAccelerator和Xache等,但是截至目前这些扩展都没有集成到PHP内核,从PHP 5.5.0开始,PHP内置了字节码缓存功能,名为Zend Opcache。 开始之前,我们先来看看什么是字节码缓存,以及字节码缓存的作用是什么。 众所周知,PHP是解释型语言,构建在Zend 虚拟机之上,PHP解释器在执行PHP脚本时会解析PHP脚本代码,把PHP代码编译成一系列Zend操作码(opcode:http://php.net/manual/zh/internals2.opcodes.php,由于每个操作码都是一个字节长,所以又叫字节码,字节码可以直接被Zend虚拟机执行),然后执行字节码。每次请求PHP文件都是这样,这会消耗很多资源,如果每次HTTP请求都必须不断解析、编译和执行PHP脚本,消耗的资源更多。如果PHP源码不变,相应的字节码也不会变化,显然没有必要每次都重新生成Opcode,结合在Web应用中无处不在的缓存机制,我们可以把首次生成的Opcode缓存起来,这样下次直接从缓存取,岂不是很快?下面是启用Opcode缓存之前和之后的流程图: 字节码缓存能存储预先编译好的PHP字节码,这样,下次请求PHP脚本时,PHP解释器不用每次读取、解析和编译PHP代码,直接从内存中读取预先编译好的字节码,然后立即执行,这样能省很多时间,极大提升应用的性能。 2、启用Zend Opcache 注:如果使用Windows开发环境,或者使用brew或apt-get等命令安装的PHP可以略过编译步骤。 默认情况下,Zend Opcache没有开启,需要我们在编译时使用--enable-opcache指定启用Zend Opcache。 编译好PHP后还需要在php.ini中指定Opcache扩展路径:
|
1 |
zend_extension=/path/to/opcache.so |
一般而言PHP编译成功后会显示Zend Opcache扩展路径,但如果想不起来,可以使用如下命令找到PHP扩展所在目录:
|
1 |
php-config --extension-dir |
注:如果你使用Xdebug,需要在php.ini中先加载Zend Opcache,再加载Xdebug。 更新php.ini后重启PHP进程并查看是否启用成功: 3、配置Zend Opcache 启用Zend Opcache后还需要在php.ini中配置Zend Opcache,下面是一份配置示例作为参考:
|
1 2 3 4 5 6 |
opcache.validate_timestamps=1 //生产环境中配置为0 opcache.revalidate_freq=0 //检查脚本时间戳是否有更新时间 opcache.memory_consumption=64 //Opcache的共享内存大小,以M为单位 opcache.interned_strings_buffer=16 //用来存储临时字符串的内存大小,以M为单位 opcache.max_accelerated_files=4000 //Opcache哈希表可以存储的脚本文件数量上限 opcache.fast_shutdown=1 //使用快速停止续发事件 |
注:后续我们还会进一步介绍Zend Opcache的配置,PHP官网中列出了Zend Opcache的全部设置:http://ua2.php.net/manual/zh/opcache.configuration.php。 4、使用Zend Opcache Zend Opcache使用起来很简单,因为启用之后它会自动运行,Zend Opcache会自动在内存中缓存预先编译好的PHP字节码,如果缓存了某个文件的字节码,就执行对应的字节码。 如果php.ini中配置了opcache.validate_timestamps值为0,需要小心,因为Zend Opcache将不能觉察PHP脚本的变化,必须手动清空Zend OPcache缓存的字节码,才能让它发现PHP文件的变动。这个配置适合在生产环境中设置为0,但在开发环境会带来不便,我们可以在开发环境中这样配置启用自动验证缓存功能:
|
1 2 3 4 |
opcache.validate_timestamps=1 opcache.revalidate_freq=0 from:<a href="http://laravelacademy.org/post/4396.html">http://laravelacademy.org/post/4396.html</a> |
1、概述 闭包和匿名函数在PHP 5.3.0中引入,这两个特性非常有用,每个PHP开发者都应该掌握。 闭包是指在创建时封装周围状态的函数,即使闭包所在的环境的不存在了,闭包中封装的状态依然存在。 匿名函数其实就是没有名称的函数,匿名函数可以赋值给变量,还能像其他任何PHP函数对象那样传递。不过匿名函数仍然是函数,因此可以调用,还可以传入参数,适合作为函数或方法的回调。 注:理论上讲闭包和匿名函数是不同的概念,不过PHP将其视作相同的概念(匿名函数在PHP中也叫作闭包函数),所以下面提到闭包时指的也是匿名函数;反之亦然。 2、创建闭包 创建闭包很简单:
|
1 2 3 4 5 6 |
<?php $greet = function ($name) { return sprintf("Hello %s\r\n", $name); }; echo $greet('LaravelAcademy.org'); |
结果打印:
|
1 |
Hello LaravelAcademy.org |
闭包和普通的PHP函数很像:常用的句法相同,也接受参数,而且能返回值。不过闭包没有函数名。 注:我们之所以能调用$greet变量,是因为这个变量的值是一个闭包,而且闭包对象实现了__invoke()魔术方法,只要变量名后有(),PHP就会查找并调用__invoke方法。 我们通常把PHP闭包当做函数会方法的回调使用,事实上,很多PHP函数都会用到闭包,比如array_map和preg_replace_callback,这是使用PHP匿名函数的绝佳时机。记住,闭包和其他值一样,可以作为参数传入其他PHP函数:
|
1 2 3 4 5 6 |
<?php $numberPlusOne = array_map(function ($number) { return $number += 1; }, [1, 2, 3]); print_r($numberPlusOne); |
在闭包出现之前,要实现这样的功能,PHP开发者只能单独创建具名函数,然后使用名称引用这个函数:
|
1 2 3 4 5 6 7 |
<?php function incrementNumber ($number) { return $number += 1; } $numberPlusOne = array_map(‘incrementNumber’, [1, 2, 3]); print_r($numberPlusOne); |
这样做把回调的实现和使用场所隔离开了,而且使用闭包实现代码更加简洁。 3、从父作用域继承变量 在PHP中必须手动调用闭包对象的bindTo方法或使用use关键字把父作用域的变量及状态附加到PHP闭包中。而实际应用中,又以使用use关键字实现居多。 use关键字 实际上,Laravel框架中也大量使用了闭包,最常见的比如路由定义:
|
1 2 3 4 5 |
Route::group(['domain' => '{account}.myapp.com'], function () { Route::get('user/{id}', function ($account, $id) { // }); }); |
这里面的两个function都是闭包。而从父作用域继承变量的使用场景在Laravel底层源码中也是俯拾即是,比如Model.php(Illuminate\Database\Eloquent)的saveOrFail方法: 该方法的作用是使用事务将模型数据保存到数据库,这里面我们使用闭包返回保存状态,同时使用use关键字将父作用域的$options传递给该闭包以便其能够访问这个数据。 此外,还支持传递多个父作用域变量到闭包,比如还是在Model类中的forceFill方法: 多个变量以逗号分隔即可。 bindTo方法 我们在前面已经提到,闭包是一个对象,所以我们可以在闭包中使用$this关键字获取闭包的内部状态,闭包对象的默认状态没什么用,需要注意的是其中的__invoke魔术方法和bindTo方法。 __invoke的作用前面已经说过,当尝试以调用函数的方式调用一个对象时,__invoke() 方法会被自动调用。 接下来我们来看看bindTo方法,通过该方法,我们可以把闭包的内部状态绑定到其他对象上。这里bindTo方法的第二个参数显得尤为重要,其作用是指定绑定闭包的那个对象所属的PHP类,这样,闭包就可以在其他地方访问邦定闭包的对象中受保护和私有的成员变量。 你会发现,PHP框架经常使用bindTo方法把路由URL映射到匿名回调函数上,框架会把匿名回调函数绑定到应用对象上,这样在匿名函数中就可以使用$this关键字引用重要的应用对象:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
<?php class App { protected $routes = []; protected $responseStatus = '200 OK'; protected $responseContentType = 'text/html'; protected $responseBody = 'Laravel学院'; public function addRoute($routePath, $routeCallback) { $this->routes[$routePath] = $routeCallback->bindTo($this, __CLASS__); } public function dispatch($currentPath) { foreach ($this->routes as $routePath => $callback) { if( $routePath === $currentPath) { $callback(); } } header('HTTP/1.1 ' . $this->responseStatus); header('Content-Type: ' . $this->responseContentType); header('Content-Length: ' . mb_strlen($this->responseBody)); echo $this->responseBody; } } |
这里我们需要重点关注addRoute方法,这个方法的参数分别是一个路由路径和一个路由回调,dispatch方法的参数是当前HTTP请求的路径,它会调用匹配的路由回调。第9行是重点所在,我们将路由回调绑定到了当前的App实例上。这么做能够在回调函数中处理App实例的状态:
|
1 2 3 4 5 6 |
$app = new App(); $app->addRoute(‘user/nonfu’, function(){ $this->responseContentType = ‘application/json;charset=utf8’; $this->responseBody = ‘{“name”:”LaravelAcademy"}'; }); $app->dispatch(‘user/nonfu'); |
在Larval底层也有用到bindTo方法,详见Illuminate\Support\Traits\Macroable的__call方法: from:http://laravelacademy.org/post/4341.html
View Details