Laravel使用chunk自更新有误的原因
发表于|更新于|码不能停
|总字数:231|阅读时长:1分钟|浏览量:
Laravel 的 chunk 在查询大量数据时非常好用,但是在用某个字段做为检索条件,并且在程序内部有更新这个字段的时候,就会有一部分数据丢失。
原因是这样的,比如有7条数据,每次取2条,那么这7条数据就会被分为2,2,2,1这样几块,第一次调用时,取得第一页,也就是第一个2的数据块,更新完后再取数据,原来的第二页就变成了第一页,但是这时候取的还是第二页,所以这时候取的是第三个数据块,到了第三次调用的时候,原来的第二页变成了第一页,原来的第四页变成了第二页,第三页就变成了空的,所以第三页就什么都取不到了。
解决方法:
可以使用 laravel 的游标 cursor()

文章作者: m-finder
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 M-finder!
相关推荐

2019-03-15
Laravel 生命周期
Laravel 的生命周期主要分为四个阶段: 加载依赖 创建应用实例 接收请求并响应 请求结束进行回调 这四个阶段都在 index.php 中完成: 1234567891011121314151617181920<?php// 加载依赖require __DIR__.'/../vendor/autoload.php';// 创建应用实例$app = require_once __DIR__.'/../bootstrap/app.php';// 实例化 HTTP 内核$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);// 接收请求,生成响应$response = $kernel->handle( $request = Illuminate\Http\Request::capture());// 发送响应$response->send();// 请求结束,进行回调$kernel->terminate($request, $response); 1. 加载依赖laravel 框架依赖 composer 管理扩展包,通过引入 composer 的自动加载程序,就可以轻松完成扩展加载: 1require __DIR__.'/../vendor/autoload.php'; 2 创建应用实例这一步主要由以下几个小步骤组成: 创建应用实例 完成基础注册 基础绑定 基础服务提供者注册 event log route 核心类别名注册 绑定核心 创建应用实例,由 bootstrap/app.php 完成,然后注册三个核心。 1234567<?php// 第一部分: 创建应用实例$app = new Illuminate\Foundation\Application( realpath(__DIR__.'/../'));…… 2.1 完成基础注册应用实例创建后,再来看一下具体是怎么工作的,打开 Illuminate\Foundation\Application,其代码如下: 1234567891011121314public function __construct($basePath = null){ // 应用的路径绑定 if ($basePath) { $this->setBasePath($basePath); } // 将基础绑定注册到容器中,容器名 $this->registerBaseBindings(); // 将基础服务提供者注册到容器 Event、Log、Route $this->registerBaseServiceProviders(); // 将核心类别名注册到容器 $this->registerCoreContainerAliases();} 2.2 内核绑定接着看 bootstrap/app.php 中的代码: 12345678910111213141516171819……// 第二步,内核绑定$app->singleton( Illuminate\Contracts\Http\Kernel::class, App\Http\Kernel::class);$app->singleton( Illuminate\Contracts\Console\Kernel::class, App\Console\Kernel::class);$app->singleton( Illuminate\Contracts\Debug\ExceptionHandler::class, App\Exceptions\Handler::class); 绑定三个内核,HTTP、Console、Exception内核。 3 接收请求并响应再次回到 index.php,查看请求和响应的相关代码: 1234567891011……$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);$response = $kernel->handle( $request = Illuminate\Http\Request::capture());$response->send();…… 这一步也是由几个小步骤组成: 实例化 HTTP 核心 实例化内核 注册中间件到路由 session 共享错误 身份验证请求 …… 请求处理 创建请求实例 处理请求,返回响应 发送响应 3.1 注册中间件到路由在 Illuminate\Contracts\Http\Kernel::class 类的构造方法中,将在 HTTP 内核定义的「中间件」注册到路由,注册完后就可以在实际处理 HTTP 请求前调用这些「中间件」实现过滤请求的目的。 1234567891011121314151617181920212223242526272829303132protected $middlewarePriority = [ \Illuminate\Session\Middleware\StartSession::class, \Illuminate\View\Middleware\ShareErrorsFromSession::class, \Illuminate\Contracts\Auth\Middleware\AuthenticatesRequests::class, \Illuminate\Routing\Middleware\ThrottleRequests::class, \Illuminate\Session\Middleware\AuthenticateSession::class, \Illuminate\Routing\Middleware\SubstituteBindings::class, \Illuminate\Auth\Middleware\Authorize::class,];public function __construct(Application $app, Router $router){ $this->app = $app; $this->router = $router; $this->syncMiddlewareToRouter();}// 注册中间件到路由protected function syncMiddlewareToRouter(){ $this->router->middlewarePriority = $this->middlewarePriority; foreach ($this->middlewareGroups as $key => $middleware) { $this->router->middlewareGroup($key, $middleware); } foreach ($this->routeMiddleware as $key => $middleware) { $this->router->aliasMiddleware($key, $middleware); }} 3.2 处理请求处理请求实际包含两个阶段: 创建请求实例 处理请求 3.2.1 创建请求实例通过 Symfony 实例创建一个 Laravel 请求实例。 12345678910111213141516171819202122public static function capture(){ static::enableHttpMethodParameterOverride(); return static::createFromBase(SymfonyRequest::createFromGlobals());}public static function createFromBase(SymfonyRequest $request){ $newRequest = (new static)->duplicate( $request->query->all(), $request->request->all(), $request->attributes->all(), $request->cookies->all(), $request->files->all(), $request->server->all() ); $newRequest->headers->replace($request->headers->all()); $newRequest->content = $request->content; $newRequest->request = $newRequest->getInputSource(); return $newRequest;} 3.2.2 处理请求在 HTTP 核心的 handdle 方法内,接收一个请求,也就是上一步创建的请求实例,最终生成一个响应。 主要步驟如下: 注册请求到容器 运行引导程序 环境检测,将 env 中的配置读取到变量中 配置文件加载 加载异常处理 注册门面 注册服务提供者 服务启动 发送请求到路由 查找路由 运行控制器或匿名函数 返回响应 HTTP 核心的 handle 方法: 123456789101112131415161718public function handle($request){ try { $request->enableHttpMethodParameterOverride(); $response = $this->sendRequestThroughRouter($request); } catch (Throwable $e) { $this->reportException($e); $response = $this->renderException($request, $e); } $this->app['events']->dispatch( new RequestHandled($request, $response) ); return $response;} 再往下深入,查看 $response = $this->sendRequestThroughRouter($request); 的具体实现: 12345678910111213141516protected function sendRequestThroughRouter($request){ // 将请求注册到容器 $this->app->instance('request', $request); Facade::clearResolvedInstance('request'); // 启动引导程序 $this->bootstrap(); // 发送请求至路由 return (new Pipeline($this->app)) ->send($request) ->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware) ->then($this->dispatchToRouter());} 首先,将 request 注册到容器内,然后清除掉之前的 request 实例缓存,启动引导程序,然后将请求发送到路由。 接下来,看一下引导程序是做什么的: 12345678910111213141516171819202122232425262728293031323334protected $bootstrappers = [ \Illuminate\Foundation\Bootstrap\LoadEnvironmentVariables::class, \Illuminate\Foundation\Bootstrap\LoadConfiguration::class, \Illuminate\Foundation\Bootstrap\HandleExceptions::class, \Illuminate\Foundation\Bootstrap\RegisterFacades::class, \Illuminate\Foundation\Bootstrap\RegisterProviders::class, \Illuminate\Foundation\Bootstrap\BootProviders::class,];public function bootstrap(){ if (! $this->app->hasBeenBootstrapped()) { $this->app->bootstrapWith($this->bootstrappers()); }}protected function bootstrappers(){ return $this->bootstrappers;}// src/Illuminate/Foundation/Application.phppublic function bootstrapWith(array $bootstrappers){ $this->hasBeenBootstrapped = true; foreach ($bootstrappers as $bootstrapper) { $this['events']->dispatch('bootstrapping: '.$bootstrapper, [$this]); $this->make($bootstrapper)->bootstrap($this); $this['events']->dispatch('bootstrapped: '.$bootstrapper, [$this]); }} 在容器内的具体实现方法中,会先解析引导程序,然后再通过调用引导程序的 bootstrap 方法来启动服务。引导程序功能: 环境检测,将 env 配置文件载入到 $_ENV 变量中 加载配置文件 加载异常处理 加载 Facades,注册完成后可以用别名的方式访问具体的类 注册服务提供者,在这里我们会将配置在 app.php 文件夹下 providers 节点的服务器提供者注册到 APP 容器,供请求处理阶段使用 服务启动 在发送请求至路由这行代码中,完成了:管道(pipeline)创建、将 request 传入管道、对 request 执行中间件处理和实际的请求处理四个不同的操作。 1234return (new Pipeline($this->app)) ->send($request) ->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware) ->then($this->dispatchToRouter()); 继续深入 $this->dispatchToRouter(),分析程序是如何处理请求的: 注册请求 查找路由 运行控制器 返回响应结果 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364protected function dispatchToRouter(){ return function ($request) { // 将请求注册到容器 $this->app->instance('request', $request); return $this->router->dispatch($request); };}public function dispatch(Request $request){ $this->currentRequest = $request; return $this->dispatchToRoute($request);}public function dispatchToRoute(Request $request){ return $this->runRoute($request, $this->findRoute($request));}// 查找路由protected function findRoute($request){ $this->current = $route = $this->routes->match($request); $this->container->instance(Route::class, $route); return $route;}protected function runRoute(Request $request, Route $route){ $request->setRouteResolver(function () use ($route) { return $route; }); $this->events->dispatch(new RouteMatched($route, $request)); return $this->prepareResponse($request, $this->runRouteWithinStack($route, $request) );}protected function runRouteWithinStack(Route $route, Request $request){ $shouldSkipMiddleware = $this->container->bound('middleware.disable') && $this->container->make('middleware.disable') === true; $middleware = $shouldSkipMiddleware ? [] : $this->gatherRouteMiddleware($route); // 返回运行结果 return (new Pipeline($this->container)) ->send($request) ->through($middleware) ->then(function ($request) use ($route) { // 运行匹配到的路由控制器或匿名函数 return $this->prepareResponse( $request, $route->run() ); });} 执行 $route->run() 的方法定义在 Illuminate\Routing\Route 类中: 1234567891011121314public function run(){ $this->container = $this->container ?: new Container; try { if ($this->isControllerAction()) { return $this->runController(); } return $this->runCallable(); } catch (HttpResponseException $e) { return $e->getResponse(); }} 如果路由的实现是一个控制器,会完成控制器实例化并执行指定方法;如果是一个匿名函数就会直接调用。最终响应通过 prepareResponse 返回。 3.2.3 发送响应绕了一大圈,最后终于回到了开始的地方 12// 发送响应$response->send(); 最终发送,由 src/Illuminate/Http/Response.php 的父类 Symfony\Component\HttpFoundation\Response 完成: 12345678910111213public function send(){ $this->sendHeaders(); $this->sendContent(); if (\function_exists('fastcgi_finish_request')) { fastcgi_finish_request(); } elseif (!\in_array(\PHP_SAPI, ['cli', 'phpdbg'], true)) { static::closeOutputBuffers(0, true); } return $this;} 4 请求结束,进行回调1$kernel->terminate($request, $response); 继续往下看,核心的 terminate 方法: 123456public function terminate($request, $response){ $this->terminateMiddleware($request, $response); $this->app->terminate();} terminateMiddleware 中,进行终止中间件,$this->app->terminate() 终止程序。 总结创建应用实例,完成项目路径注册、基础服务注册、核心类别名注册,然后将 HTTP 和 Console, Exception 核心注册到容器。 然后再实例化内核,将中间件加载到路由,再将请求注册到容器,然后运行引导程序,进行环境检测、加载系统配置等系统环境配置。 然后进行中间件校验,通过校验后才会最终处理实际的控制器或匿名函数并生成响应。 最终,发送响应给用户,清理项目中的中间件,完成一个请求周期。

