Laravel 应该是在5.几之后吧,其laravel/ui项目的前端框架默认使用Bootstrap与Vue。 1. 无法打印vue实例 对于vue官方文档中的例子,我们都可以在浏览器console窗口通过app变量打印出vue实例。 但是在laravel/ui的页面中,我们打印app变量时候,输出的却是id="app"的那个dom元素,并不是在/resources/js/app.js中定义的vue实例。 const app = new Vue({ el: '#app', }); 原因是laravel mix在编译打包js文件的时候,会将每个js源文件作为一个独立的作用域,这样可以使不同js文件中的变量互不干扰。而这个app变量(打包时候还会将const变成var。。。=。=# js真是迷)的作用域就只在该/resources/js/app.js文件中。在其他地方是无法读取该变量的。 我们可以看一下laravel mix打包后生成的/pulbic/js/app.js文件,整个/resources/js/app.js源文件的代码(包括app变量)都被function(module, exports, __webpack_require__) 这个匿名函数包裹起来,所以app变量只作为该匿名函数的一个局部变量。 /*!*****************************!*\ !*** ./resources/js/app.js ***! \*****************************/ /***/ (function(module, exports, __webpack_require__) { /** * ... */ var app = new Vue({ el: '#app' }); /***/ }), 一个题外话,关于webpack打包的基本原理。(laravel-mix就是基于webpack的封装) webpack在打包项目中的js源文件,最终生成一个app.js的时候。我们来看一下最终的app.js的简化结构: (function(modules) { // 这是一个立即执行函数(IIFE)的形式 对于modules实参数组中的每个module: 执行module源代码 // 一个module就代表一个被打包的js源文件 })({"module_filename_1": (function() { module_1 源码 }), "module_filename_2": (function() { module_2 源码 }) // 这就是modules的实参数组,每个js源文件的代码都被包裹在闭包中 }); 这样子,每个js源文件就是一个module,用闭包将它们的作用域隔离开。然后通过IIFE的形式立即执行所有module闭包,即执行每个js源文件。 如果非要在全局范围使用该app变量,可以在/resources/js/app.js源文件中将该变量定义为window全局对象的一个属性: window.app = new Vue({ el: '#app', }); 这样在浏览器的console窗口打印app时候,就能打印出该vue实例了。 2. 无法立即使用jquery 我们来看一下laravel/ui是如何引入jquery的,在/resources/js/app.js中,通过require('./bootstrap'); 引入同目录下的bootstrap.js文件: try { window.Popper = require('popper.js').default; // bootstrap的tooltips与popovers组件需要popper.js window.$ = window.jQuery = require('jquery'); // 导入并注册jquery全局变量 require('bootstrap'); // 导入bootstrap } catch (e) {} 所以我们可以看到,它已经将jQuery以及$符号注册到window全局对象中。理论上我们就可以直接在前端页面的代码中直接使用$符号了: <script type="application/javascript"> console.log($('.container')); </script> 但是实际上它却报错了:Uncaught ReferenceError: $ is not defined。$未定义?它不是已经在/resources/js/bootstrap.js中注册了$符号了嘛。而如果我们将前端页面的这个代码修改一下: <script type="application/javascript"> window.onload = function() { console.log($('.container')); } // console.log($('.container')); </script> 在window.onload事件函数中使用这个$符号却是可行的。 这其实是由于laravel/ui前端模版页面app.blade.php在引入app.js文件的<script>标签中,使用了defer属性:<script src="{{ asset('js/app.js') }}" defer></script> 。defer的意思是告诉浏览器将该脚本文件异步下载并延迟执行。 关于浏览器对JavaScript文件的下载执行顺序,参考该讨论:https://segmentfault.com/q/1010000000640869 关于原生的 window.onload 、DOMContentLoaded 与 jquery的 $(document).ready 之间的区别,参考:https://api.jquery.com/ready/。(实际上去看jquery的源码会发现,jQuery.ready就是由DOMContentLoaded触发的) 总结一下js代码在浏览器中的执行顺序: 由于defer属性,使得当浏览器解析并执行console.log($('.container')); 这一句的时候,app.js文件还没被执行,jquery还没被引入。所以这时候“$ is not defined”。如果把这一句放在 window.onload 事件函数中,window.onload事件是在所有资源(包括图片、css、script)都加载完后才触发的,此时app.js已经下载并执行了。那么这时候再调用$符号就是OK的。 2.1 如何补救 a. 取消defer加载,并将<script>标签移到html文件尾 既然如上面所诉,那么我们把laravel/ui前端模版页面中<script src='app.js'>标签的defer属性去掉会怎么样呢?<script src="{{ asset('js/app.js') }}" ></script> [Vue warn]: Cannot find element: #app 可以在浏览器的控制台窗口看到它报错了!jquery可以用了,却引发了vue的错误。 原因是在laravel/ui的前端模版app.blade.php中,这个<script>标签是放在文件头的,浏览器解析到这里后就会“阻塞”地去下载这个app.js文件并执行。而在app.js文件中,它创建了一个vue实例,并绑定到id为 '#app' 的这个<div>元素。但此时浏览器还没解析到前端页面的<div id="app"> 这一句,所以此时是找不到 '#app' 这个元素的。 所以,在去掉defer属性的同时,我们还需要将这个<script>标签移动到html文件的尾部。比如将app.blade.php修改成如下样式: ... ... <script src="{{ asset('js/app.js') }}"></script> @yield('js') // 后续继承这个app.blade的模版页都可以在这个section('js')中引入JavaScript // 在这个section('js')中就能直接使用jquery了。 </body> </html> ps:顺便吐槽一下,我猜测Laravel之所以把app.js文件放在html头部使用defer属性加载。是因为所谓的“前端工程化”(我觉得应该叫“前端复杂化”)后,会导致打包出来的app.js文件特别大,所以它必须让浏览器在解析文件头时就开始异步下载app.js文件。 b. 使用DOMContentLoaded事件 如果不改动默认的<script src="app.js">标签的话,上面我们已经试过可以在window.onload事件函数中直接使用jquery。但windows.onload事件的触发时间太滞后了,我们可以采用DOMContentLoaded事件。在DOMContentLoaded事件触发之前,<script src="app.js" defer>标签就已经下载并执行完了,app.js中的jquery注册已经完成。所以在DOMContentLoaded事件函数中就可以直接使用jquery了。 <script type="application/javascript"> document.addEventListener("DOMContentLoaded", function(event) { console.log($('.container')); }); </script> c. 从app.js中剥离jquery 更彻底的解决方案就是不使用 laravel/ui 默认的将vue与jquery、bootstrap统一打包进app.js这种方法,可以将二者分开打包。 或者再简单一点,对于我这种前端苦手,由于比较少用,可以把app = new Vue({}); 这一块从app.js文件中删掉,哪个页面要用vue了,再单独在那个页面创建这个vue实例。
Laravel 6 - 加密解密与哈希
1. 加密解密 全局辅助函数: encrypt($value, $serialize = true) decrypt($value, $unserialize = true) Facade: Crypt::encrypt($value, $serialize = true) Crypt::decrypt($value, $unserialize = true) 这两组加解密函数的效果是一样的。它们实际调用的都是app('encrypter') 绑定实例,这是一个在Illuminate\Encryption\EncryptionServiceProvider 中注册的Illuminate\Encryption\Encrypter 单例。 在构造Encrypter 加密器的时候需要读取config('app.key')与config('app.cipher')这两个配置。所以这两个配置一旦生成就不要改变,否则会导致之前存储的密文无法解密。 对同一个明文进行两次加密,得到的密文是不一样的!所以不要用encrypt(string)==encryptedString来判断原文是否一致。 encrypt($value, $serialize = true) 默认会对$value实参进行序列化,解密时默认会进行反序列化。所以我们除了加密解密字符串外,还可以用来加密解密对象、数组类型。 Laravel中对cookie的加密解密就是用的app('encrypter')。 2. 哈希 全局辅助函数: bcrypt($value, $options = []) 使用bcrypt算法计算$value的哈希值 Facade: Hash::make($value, array $options = []) 使用默认哈希算法计算$value的哈希值 Hash::check($value, $hashedValue, array $options = []) 使用默认哈希算法校验$value与$hashedValue Laravel默认使用bcrypt哈希算法。可以在/config/hashing.php中修改默认哈希算法驱动以及其系数。 对于同一个明文进行两次bcrypt哈希,得到的密文是不一样的!所以不要用bcrypt(string)==hashedString来判断原文是否一致。 哈希算法是不可逆的,所以不存在解密一说。如果要判断原文是否一致,请使用Hash::check($value, $hashedValue) 。 Laravel中对用户密码的加密就是用Hash::make(password) 。
Laravel 6 - Model的mass assignment
在Laravel的Eloquent ORM中,Mass Assignment是指以数组形式对模型的属性进行赋值,比如:Model->fill(array $attributes) ,Model::create(array $attributes) 。与之对立的是指单一属性的赋值,比如:Model->age=18 。 考虑这么一个场景: 在用户注册的时候,后台通常使用User::create($attributes) 来进行用户模型的创建并写入数据库。这里的$attributes数组来自于用户提交的表单中的$_POST['email'], $_POST['password']等属性。正常情况下用户注册时候只会提交表单中列出的属性,所以一切正常。 但是如果有个不怀好意的用户,在提交表单时候添加一些虚假数据进来,比如$_POST['user_type']='admin'。而偏巧数据库中user表结构就有一个字段'user_type'用来表示用户类型是管理员(admin)还是客户(customer)。那么此时他就成功注册了一个管理员用户。这不是我们想见到的。 为了防止这种情况的发送,Laravel提供了mass assignment保护。在定义一个Model类型的时候,必须通过Model->fillable 或者Model->guarded 变量来指定哪些属性是允许mass assignment的。(这两个成员变量继承自trait GuardsAttributes ,默认情况下所有的属性都不允许mass assignment。) $fillable 数组的作用相当于一个白名单,在数组里的属性都是允许mass assignment的,不在里面的都是不允许的。 $guarded 数组的作用相当于黑名单,在数组里的属性都是不允许mass assignment的,不在里面的都是允许的。 注意,在一个模型类的定义里,$fillable 与$guarded 只需要二选一即可,不要两个都出现。 在执行mass assignment方法的时候,Model会自动忽略不允许mass assignment的属性。详细实现可以查看Model->fill(array $attributes) 的源码,实际上Model::create() 底层也是调用了Model->fill() 对新建实例的$attributes进行填充。mass assignment保护会在底层默默执行而不会报告异常,除非在所有属性都不允许mass assignment的情况下调用mass assignment方法时才会抛出一个MassAssignmentException异常。 以larael/ui自带的用户模型为例: class User { protected $fillable = [ 'name', 'email', 'password', // 只有这三个属性是允许mass assignment的,其他均不允许 ]; // ... ... // 新建用户成功,但email_verified_at会被自动忽略 \App\User::create([ 'name'=>'test', 'password'=>'pass', 'email'=>'test@qq.com', 'email_verified_at'=>date("Y-m-d H:i:s") ]); $u1 = \App\User::find(1); $u2 = \App\User::find(2); // email_verified_at会被自动忽略,name修改成功 $u1->fill(['email_verified_at'=>date("Y-m-d H:i:s"), 'name'=>'u1_'.date('is')]); $u1->save(); $u2->email_verified_at = date("Y-m-d H:i:s"); // 单一属性赋值是可以的 $u2->save(); // 注意,fill()方法只是修改模型实例,并不会写入数据库。 另外,Model还实现了forceFill(array $attributes) 方法避开mass assignment保护。
Laravel 6 - 输出详细日志
Log::debug('debug message'); 的默认输出实在是太简陋了 最起码应该加上输出日志时候的文件与代码所在行吧。幸好Log::debug() 接受一个数组实参作为第二个参数,并格式化后一同输出。所以我们可以把__FILE__魔术常量放在这个数组参数中交给Log打印。 Log::debug('debug message', ['file' => __FILE__, 'line' => __LINE__]); 输出如下: 我们也可以将一些想要打印的变量放在第二个实参数组中打印,比如: Log::info('User failed to login.', ['user' => $user]); 另外,还有一个更复杂但一劳永逸的方法:larave 日志自定义配置格式记录调用文件路径与行号
Laravel 6 – 从基本用户认证深入理解Laravel
启用Laravel的基本用户认证模块后,就会自动生成几个用户注册/登录/密码找回的页面。我们就来从这几个页面深入理解Laravel的用法。 1. 找到路由 这几个路由都由/routes/web.php文件中新增的Auth::routes(); 这一行注册的。 1.1 Facade 这个用法涉及到了Laravel的facade概念,facade是指用看起来像调用类的静态方法的语法(类名::静态方法)来调用容器中对应实例的同名公有方法的一种用法。 我们可以在/config/app.php的aliases数组中找到这个Auth别名,该别名指向实际的facade类'Auth' => Illuminate\Support\Facades\Auth::class, 所以在路由/routes/web.php中的Auth::routes()实际上就是Illuminate\Support\Facades\Auth::routes() 然后我们再去看这个...\Facades\Auth的代码。 class Auth extends Facade { protected static function getFacadeAccessor() { return 'auth'; } // 实际调用的是这个方法 public static function routes(array $options = []) { static::$app->make('router')->auth($options); } } 其中getFacadeAccessor()方法是所有Facade派生类都必须重写的,并且都要返回一个字符串,该字符串指向容器中的一个绑定名。这样就可以通过这个facade来找到容器中绑定的对应实例了。 所以这里我们就要去容器中找名为"auth"的绑定。OK,那么如何找到容器中的绑定关系呢?这个其实挺麻烦的,因为/config/app.php文件中的providers数组只是定义了ServiceProvider类,所有的绑定都是在ServiceProvider类的register()方法中进行的。最简单粗暴的办法就是直接在代码编辑器中全局查找内容singleton('auth 或者bind('auth 。还有一种办法是写一个测试页面通过dd(app()); 打印出整个容器。总之,我们可以在Illuminate\Auth\AuthServiceProvider.php的register()方法中找到这个"auth"其实绑定的是一个Illuminate\Auth\AuthManager类的共享实例(单例)。 然后由于Facade基类重写了php魔术方法__callStatic() ,当系统找不到Facade::method_name()这个静态方法时,就会自动调用__callStatic() 找到在getFacadeAccessor()中关联的实例,并调用其同名公有方法(非静态)。这就是facade的实现原理。 不过实际上由于...\Facades\Auth类中已经定义了routes()静态方法,所以会直接调用它。不会再通过__callStatic()去AuthManager实例那找了(况且AuthManager也没定义routes()方法 ^_^)。 1.2 Router 所以真实的路由,我们可以在Illuminate\Routing\Router类的auth方法中找到。 public function auth(array $options = []) { // Authentication Routes... $this->get('login', 'Auth\LoginController@showLoginForm')->name('login'); $this->post('login', 'Auth\LoginController@login'); $this->post('logout', 'Auth\LoginController@logout')->name('logout'); // 等价与issset($options['register']) ? $options['register'] : true if ($options['register'] ?? true) { $this->get('register', 'Auth\RegisterController@showRegistrationForm')->name('register'); $this->post('register', 'Auth\RegisterController@register'); } // Password Reset Routes... if ($options['reset'] ?? true) { $this->resetPassword(); } // Password Confirmation Routes... if ($options['confirm'] ?? class_exists($this->prependGroupNamespace('Auth\ConfirmPasswordController'))) { $this->confirmPassword(); } // Email Verification Routes... if ($options['verify'] ?? false) { $this->emailVerification(); } } 从上面的代码看出我们可以通过在/routes/web.php中给Auth::routes() 添加参数来关闭,开启某些路由。比如通过Auth::routes(['register'=>false])来关闭注册页面的路由,通过Auth::routes(['verify'=>true])来开启邮箱验证路由(这还要配合开启注册时候的邮箱验证来使用)。 2. 中间件 中间件提供了一种方便的机制过滤进入应用程序的 HTTP 请求。中间件是HTTP请求经过路由分发后的第一道槛。在“AUTH路由图”中我们可以看到所有的AUTH路由都关联了web中间件,还有部分关联guest,auth中间件。 2.1 定义中间件 中间件的定义文件默认都放在app/Http/Middleware 目录下。 2.2 注册中间件 定义了中间件的类之后,还需通过app/Http/Kernel.php 文件向系统注册中间件,告诉系统有哪些中间件,叫什么名字。 $middleware : 全局中间件数组,所有的HTTP请求都要经过这里的中间件。(不需要定义名字) $routeMiddleware : 注册[中间件名 => 中间件类]的对应关系。(auth中间件、guest中间件就在这) $middlewareGroups : 注册[中间件组名 => [中间件组] ]的对应关系。(web中间件组就在这) 如果想要为指定的路由分配中间件,那么就必须先注册中间件名或中间件组名。 2.3 关联中间件 定义并注册了中间件之后,就要把中间件分配到路由上了。 a. web中间件组是如何关联到/routes/web.php文件中的所有路由的? 在App\Providers\RouteServiceProvider@mapWebRoutes() 方法中,加载/routes/web.php文件的同时就将web中间件组分配到所有的web路由组。 protected function mapWebRoutes() { Route::middleware('web') // 实际上是调用 RouteRegistrar->attribute('middleware', ['web']) ->namespace($this->namespace) // RouteRegistrar->attribute('namespace', $this->namespace) ->group(base_path('routes/web.php')); // RouteRegistrar->group('web.php') } protected function mapApiRoutes() { Route::prefix('api') // 实际上是调用 RouteRegistrar->attribute('prefix', 'api') ->middleware('api') // RouteRegistrar->attribute('middleware', ['api']) ->namespace($this->namespace) // RouteRegistrar->attribute('namespace', $this->namespace) ->group(base_path('routes/api.php')); // RouteRegistrar->group('api.php') } 这几行代码看着简单,其实底层涉及很多概念,所以必须要好好说道说道了(ง •̀_•́)ง 首先是Route::static_method() 这样的facade用法,它实际上调用的是容器中名为"router"的\Illuminate\Routing\Router 单例绑定。(在Illuminate\Routing\RoutingServiceProvider 中注册的) 然后,我们会发现在\Illuminate\Routing\Router 类中根本找不到middleware() ,namespace() 方法。而且它的prefix() 方法是protected的,也不能通过Route::prefix() 这种facade方式来调用。 实际上,\Illuminate\Routing\Router 实现了php的魔术方法__call($method, $parameters) 。当系统找不到$router->method() 时就会自动调用$router->__call($method, $parameters) 。 public function __call($method, $parameters) { // 先尝试通过Macroable trait注册的外部方法(关于Laravel Macroable用法请自行搜索) if (static::hasMacro($method)) { return $this->macroCall($method, $parameters); } if ($method === 'middleware') { return (new RouteRegistrar($this))->attribute($method, is_array($parameters[0]) ? $parameters[0] : $parameters); } return (new RouteRegistrar($this))->attribute($method, $parameters[0]); } 所以Route::middleware() ,Route::prefix() ,Route::namespace() 最终调用的都是Illuminate\Routing\RouteRegistrar->attribute($method, $parameters) 方法。它负责将$method, $parameters转换成[属性 => 值]的形式写入自己的$attributes数组中,并返回RouteRegistrar实例本身。 由于方法返回实例本身,所以就能方便地使用php链式调用(Method Chaining)语法了:$instance->method()->another_method(); 最后,Illuminate\Routing\RouteRegistrar->group() 就会调用Illuminate\Routing\Router->group() 方法,加载路由文件(或是定义路由的闭包函数),为每个路由生成Illuminate\Routing\Route 实例并关联上这组路由的属性(即middleware、prefix、namespace这些)。 所有的Illuminate\Routing\Route…
Laravel 6 - 使用Laravel 6的用户认证“脚手架”
1. 创建laravel 6项目 在工作目录下执行如下命令,通过composer创建laravel项目。 composer create-project --prefer-dist laravel/laravel blog "6.*" 该命令会在当前工作目录下生成blog项目目录。整个目录结构如下,关于laravel项目目录结构的说明可以参考Laravel Docs 然后使用php artisan serve 启动网站服务,或者配置nginx来运行网站。 初始的网站只有两个路由,可以通过命令php artisan route:list 来检查路由。 默认路由/,只是一个静态的欢迎页面。 API路由/api/user,出错了,原因是刚刚初始的laravel项目还没有安装laravel/ui。laravel/ui就是在laravel version 6之后单独拎出来的用户认证“脚手架”。它提供了用于基本用户认证控制器、模型、视图等代码。 2. 下载laravel/ui 执行命令:composer require laravel/ui:^1.0 --dev 注意要laravel framework 6只支持版本1的laravel/ui,所以这里必须加上版本限制,--dev可加可不加。 这个命令只是修改了根目录下的composer.json与composer.lock文件(还有下载laravel/ui包到/vendor目录下,并修改了/vendor/composer/autoload_*.php文件,追加了Laravel\Ui命名空间的自动加载。不过默认的git是不追踪vendor目录的^_^) 3. 使用laravel/ui的用户认证“脚手架” 执行命令:php artisan ui vue --auth 该命令自动为我们生成了控制器HomeController.php(认证相关的控制器本来就有),在routes/web.php生成新的路由信息,生成认证页面的模版文件,生成认证页面的静态文件(js, sass, vue这些) 这时候访问网站首页,会发现右上角多了两个按钮LOGIN与REGISTER。点击LOGIN或者REGISTER就能进入登录或者注册页面。但是此时页面排版是混乱的,因为没有真正的静态文件js与css。刚刚生成的静态文件其实是在/resources目录下预处理前的静态文件,还需要用npm生成真正的静态文件并放到/public目录下才行。 4. 下载npm依赖包 执行命令:npm install 该命令会在项目根目录下新建目录/node_modules,将JavaScript依赖包下载到该目录中。同时生成/package-lock.json文件。 5. 生成前端静态文件 执行命令:npm run dev 该命令会生成项目真正的前端静态文件。这时候再刷新登录/注册页面就会发现页面正常了。 部署到生产环境的话,应该用: npm run production 。可以有效减小生成的app.js与app.css的文件大小。 6. 配置数据库 虽然登录/注册页面看起来正常了,但是此时我们还没有配置网站的数据库连接,也还没生成用户表。 6.1 配置数据库连接 修改/.env文件中的数据库连接配置。 6.2 自动生成数据库表 执行命令:php artisan migrate 该命令会根据/database/migrations目录下的几个migrate文件自动创建表。其中migrations表使用来记录migrate动作的。 此后就可以正常的使用用户登录/注册功能了。 7. 配置邮件发送服务 此时如果想使用默认的重置密码页面,就得先配置Laravel的邮件发送服务。 以使用网易个人邮箱的smtp服务为例,去网易邮箱的设置页面开启POP3/SMTP服务,这时会给你一个授权码,这个授权码就第三方邮件服务登录网易邮箱的密码(而不是用你自己的用户登录密码)。所以修改/.env文件中的MAIL配置如下: MAIL_USERNAME就是网易邮箱的用户名 MAIL_PASSWORD就是刚刚得到的授权码
坑,Laravel的cache系统与entrust与throttle
laravel默认是使用file做cache驱动的,但file与database驱动的cache无法支持tag,这就与我用到的entrust插件冲突了,所以改成了array作为cache系统的驱动。 后来想用throttle中间件的时候,发现throttle无效,一猜就是cache的问题,因为改用array做cache后,array其实相当于一次进程获得的内存空间开辟的一块数组,那么如果cache在这个数组里面,请求处理完后,这个数组已经没有啦!所以throttle根本不起作用。想了想最好的办法还是得换redis或者memcache做cache驱动,尤其在生产环境。
Laravel框架 - 打印容器实例
初学laravel的时候,看到service provider这一块,一直纠结如何才能打印出容器内所有的service provider,因为光靠config/app.php文件,很难从alias看出其对应的service provider到底是哪个。后来才发现,其实在程序的任意地方(Application启动后),都可以通过 Container::getInstance()这个静态方法获取当前的Application实例。或者可以使用在全局helps.php中定义的的 app()方法来获取当前Application实例。 Route::get('/test', function () { dd(app()); // dd(Illuminate\Container\Container::getInstance()); }); 注意: 在laravel中打印变量尤其是“超大型”变量的时候,千万不要直接使用var_dump()。一来不直观,二来大型变量直接 var_dump可能会导致浏览器卡死(我猜应该是卡死在尝试将var_dump数据解析成dom上面)。=。=# 最好使用larave的全局函数dd()或者dump(),输出又美观,又不会卡死浏览器。 ps: 最好用dd吧,少用dump,今天发现一个bug,如果使用dump的话,会导致cookie操作失效=。=# 发起了一个讨论帖。 @2016.07.03 打印出来的Application如下如所示: serviceProviders:All of the registered service providers. 所有已注册的service provider。其实等同于loadedProviders,只是数组的格式不一样。 loadedProviders:The names of the loaded service providers. 所有已经加载的service provider。等同于serviceProviders。 deferredServices:The deferred services and their providers. 所有延迟加载的service provider。 [绑定名=>service provider] resolved:An array of the types that have been resolved. 已经解析(resolve)过的绑定关系会记录在这里。 bindings:The container's bindings. 容器中所有的绑定关系(并未指向真正的绑定实例,只是指向绑定关系的那段代码),即由Container::bind()与Container::singleton()定义的绑定关系。 [绑定名=>[生成绑定实例的闭包函数, 是否单例绑定]] instances:The container's shared instances. 容器中所有单例绑定生成的实例对象。 aliases:The registered type aliases. 绑定关系的别名,实际上是[绑定实例的类名=>绑定名]这样一个数组。 这个aliases通过Container::alias(绑定名, 别名)来定义,容器查找绑定关系的时候,会同时查找bindings[]与aliases[]数组。 [绑定实例的类名=>绑定名] 另外,注意这个aliases与/config/app.php文件中定义的aliases数组不同。/config/app.php文件中aliases数组是用来定义类的别名,底层调用的是php的class_alias函数。展开来说:在容器初始化的时候会创建一个Illuminate\Foundation\Bootstrap\RegisterFacades类实例。该实例通过AliasLoader::getInstance()静态方法来创建一个AliasLoader类的单例,并在构造单例时令其读取/config/app.php中的aliases数组,然后再调用该单例的register()方法,将AliasLoader单例的load()方法通过php的spl_autoload_register函数注册为__autoload函数(php在找不到类实现的时候就会自动调用__autoload函数栈来查找类的实现)。就是在这个单例的load()方法中,通过php的class_alias函数来注册类别名的。而容器中的aliases数组其实只是绑定名与绑定实例类名的对应关系。 resolvingCallbacks: 所有已注册的解析回调。 [绑定名=>解析回调的闭包函数] extenders: 扩展绑定。 [绑定名 => 扩展绑定的闭包函数]
Laravel框架 - 容器、绑定与依赖注入
laravel框架的核心概念就是容器,打开laravel的入口文件public/index.php,一路追寻,会发现laravel框架在初始化时会新建一个Illuminate\Foundation\Application实例对象赋值给$app这个全局变量。这个Application实例就是我们的laravel容器(Application是Container的子类),是整个laravel程序的基础: 一、容器 容器,就是用来装东西的,对吧。现实中的容器可以用来装水、装米,但编程世界中的容器装的则是一个个实例对象。举个例子,在程序运行过程中,我们可能需要一个Mailer实例来发送邮件,通常直观的做法是在需要的时候才创建这个Mailer实例,但我们也可以在程序初始化的时候先行创建一个Mailer实例,并在容器中注册该实例(laravel中叫做binding),随后在程序中任何需要用到Mailer的地方,就可以直接向容器申请这个实例(laravel叫做resolving)或者由容器进行自动依赖注入。 实际上laravel容器的绑定并不是真的绑定一个实例,而是绑定一个闭包函数,该闭包负责生成要绑定的实例对象。同时在绑定的时候定义是否为单例绑定(laravel中把单例绑定叫做shared)。然后直到程序向容器申请该实例的时候,才真正执行这个闭包函数去生成一个实例。对于非单例绑定,每次resolving都会生成一个新的实例;对于单例绑定,只在第一次resolving的时候生成实例,然后将该实例放入容器的instances数组中(Illuminate\Container\Container->instances),后面在申请时直接从该数组中返回实例。 (如果你有java的spring框架基础的话,那么就很好理解了。laravel容器就相当于spring容器,容器中绑定的实例对象相当于spring bean的概念,他们的目的都是为了依赖注入。另外,laravel的中间件也有点类似spring中切面的概念。推荐可以看一下Craig Walls写的《Spring实战》这本书,对于容器与依赖注入的概念描述得很棒。) (或者,关于容器与依赖注入的概念,还可以参考这篇博文:https://www.insp.top/article/learn-laravel-container) 1. 打印Laravel容器 https://www.hawu.me/coding/1132 2. binding 绑定操作通常在ServiceProvider的register()方法中定义,然后通过写入/config/app.php文件中的providers数组让laravel框架在初始化时自动加载。(当然,你也可以在程序其他地方直接调用bind()与singleton()方法进行绑定) 绑定的作用其实是告诉容器,在解析绑定时候如何生成需要的实例对象。如果一个类不依赖于任何其他类(即构造方法无参)或者它的构造器依赖可以被laravel解析,那么这个类就无需绑定到容器。容器不需要被告知如何生成这样的类对象,因为容器可以通过php的反射机制自动解析出具体对象。 However, there is no need to bind classes into the container if they do not depend on any interfaces. The container does not need to be instructed on how to build these objects, since it can automatically resolve such "concrete" objects using PHP's reflection services. —— laravel官方文档 Container::bind()与Container::singleton()方法定义如下: /** * Register a binding with the container. * * @param string|array $abstract 绑定名,即该绑定关系在容器中的id。 * @param \Closure|string|null $concrete 要绑定的闭包函数,该闭包负责返回一个实例对象。 * @param bool $shared 是否为单例绑定。 * @return void */ public function bind($abstract, $concrete = null, $shared = false) { // ... } public function singleton($abstract, $concrete = null) { $this->bind($abstract, $concrete, true); } 举个例子,假设我们在\app\Providers\AppServiceProvider.php的register()方法中绑定自定义的App\Common\Utility类: use App\Common\Utility as Utility; public function register() { $this->app->bind('utility', function ($app) { return new Utility(); }); } 首先在ServiceProvider派生类中我们总是可以通过$this->app 来访问容器。 然后在绑定的闭包函数function ($app) 中(laravel管这个闭包函数叫做resolver),这里的第一个参数是laravel“规定”的。实际上这个resolver应该是function($app, array $parameters) 这样的形式。laravel在解析绑定,执行resolver闭包函数时默认会传递两个实参给闭包函数。第一个实参就是容器本身,第二个实参是一个数组(可以查阅Container类的make()与makeWith()方法的源码)。这样我们就可以在resolver中通过$app实参引用容器了。另外如果在resolver中需要用到某些额外参数,就可以通过第二个数组实参传递进来。比如我们将Utility类改为需要一个参数才能构造,如下所示。 use App\Common\Utility as Utility; public function register() { $this->app->bind('utility', function ($app, $paramaters) { // 假设Utililty类的构造函数需要一个参数 return new Utility($paramaters['key1']); }); } 那么在解析这个utility绑定时候,就得使用app()->make('utility', ['key1'=>'value1']); 或app()->makeWith('utility', ['key1'=>'value1']) 。 最后,由于php语法中函数实参的个数只要不小于形参个数即可。所以如果不需要用到额外参数与容器实例的话,我们甚至可以将resolver闭包的形参都去掉,只写作function () { return new Utility(); } 。 3. resolving 获取绑定对象,可以通过如下几种方法手工获取绑定对象: // 1. 使用Container的make方法 $obj = $this->app->make('绑定名'); // 可以通过$this->app属性访问容器时 $obj = app()->make('绑定名'); // 当前类没有app属性时,可以使用app()全局辅助函数访问容器 // 2. 使用数组形式,Conatiner类实现了ArrayAccess接口。 $obj = $this->app['绑定名']; $obj = app()['绑定名']; // 3. 使用全局辅助函数app() 或者 resolve()。在Illuminate/Fundation/helpers.php中定义的 $obj = app('绑定名'); // app()函数在无参时返回整个容器的实例,有参数时则返回绑定的实例。 $obj = resolve('绑定名'); // 对于需要额外参数来解析绑定的时候 $obj = app('utility', ['key1'=>'xxx']); $obj = resolve('utility', ['key1'=>'xxx']); $obj = app()->make('utility', ['key1'=>'xxx']); // 好像早期某些版本的make()不支持第二个参数,laravel 6是可以的。 $obj = app()->makeWith('utility', ['key1'=>'xxx']); 可以稍微看一下Container@make()方法的定义: public function make($abstract, array $parameters = []) { // 从aliases[]数组中取$abstract对应的绑定名(如果存在的话,不存在别名则直接使用$abstract) $abstract = $this->getAlias($this->normalize($abstract)); // 如果要解析的实例已经存在于instances[]数组中(属于单例绑定),则返回对应实例。 if (isset($this->instances[$abstract])) { return $this->instances[$abstract]; } // 取得绑定的闭包函数 $concrete =…
Laravel框架 - 命令行工具artisan
说在前面的话: laravel算是近几年最火的php web开发框架了,在github的php项目中稳居第一(Most Popular PHP Project at Github)。star数量甚至是我先前挚爱codeigniter的两倍。这几天抽空看了下laravel的文档与入门视频(laracasts.com),惊讶于现在的php居然已经能写成这样了,laravel简直就是php届的spring。容器、依赖注入、中间件。。。跟朴实无华的codeigniter相比简直就是在炫技,当然这也成了laravel反对派们的口实——“将追求简单直接的php写成了晦涩臃肿的java”。我试了一下laravel与codeigniter渲染一个index页面的性能,laravel平均耗时80ms,而ci只需要8ms,=。=# but!who care!“当你需要考虑性能的时候,性能早已不是问题了。” balabala吹完laravel,该说正题了。除了laravel本身的框架之外,laravel还提供了一个屌炸天命令行工具artisan,简直就是开发神器。这篇文章就是想记录下平时常用的命令,免得一段时间不写全忘光光。 一、启动服务 $ php artisan serve 使用这个命令,直接在8000端口启动web服务,而不需要在配置nginx,这对于开发环境特别方便。 二、路由相关 1. 打印所有路由 $ php artisan route:list 2. 缓存路由 $ php artisan route:cache $ php artisan route:clear route:cache命令会生成路由缓存文件bootstrap/cache/routes.php,然后项目启动时候使用路由缓存取代app/Http/routes.php,据官方说这能提高路由效率(不过我试了下好像并没有感觉=。=#)。 注意:路由缓存不支持闭包路由,即在app/Http/routes.php文件中的所有路由必须是基于控制器的路由(Controller@Method的方式)。另外,如果修改了app/Http/routes.php文件,那么就必须重新生成路由缓存,所以只推荐在生产环境上使用路由缓存。 三、migrate 我也不知道为啥要取migrate这个名字,迁移。laravel的migrate功能说白了就是定义数据库表结构与初始数据,并通过migrate命令自动导入数据库。 1. 创建migration文件 $ php artisan make:migration create_user_table makr:migration命令会在database/migrations目录下创建migration文件(时间戳_迁移名.php),我们可以在该文件中定于数据库表结构。 注意:通常在定义了create类型的migration文件后,如果我们想修改表结构,但又不想再回去修改原来的migration文件,那么我们可以添加一个alter类型的migration文件,在up方法中使用Schema::table()而不是Schema::create()。 class CreateUserTable extends Migration { public function up() { Schema::create('user', function (Blueprint $table) { $table->increments('id'); $table->string('name'); $table->string('password'); }); } public function down() { Schema::drop('user'); } } class AlterUserTable extends Migration { public function up() { Schema::table('user', function (Blueprint $table) { $table->integer('age')->unsigned(); }); } public function down() { Schema::table('user', function (Blueprint $table) { $table->dropColumn('age'); }); } } 2. 执行migrate迁移 $ php artisan migrate 执行完后,会发现数据库中已经自动生成了对应的几张表。 注意:迁移执行顺序是依据migration文件的文件名时间戳前缀来依次执行的,所以如果某个表存在外键依赖关系,要保证被依赖的表先被migrate。 注意:有些时候迁移可能会出现异常,比如说“Class not found”或者“No such file or directory”,可以尝试先执行composer dump-autoload命令后再进行迁移。 四、tinker tinker是一个交互工具,进入tinker模式就好比进入了一个php的shell,我们可以在这个“shell”上直接敲php代码并执行获得反馈。 1. 进入tinker $ php artisan tinker 2. 数据库操作 DB::listen(function($query) { var_dump($query->sql); }); DB::table('table_name')->get(); DB::table('table_name')->insert(...); 3. 数据模型操作 laravel初始项目默认有一个App\User数据模型,对应数据库的users表。我们可以在tinker直接使用Model类的方法来执行数据模型操作。 注意,使用DB查询数据库返回的数据是stdClass,使用Model查询数据库返回的是Model对象。 五、任务队列 php artisan queu:listen 持续监听 php artisan queue:work 执行头一个job php artisan queue:work --daemon 并不是后台运行=。=# 而是表示持续执行work,官方文档推荐用这个取代listen,因为daemon worker不会每次重启框架,所以速度快,但你要个更注意程序中的内存问题,不处理好容易导致worker占用越来越大越来越大的内存。。。另外因为不会重启框架,所以期间如果修改了代码,必须restart worker 六、vendor:publish 执行第三方包的publish指令。publish指令的主要作用是拷贝第三方包的assets文件(可能是config文件、view文件等)到项目目录中。publish指令通常定义在xxxServiceProvider.php的boot()方法中。 以zizaco/entrust包为例,在EntrustServiceProvider.php中定义的publish指令,其意义是拷贝entrust包内的config/config.php文件到项目配置目录(app/config/)下,并命名为entrust.php。 // 执行所有第三方包的publish指令 php artisan vendor:publish // 执行某个ServiceProvider的publish指令 php artisan vendor:publish --provider="Zizaco\Entrust\EntrustServiceProvider" 另外: laravel的25个小技巧:25 Laravel Tips and Tricks