2019-03-15
使用 laravel mix 编译资源
学习下如何在 laravel 框架中,用 laravel mix 编译前端资源。 使用本次操作的环境依然是 laradock,如果没用特殊说明,以后应该默认 laradock。 workspace 容器中,已经提前装好了 node 环境,而在项目根目录中,package.json 和 webpack.mix.js 也已经为我们预设好了,所以laravel 项目建好后,直接在根目录安装即可: 1npm install 在 webpack.mix.js 中,已经加载了两个默认的文件: 12mix.js('resources/js/app.js', 'public/js') .sass('resources/sass/app.scss', 'public/css'); 我们只需要把自己的资源文件,按照同样的格式写入进去,然后开始运行,就可以生成编译后的资源了。 虽然示例中只写了 sass 一种样式文件,但是其实可以支持常见的以及不常见的很多中格式,只需要调用对应的接口即可。而且还可以把多个资源文件合并成一个。 举个🌰: 123456mix.less('resources/assets/less/app.less', 'public/stylesheets/styles.css');mix.styles([ 'public/css/vendor/normalize.css', 'public/css/vendor/videojs.css'], 'public/css/all.css'); 运行12npm run devnpm run watch 在上一篇中,我们修改视图后使之生效的命令,其实就是通知 mix 开始工作的。 单独使用在 laravel 框架之外也是可以使用 mix 的,具体教程请参考 [ learnku ] 其实不难,挺简单的。😎

2019-04-11
laravel 测试模块学习
学习下 larave 内置的测试模块。 当你想把一些东西写到 print 语句或者调试表达式中时,别这么做,将其写成一个测试来代替。 –Martin Fowler 最开始看到的关于 laravel 测试的信息是借助模型工厂来生成测试数据,今天查完资料发现,这只是测试中的一小部分。 laravel 内置了 PHPUnit 来做测试,并且已经做好了配置文件,还提供了一些便利的辅助函数,可以更直观的测试程序。 在 laravel 的项目中,包含一个 tests 目录,这个目录又有两个子目录:Feature 和 Unit 分别用来做功能测试和单元测试。 功能测试用于测试较大区块的代码,包括若干组件之间的交互,甚至一个完整的 HTTP 请求。 单元测试用于小的 、相互隔离的代码。 配置可以使用默认的配置,也可以创建一个 .env.testing 文件,在运行测试或执行带有 --env=testing 开关的 Artisan 命令时覆盖 .env 文件中的环境变量。 创建 & 运行测试运行 artisan 生成测试用例: 12345// 在 Feature 目录下创建测试类php artisan make:test UserTest// 在 Unit 目录下创建测试类php artisan make:test UserTest --unit 先生成一个单元测试,然后改造一下: 1234$user = DB::table('users')->where('id',1)->first();$name = $user->name;$username = ucfirst($user->name);$this->assertEquals($username, $name); 把用户名首s字母大写,然后判断和原用户名是否相等。 运行结果如下: HTTP测试新建一个路由和方法: 1234567Route::get('/user-info/{id?}', 'HomeController@userInfo');public function userInfo(){ $id = request('id'); $user = User::where('id', $id)->first(); return $user;} 新建测试12345678910111213141516php artisan make:test HttpStatusTest$user = DB::table('users')->where('id',1)->first();$username = ucfirst($user->name);$response = $this->get('/user-info/1');$response->assertStatus(200)->assertJson([ 'id' => 1, 'name'=> 'wu', 'email'=> 'yf-wu@qq.com', 'email_verified_at'=>null, 'created_at'=> '2019-04-09 07:36:52', 'updated_at'=> '2019-04-09 07:36:52']); 运行测试发现接口需要登录,所以会被拦截: 所以需要先模拟用户登录,可以使用:Auth::loginUsingId(1); 使 id 为 1 的用户强制登录。 再次运行,结果 ok。 数据库测试数据库测试功能点更多,可以验证表中是否存在某条数据,也可以用来生成测试数据等。 123$this->assertDatabaseHas('users', [ 'email' => 'sally@example.com']); 也可以使用 assertDatabaseMissing 帮助程序断言数据库中不存在数据。 生成模型工厂运行命令生成模型工厂: 1php artisan make:factory PostFactory 在项目的 database\factories 目录中,已经预先生成了一个 UserFactory: 123456789$factory->define(User::class, function (Faker $faker) { return [ 'name' => $faker->name, 'email' => $faker->unique()->safeEmail, 'email_verified_at' => now(), 'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password 'remember_token' => Str::random(10), ];}); 改造之前的 TestUser 来测试一下: 123456public function testExample() { $user = factory(User::class)->create(['email' => 'm@m-finder.com']); $this->assertDatabaseHas('users', [ 'email' => 'm@m-finder.com' ]);} 状态ok。 更多操作还是要参考: [ PHPUnit ]

2018-06-12
Laravel使用ftp传输文件时报错ftp_put() No data connection的解决
代码: 123456789101112131415161718192021222324252627282930313233<?php$file = "test.dat";$ftp_server="ftp.server.com";$ftp_user = "myname";$ftp_pass = "mypass";$destination_file = "test.dat";$cid=ftp_connect($ftp_server);if(!$cid) { exit("Could not connect to server: $ftp_server\n");}$login_result = ftp_login($cid, $ftp_user, $ftp_pass);if (!$login_result) { echo "FTP connection has failed!"; echo "Attempted to connect to $ftp_server for user $ftp_user"; exit;} else {echo "Connected to $ftp_server, for user $ftp_user";}$upload = ftp_put($cid, $destination_file, $file, FTP_BINARY);if (!$upload) { echo "Failed upload for $source_file to $ftp_server as $destination_file<br>"; echo "FTP upload has failed!";} else { echo "Uploaded $source_file to $ftp_server as $destination_file";}ftp_close($cid);?> 原因是没有定义ftp的主被动模式,true是被动模式: 1ftp_pasv($cid, true);

2023-11-20
为你的 Laravel 应用添加一个基于 Swoole 的 WebSocket 服务
做了一个基于 Swoole 的 WebSocket 扩展包,可以用来做实时状态推送,或者自定义消息处理实现 im,有需要的可以看看: [giorgio-socket] 使用方法安装安装扩展包 1composer require wu/giorgio-socket 发布配置文件 1php artisan vendor:publish --provider="GiorgioSocket\Providers\SocketServiceProvider" 运行 Socket 服务 1php artisan socket:start 注意事项 可以通过实现 GiorgioSocket\Services\Handlers\Interfaces 下的接口类来自定义自己的业务逻辑。 如果要从服务端发送消息,这里有两种方式: 第一种,借助 Laravel HTTP 客户端 123456Route::get('/socket', function () { \Illuminate\Support\Facades\Http::asForm()->post('http://127.0.0.1:9501', [ 'to' => 2, 'message' => 'server message', ]);}); 第二种:借助 Laravel Listener,需要将 .env 文件中的 QUEUE_CONNECTION 配置修改为 redis 或其他异步队列。配置更改后,运行以下命令:php-artisan queue:work --queue=socket-listener监听队列,然后按以下代码调用 event: 123Route::any('socket', function (Request $request){ \GiorgioSocket\Events\SocketEvent::dispatch($request->get('to'), $request->get('message'));}); 如果你正在使用 laravel/breeze 扩展包,并且使用了 Blade 模板,可以将以下代码粘贴到 dashboard.blade.php 中进行快速测试: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111@auth <div class="py-12"> <div class="max-w-7xl mx-auto sm:px-6 lg:px-8"> <div class="bg-white dark:bg-gray-800 overflow-hidden shadow-sm sm:rounded-lg"> <div class="grid grid-cols-1 md:grid-cols-2"> <div class="p-6" id="server-message"> messages:<br/> </div> <div class="p-6"> <label class="block font-medium text-sm text-gray-700 dark:text-gray-300" for="from">from</label> <input class="border-gray-300 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-300 focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 rounded-md shadow-sm block mt-1 w-full" value="{{ auth()->user()->getKey() }}" id="from"> <label class="block font-medium text-sm text-gray-700 dark:text-gray-300" for="to">to</label> <input class="border-gray-300 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-300 focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 rounded-md shadow-sm block mt-1 w-full" value="" id="to"> <label class="block font-medium text-sm text-gray-700 dark:text-gray-300" for="message">message</label> <textarea class="border-gray-300 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-300 focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 rounded-md shadow-sm block mt-1 w-full" id="message"></textarea> <input class="inline-flex items-center px-4 py-2 bg-gray-800 dark:bg-gray-200 border border-transparent rounded-md font-semibold text-xs text-white dark:text-gray-800 uppercase tracking-widest hover:bg-gray-700 dark:hover:bg-white focus:bg-gray-700 dark:focus:bg-white active:bg-gray-900 dark:active:bg-gray-300 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800 transition ease-in-out duration-150 mt-3" type="button" id="submit" value="submit"> </div> </div> </div> </div> </div> <script type="text/javascript"> let heartBeatTimer = 0; let socket = connectWebSocket(); function startHeartbeat(interval) { interval = interval || 30; heartBeatTimer = setInterval(function () { sendMessage(null, "heart_beat"); }, interval * 1000); } function stopHeartbeat() { clearInterval(heartBeatTimer); } function connectWebSocket() { const wsServer = 'ws://127.0.0.1:9501'; const socket = new WebSocket(wsServer); let userId = document.getElementById('from').value; socket.onopen = function (evt) { let data = { user_id: userId, type: 'connect' }; console.log('open', data) socket.send(JSON.stringify(data)); }; socket.onmessage = function (evt) { console.log('get message from server: ' + evt.data); if (evt.data !== 'heart_beat') { let data = JSON.parse(evt.data); let message = document.getElementById("server-message") message.innerHTML += data.user_name + ': ' + data.data + '<br/>' } }; socket.onerror = function (evt) { console.log(evt); }; socket.onclose = function () { let data = { user_id: userId, type: 'close' }; socket.send(JSON.stringify(data)); }; return socket; } function sendMessage(to, message) { if (socket != null && socket.readyState === WebSocket.OPEN) { if (message !== 'heart_beat') { let messageBox = document.getElementById("server-message") messageBox.innerHTML += 'me: ' + message + '<br/>' } let from = document.getElementById("from") socket.send(JSON.stringify({ user_id: from.value, user_name: '{{ auth()->user()->name }}', to: to, type: 'message', data: message, })); console.log("webSocket send message:" + JSON.stringify({ user_id: from.value, user_name: '{{ auth()->user()->name }}', to: to, type: 'message', data: message, })); } else { console.log("webSocket closed"); } } let button = document.getElementById("submit"); button.addEventListener('click', function () { let message = document.getElementById("message"); let to = document.getElementById("to"); sendMessage(to.value, message.value) }); </script>@endauth 如有任何疑问,欢迎提交 [issue]

2019-03-15
Laravel 服务容器
在 Laravel 生命周期中,我们了解到框架运行过程中,会通过创建应用实例来完成很多事情,这个应用实例,也就是我们今天的主角,服务容器。 Laravel 的服务容器,是用于管理类的依赖和执行依赖注入的工具。 依赖注入 DI开始之前,需要我们先了解一下,什么是依赖注入。 简单来说,就是将类的依赖通过参数或其他方式注入。 比如: 12345678910111213141516171819202122232425262728293031323334interface Storage{ public function set($key, $value); public function get($key);}class SessionStorage implements Storage{ function __construct($cookieName='PHPSESSID'){ session_name($cookieName); session_start(); } function set($key, $value){ $_SESSION[$key] = $value; } function get($key){ return $_SESSION[$key]; }}class User{ private $storage; function __construct(Storage $storage){ $this->storage = $storage; } function setLanguage($language){ $this->storage->set('language', $language); }}$storage = new SessionStorage('SESSION_ID');$user = new User($storage); 依赖注入 并不局限于构造函数,也可以通过设值方法注入,或者类成员变量方式,通过构造函数注入适用于必要的依赖,设值注入适用于可选依赖,比如项目需要一个缓存功能的实现。在上面的例子中,我们如果需要改用 Redis 或者 MongoDB 来存储数据,只需要继承并实现 Storage 接口,然后在外部就可以很轻松的切换服务了。 依赖注入容器 IOC在实际的开发中,用上边的依赖注入方式还是很累的,所以,我们还需要了解一个新的概念,依赖注入容器,也可以叫控制反转。 简单来说,依赖注入容器就是将组件间的依赖关系由程序内部提到外部容器来管理,也就是将依赖的配置和使用分开,原本是程序控制执行流程,现在程序反倒成了被控制的对象,也就形成了控制反转。 通常用于管理大量依赖组件的实例。比如一个框架。 首先,我们可以定义一个容器: 123456789101112131415class Container{ public function getStorage() { return new SessionStorage(); } public function getUser() { return new User($this->getStorage()); }}// 更改实例化方式$container = new Container();$user = $container->getUser(); 在这个容器中,我们只需要调用容器 getUser 方法,既可以获取到 User 实例,并不需要关心它是怎么创建出来的。 但是,这个容器还存在一些问题,Storage 的实例化还是硬编码,如果要切换其他服务,只能通过改代码的方式。 对此,我们可以再次升级容器: 1234567891011121314151617181920212223242526272829303132333435363738394041class Container{ protected $binds; protected $instances; // 绑定 public function bind($abstract, $concrete) { // 判断是否为匿名函数 if ($concrete instanceof Closure) { $this->binds[$abstract] = $concrete; } else { $this->instances[$abstract] = $concrete; } } // 实例化 public function make($abstract, $parameters = []) { if (isset($this->instances[$abstract])) { return $this->instances[$abstract]; } array_unshift($parameters, $this); return call_user_func_array($this->binds[$abstract], $parameters); }}$container = new Container;$container->bind('Storage', function($container){ return new SessionStorage;});$container->bind('User',function($container,$module){ return new User($container->make($module));});$user = $container->make('User',['Storage']); 一个类似于 laravel 的服务容器就好了,当然 larave 的服务容器比这个要复杂的多。 总的来说,laravel 的服务容器有两大功能: 注册基础服务 管理需要实例化的类及其依赖 Laravel 服务容器的使用方法laravel 服务容器在使用时一般分为两个阶段:使用之前进行绑定(bind)完成将实现绑定到接口;使用时对通过接口解析(make)出服务。 laravel 内置多种不同的绑定方法以用于不同的使用场景: bind 简单绑定 singleton 单例绑定 instance 实例绑定 contextual-binding 上下文绑定 还有好几种,看文档吧 它们的最终目标是一致的:绑定接口到实现。 这样的好处是在项目的编码阶段建立起接口和实现的映射关系,到使用阶段通过抽象类(接口)解析出它的具体实现,这样就实现了项目中的解耦。 bindbind 方法的功能是将实现与接口进行绑定,然后在每次执行服务解析操作时,Laravel 容器都会重新创建实例对象。 例如: 123456789101112131415161718$this->app->bind( UserRepositoryInterface::class, UserRepository::class);class UserController extends Controller{ private $repository; function __construct(UserRepositoryInterface $userRepository) { $this->repository = $userRepository; } function users(){ return $this->repository->all(); }} 在服务提供者中,将 User 仓库的具体实现与接口进行绑定,使用时可以直接通过接口注入依赖。 singleton采用单例绑定时,仅在首次解析时创建实例,后续使用 make 进行解析服务操作都将直接获取这个已解析的对象,实现共享操作。 绑定处理类似 bind 绑定,只需将 bind 方法替换成 singleton 方法即可。 instance将已经创建的实例对象绑定到接口以供后续使用,这种使用场景类似于注册表。 比如用于存储用户模型: 12345678// 创建一个用户实例$artisan = new User();// 将实例绑定到服务容器App::instance('login-user', $artisan);// 获取用户实例$artisan = App::make('login-user'); contextual-binding主要用于一个接口多处实现,然后根据不同控制器去进行判断具体应该用哪个实现。 12345$this->app->when(PhotoController::class) ->needs(Filesystem::class) ->give(function () { return Storage::disk('local'); });