Laravel中集成PayPal

最近在写一个面向国外买家的一个商城项目,既然面向国外,那就要用到PayPal这个支付平台。因为在对接PayPal的过程中遇到了一些问题,花费了一些时间,所以把对接的过程记下来,也希望能帮助到用到PayPal的朋友。 我集成的是paypal/rest-api-sdk-php。PayPal的api有 v1和v2两个版本,我用的这个是v1版本。 以下皆以代码的形式展现,没有截图,但我尽量用代码讲解明白。假设你已经有了用laravel写的一个项目:Step:1安装扩展包composer require paypal/rest-api-sdk-phpStep:2创建paypal.php配置文件在Config目录新建paypal.php配置文件,内容如下return [ ‘paypal’ => [ /** set your paypal credential / ‘client_id’ =>‘paypal client_id’, ‘secret’ => ‘paypal secret ID’, / * SDK 配置 / ‘settings’ => array( /* * 沙盒测试’sandbox’ 或者 ’live’ / ‘mode’ => ‘sandbox’, /* * 请求超时时间 / ‘http.ConnectionTimeOut’ => 1000, /* * 是否开启日志:true开启,false不开启 / ’log.LogEnabled’ => true, /* * 日志存储的文件 / ’log.FileName’ => storage_path() . ‘/logs/paypal.log’, /* * 日志级别 ‘DEBUG’, ‘INFO’, ‘WARN’ or ‘ERROR’ * / ’log.LogLevel’ => ‘INFO’ ), ], ‘2checkout’ => [ // ]];Step:3创建路由 // 第一步:显示表单,这是一个简单的表单页面,我们把金额输入,然后点击提交 Route::get(‘paypal-form’, ‘Payment\PayPalController@payPalShow’)->name(‘paypal-form’); // 第二步:第一步提交后发送请求,请求该方法,该方法用于创建订单,创建支付流程 Route::post(‘paypal-pay’, ‘Payment\PayPalController@pay’)->name(‘payment.paypay.pay’); // 第三步:异步回调 Route::post(‘paypal-notify’, ‘Payment\PayPalController@payPalNotify’)->name(‘payment.paypal.notify’); // 第三步:前端回调 Route::get(‘paypal-return’, ‘Payment\PayPalController@payPalReturn’)->name(‘payment.paypal.return’); // 第三步:取消 Route::get(‘paypal-cancel’, ‘Payment\PayPalController@payPalCancel’)->name(‘payment.paypal.cancel’);Step:3创建控制器<?phpnamespace App\Http\Controllers\Payment;use App\Http\Controllers\BaseController;use Illuminate\Http\Request;use Illuminate\Support\Facades\Input;use PayPal\Api\Amount;use PayPal\Api\Item;use PayPal\Api\ItemList;use PayPal\Api\Payer;use PayPal\Api\Payment;use PayPal\Api\PaymentExecution;use PayPal\Api\RedirectUrls;use PayPal\Api\Transaction;use PayPal\Auth\OAuthTokenCredential;use PayPal\Rest\ApiContext;class PayPalController extends BaseController{ private $_api_context; public function __construct() { $payPal_config = config(‘payment.payPal’); // 初始化 PayPal Api context $this->_api_context = new ApiContext(new OAuthTokenCredential( $payPal_config[‘client_id’], $payPal_config[‘secret’] )); $this->_api_context->setConfig($payPal_config[‘setting’]); } // 显示表单 public function payPalShow() { return view(‘payment.paypal’); } // 第二步,请求这里 public function pay(Request $request) { $payer = new Payer(); $payer->setPaymentMethod(‘paypal’); // 产品名称,币种,数量,单个产品金额 $item1 = new Item(); $item1->setName(‘item1’) ->setCurrency(‘USD’) ->setQuantity(1) ->setPrice($request->get(‘amount’)); // 将所有产品集合到 ItemList中 $item_list = new ItemList(); $item_list->setItems([$item1]); $amount = new Amount(); $amount->setCurrency(‘USD’) ->setTotal($request->get(‘amount’)); // 生成交易 $transaction = new Transaction(); $transaction->setAmount($amount) ->setItemList($item_list) ->setDescription(‘你的交易’) ->setNotifyUrl(route(’notify_url’)) // 注意,这里设置异步回调地址 ->setInvoiceNumber($order->order_number); // 这里设置订单号 // 设置前端回调地址和取消支付地址 $redirect_urls = new RedirectUrls(); $redirect_urls->setReturnUrl(route(‘payment.paypal.cancel’)) ->setCancelUrl(route(‘payment.paypal.cancel’)); $payment = new Payment(); $payment->setIntent(‘Sale’) ->setPayer($payer) ->setRedirectUrls($redirect_urls) ->setTransactions(array($transaction)); try { // 这里生成支付流程 $payment->create($this->_api_context); } catch (\PayPal\Exception\PayPalConnectionException $ex) { if (config(‘app.debug’)) { session()->put(’error’,‘Connection timeout’); return redirect()->route(‘paypal-form’); /* echo “Exception: " . $ex->getMessage() . PHP_EOL; / / $err_data = json_decode($ex->getData(), true); / / exit; **/ } else { session()->put(’error’,‘Some error occur, sorry for inconvenient’); return redirect()->route(‘paypal-form’); } } foreach($payment->getLinks() as $link) { if($link->getRel() == ‘approval_url’) { $redirect_url = $link->getHref(); break; } } // $payment->getId()得到的是支付流水号 session()->put(‘paypal_payment_id’, $payment->getId()); if(isset($redirect_url)) { // 跳转到支付页面 return redirect()->away($redirect_url); } session()->put(’error’,‘Unknown error occurred’); return session()->route(‘paypal-form’); } public function payPalReturn(Request $request) { // 支付成功后,在前端页面跳转回来时,url地址中会带有paymentID 和 PayerID $payment_id = session()->get(‘paypal_payment_id’); session()->forget(‘paypal_payment_id’); if (empty(Input::get(‘PayerID’)) || empty(Input::get(’token’))) { session()->put(’error’,‘Payment failed’); return redirect()->route(‘paypal-form’); } $payment = Payment::get($payment_id, $this->_api_context); $execution = new PaymentExecution(); $execution->setPayerId(Input::get(‘PayerID’)); $result = $payment->execute($execution, $this->_api_context); // 当拿到的状态时approved表示支付成功 if ($result->getState() == ‘approved’) { session()->put(‘success’,‘Payment success’); return redirect()->route(‘paypal-form’); } session()->put(’error’,‘Payment failed’); return redirect()->route(‘paypal-form’); } public function payPalNotify() { Log::info(12312, Input::get()); // 这里写我们的业务逻辑,订单状态更新,物流信息等等. }}Step:4创建表单<form class=“form-horizontal” method=“POST” id=“payment-form” role=“form” action=”{!! URL::route(‘paypal-pay’) !!}" > {{ csrf_field() }} <div class=“form-group{{ $errors->has(‘amount’) ? ’ has-error’ : ’’ }}"> <label for=“amount” class=“col-md-4 control-label”>Amount</label> <div class=“col-md-6”> <input id=“amount” type=“text” class=“form-control” name=“amount” value=”{{ old(‘amount’) }}" autofocus> @if ($errors->has(‘amount’)) <span class=“help-block”> <strong>{{ $errors->first(‘amount’) }}</strong> </span> @endif </div> </div> <div class=“form-group”> <div class=“col-md-6 col-md-offset-4”> <button type=“submit” class=“btn btn-primary”> Paywith Paypal </button> </div> </div></form>以上既是我做商城项目时PayPal的对接流程,因为英语不好的问题,开发起来会出现很多问题,如果英文好,想知道更多的用法,可以看PayPal的开发者文档,还有demo演示。 ...

April 18, 2019 · 2 min · jiezi

[教程] 大白话 Laravel 中间件

文章转自:https://learnku.com/laravel/t… Laravel 中间件是什么?简而言之,中间件在 laravel 中的作用就是过滤 HTTP 请求,根据不同的请求来执行不同的逻辑操作。我们可以通过中间件实现以下功能:指定某些路由设置 HTTP 响应头记录请求过滤请求的参数决定是否启用站点维护模式响应前后做一些必要的操作自定义中间件命令行执行下面的简单命令,就可以轻松创建一个新的中间件php artisan make:middleware <MiddlewareName>//MiddlewareName 就是你要创建的中间件的名字执行上面的命令,Laravel 会在 app/Http/Middleware 目录下自动创建一个只包含 handle 方法的中间件。<?phpnamespace App\Http\Middleware;use Closure;class RedirectIfSuperAdmin{ /** * Handle an incoming request. * * @param \Illuminate\Http\Request $request * @param \Closure $next * @return mixed / public function handle($request, Closure $next) { return $next($request); }}在中间件被调用的时候,handle 方法就会执行。这里需要注意的是 handle 方法默认有两个参数 $request 和 $next 。 $request 用来接受应用的请求组求, $next 将请求传递给应用程序。这两个参数是 handle 必不可少的!中间件也包括前置中间件和后置中间件。“前置中间件” 顾名思义在将请求转发到应用程序之前处理一些逻辑。 另一方面,在中间件之后,在应用程序处理了请求并生成响应之后运行。前置中间件:<?phpnamespace App\Http\Middleware;use Closure;class RedirectIfSuperAdmin{ /* * Handle an incoming request. * * @param \Illuminate\Http\Request $request * @param \Closure $next * @return mixed / public function handle($request, Closure $next) { //你的逻辑就在这里 return $next($request); }}后置中间件:<?phpnamespace App\Http\Middleware;use Closure;class RedirectIfSuperAdmin{ /* * Handle an incoming request. * * @param \Illuminate\Http\Request $request * @param \Closure $next * @return mixed / public function handle($request, Closure $next) { $response = $next($request); //你的逻辑就在这里 例如 重定向到 / return $response; }}中间件的类别全局中间件路由中间件全局中间件针对命中应用程序的每个请求运行。 Laravel 自带了大多数这些中间件例如 ValidatePostSize, TrimStrings,CheckForMaintenanceMode 等等.路由中间件仅在它们所连接的路由上运行例如 redirectIfAuthenticated.注册中间件创建的任何中间件都必须注册,因为这是 Laravel 知道存在的唯一方式。 要注册中间件,只需打开名为 kernel.php 的文件,该文件位于 Http 文件夹中,如下所示:This file contains list of all registered middlewares that come with Laravel by default. it contains three major arrays which 此文件包含默认 Laravel 提供的所有已注册中间件的列表。 它包含三个主要的中间件组 $middleware , $middlewareGroups 和 $routeMiddleware<?phpnamespace App\Http;use Illuminate\Foundation\Http\Kernel as HttpKernel;class Kernel extends HttpKernel{ /* * 应用程序的全局HTTP中间件。 * * 这些中间件在应用程序的每个请求期间运行。 * * @var array / protected $middleware = [ \Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class, \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class, \App\Http\Middleware\TrimStrings::class, \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class, \App\Http\Middleware\TrustProxies::class, ]; /* * 应用程序的路由中间件组. * * @var array / protected $middlewareGroups = [ ‘web’ => [ \App\Http\Middleware\EncryptCookies::class, \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class, \Illuminate\Session\Middleware\StartSession::class, // \Illuminate\Session\Middleware\AuthenticateSession::class, \Illuminate\View\Middleware\ShareErrorsFromSession::class, \App\Http\Middleware\VerifyCsrfToken::class, \Illuminate\Routing\Middleware\SubstituteBindings::class, ], ‘api’ => [ ’throttle:60,1’, ‘bindings’, ], ]; /* * 应用程序的路由中间件. * * 可以将这些中间件分配给组或单独使用。 * * @var array */ protected $routeMiddleware = [ ‘auth’ => \Illuminate\Auth\Middleware\Authenticate::class, ‘auth.basic’ => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class, ‘bindings’ => \Illuminate\Routing\Middleware\SubstituteBindings::class, ‘can’ => \Illuminate\Auth\Middleware\Authorize::class, ‘guest’ => \App\Http\Middleware\RedirectIfAuthenticated::class, ’throttle’ => \Illuminate\Routing\Middleware\ThrottleRequests::class, //the just created middlware ‘superadmin’ => \App\Http\Middleware\RedirectIfSuperAdmin::class, ];} $middleware 数组包含全局中间件,它运行应用程序的每个HTTP请求,所以如果你想为每个请求运行一个中间件,你应该在这里注册它。 $middlewareGroups 使得可以在组中注册中间件,从而更容易通过使用组名将大量中间件附加到路由。 $routeMiddleware 数组包含各个注册的路由中间件。分配中间件有两个主要方法可以把注册好的中间件应用到路由中。通过控制器的构造方法通过路由通过构造方法分配中间件通过构造方法分配中间有很大的灵活性,它提供了两个重要的方法except($parameters) 和 only($parameters),这两个方法可以允许或阻止中间件应用到控制器中的辅助方法。不使用这两个方法,中间件将使用与控制器的每个方法。<?phpuse Illuminate\Http\Request;class ForumController extends Controller{ public function __construct(){ /**in this case the middleware named auth is applied to every single function within this controller */ $this->middleware(‘auth’); } public function viewForum(){ return view(‘index’); } public function edit($id){ } public function delete($id){ }}使用 except 和 only 方法我们可以选择把中间件应用到指定方法。<?phpuse Illuminate\Http\Request;class ForumController extends Controller{ public function __construct(){ /the authentication middleware here applies to all functions but viewForums() and viewForumDetails() and the opposite of this happens when you use only() / $this->middleware(‘auth’)->except([‘viewForums’, ‘viewForumDetails’]); } public function viewForums(){ return view(‘index’); } public function edit($id){ } public function delete($id){ } public function viewForumDetails(){ }}通过路由分配中间件如果注册的中间件可以直接附加到路由,如下所示:<?php//方法 1Route::get(‘admin/profile’, function () { //action})->middleware(‘auth’);/**方法 2或者像这样使用完全限定的类名:/use App\Http\Middleware\CheckAge;Route::get(‘admin/profile’, function () { // action})->middleware(CheckAge::class);//方法 3Route::group([‘middleware’ => [‘web’]], function () { //action});N:B 中间件组可以像单个中间件一样分配给路由中间件参数其他参数可以传递给中间件。 典型示例是将每个用户ID分配给角色,中间件检查用户的角色以确定是否有权访问所请求的 URI。 参数可以传递给中间件,如下所示:<?php//方法1 (Through route)Route::get(‘admin/profile’, function () { //action})->middleware(‘auth:<role>’); //<role> 这里应该被用户想要传递的任何参数替换。//方法2 (Through a controller)use Illuminate\Http\Request;class ForumController extends Controller{ public function __construct(){ $this->middleware(‘auth:<role>’); } }通过用逗号分隔每个参数,可以将多个参数传递给中间件。<?phpRoute::get(‘admin/profile’, function () { //action})->middleware(‘auth:<role>,<age>,<country>’); //<role>, <age>, <country> 这里应该被用户想要传递的任何参数替换。这些参数在 $next 变量之后传递给中间件的 handle 函数<?phpclass RedirectIfSuperAdmin{ / * Handle an incoming request. * * @param \Illuminate\Http\Request $request * @param \Closure $next * @return mixed */ public function handle($request, Closure $next, $role, $age, $country) { //使用解析参数的中间件逻辑 return $next($request); }}总结要创建中间件,请执行以下过程使用 artisan 命令创建中间件 php artisan make:middleware 中间件名.在app→Http文件夹中的 kernel.php 中注册中间件在创建的中间件中编写逻辑将中间件分配给路由或控制器ConclusionLaravel中间件可以更轻松地保护我们的路由,过滤输入并完成许多其他工作,而无需编写如此多的逻辑。 查看官方 Laravel 文档 这里 了解中间件的更多功能,最重要的是练习。文章转自:https://learnku.com/laravel/t… 更多文章:https://learnku.com/laravel/c… ...

April 16, 2019 · 3 min · jiezi

超全的设计模式简介(45种)

该文建议配合 design-patterns-for-humans 中文版 一起看。推荐阅读超全的设计模式简介(45种)design-patterns-for-humans 中文版(github 仓库永久更新)MongoDB 资源、库、工具、应用程序精选列表中文版有哪些鲜为人知,但是很有意思的网站?一份攻城狮笔记每天搜集 Github 上优秀的项目一些有趣的民间故事超好用的谷歌浏览器、Sublime Text、Phpstorm、油猴插件合集设计模式(Design pattern)代表了最佳的实践,通常被有经验的面向对象的软件开发人员所采用。设计模式是软件开发人员在软件开发过程中面临的一般问题的解决方案。这些解决方案是众多软件开发人员经过相当长的一段时间的试验和错误总结出来的。 设计模式是一套被反复使用的、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了重用代码、让代码更容易被他人理解、保证代码可靠性。 毫无疑问,设计模式于己于他人于系统都是多赢的,设计模式使代码编制真正工程化,设计模式是软件工程的基石,如同大厦的一块块砖石一样。项目中合理地运用设计模式可以完美地解决很多问题,每种模式在现实中都有相应的原理来与之对应,每种模式都描述了一个在我们周围不断重复发生的问题,以及该问题的核心解决方案,这也是设计模式能被广泛应用的原因。设计模式的类型共有 23 种设计模式。这些模式可以分为三大类:创建型模式(Creational Patterns)- 这些设计模式提供了一种在创建对象的同时隐藏创建逻辑的方式,而不是使用 new 运算符直接实例化对象。这使得程序在判断针对某个给定实例需要创建哪些对象时更加灵活。工厂模式(Factory Pattern)抽象工厂模式(Abstract Factory Pattern)单例模式(Singleton Pattern)建造者模式(Builder Pattern)原型模式(Prototype Pattern)对象池模式 (Pool)多例模式 (Multiton)静态工厂模式 (Static Factory)结构型模式(Structural Patterns)- 这些设计模式关注类和对象的组合。继承的概念被用来组合接口和定义组合对象获得新功能的方式。适配器模式(Adapter Pattern)桥接模式(Bridge Pattern)过滤器模式(Filter、Criteria Pattern)组合模式(Composite Pattern)装饰器模式(Decorator Pattern)外观模式(Facade Pattern)享元模式(Flyweight Pattern)代理模式(Proxy Pattern)数据映射模式 (Data Mapper)依赖注入模式 (Dependency Injection)门面模式 (Facade)流接口模式 (Fluent Interface)注册模式 (Registry)行为型模式(Behavioral Patterns)- 这些设计模式特别关注对象之间的通信。责任链模式(Chain of Responsibility Pattern)命令模式(Command Pattern)解释器模式(Interpreter Pattern)迭代器模式(Iterator Pattern)中介者模式(Mediator Pattern)备忘录模式(Memento Pattern)观察者模式(Observer Pattern)状态模式(State Pattern)空对象模式(Null Object Pattern)策略模式(Strategy Pattern)模板模式(Template Pattern)访问者模式(Visitor Pattern)规格模式 (Specification)访问者模式 (Visitor)J2EE 设计模式 - 这些设计模式特别关注表示层。这些模式是由 Sun Java Center 鉴定的。MVC模式(MVC Pattern)业务代表模式(Business Delegate Pattern)组合实体模式(Composite Entity Pattern)数据访问对象模式(Data Access Object Pattern)前端控制器模式(Front Controller Pattern)拦截过滤器模式(Intercepting Filter Pattern)服务定位器模式(Service Locator Pattern)传输对象模式(Transfer Object Pattern)委托模式 (Delegation)资源库模式 (Repository)下面用一个图片来整体描述一下设计模式之间的关系:设计模式的六大原则1、开闭原则(Open Close Principle)开闭原则的意思是:对扩展开放,对修改关闭。在程序需要进行拓展的时候,不能去修改原有的代码,实现一个热插拔的效果。简言之,是为了使程序的扩展性好,易于维护和升级。想要达到这样的效果,我们需要使用接口和抽象类,后面的具体设计中我们会提到这点。2、里氏代换原则(Liskov Substitution Principle)里氏代换原则是面向对象设计的基本原则之一。 里氏代换原则中说,任何基类可以出现的地方,子类一定可以出现。LSP 是继承复用的基石,只有当派生类可以替换掉基类,且软件单位的功能不受到影响时,基类才能真正被复用,而派生类也能够在基类的基础上增加新的行为。里氏代换原则是对开闭原则的补充。实现开闭原则的关键步骤就是抽象化,而基类与子类的继承关系就是抽象化的具体实现,所以里氏代换原则是对实现抽象化的具体步骤的规范。3、依赖倒转原则(Dependence Inversion Principle)这个原则是开闭原则的基础,具体内容:针对接口编程,依赖于抽象而不依赖于具体。4、接口隔离原则(Interface Segregation Principle)这个原则的意思是:使用多个隔离的接口,比使用单个接口要好。它还有另外一个意思是:降低类之间的耦合度。由此可见,其实设计模式就是从大型软件架构出发、便于升级和维护的软件设计思想,它强调降低依赖,降低耦合。5、迪米特法则,又称最少知道原则(Demeter Principle)最少知道原则是指:一个实体应当尽量少地与其他实体之间发生相互作用,使得系统功能模块相对独立。6、合成复用原则(Composite Reuse Principle)合成复用原则是指:尽量使用合成 / 聚合的方式,而不是使用继承。工厂模式工厂模式(Factory Pattern)最常用的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。 在工厂模式中,我们在创建对象时不会对客户端暴露创建逻辑,并且是通过使用一个共同的接口来指向新创建的对象。介绍意图: 定义一个创建对象的接口,让其子类自己决定实例化哪一个工厂类,工厂模式使其创建过程延迟到子类进行。主要解决: 主要解决接口选择的问题。何时使用: 我们明确地计划不同条件下创建不同实例时。如何解决: 让其子类实现工厂接口,返回的也是一个抽象的产品。关键代码: 创建过程在其子类执行。应用实例:您需要一辆汽车,可以直接从工厂里面提货,而不用去管这辆汽车是怎么做出来的,以及这个汽车里面的具体实现。Hibernate 换数据库只需换方言和驱动就可以。优点:一个调用者想创建一个对象,只要知道其名称就可以了。扩展性高,如果想增加一个产品,只要扩展一个工厂类就可以。屏蔽产品的具体实现,调用者只关心产品的接口。缺点: 每次增加一个产品时,都需要增加一个具体类和对象实现工厂,使得系统中类的个数成倍增加,在一定程度上增加了系统的复杂度,同时也增加了系统具体类的依赖。这并不是什么好事。使用场景:日志记录器:记录可能记录到本地硬盘、系统事件、远程服务器等,用户可以选择记录日志到什么地方。数据库访问,当用户不知道最后系统采用哪一类数据库,以及数据库可能有变化时。设计一个连接服务器的框架,需要三个协议,“POP3”、“IMAP”、“HTTP”,可以把这三个作为产品类,共同实现一个接口。注意事项: 作为一种创建类模式,在任何需要生成复杂对象的地方,都可以使用工厂方法模式。有一点需要注意的地方就是复杂对象适合使用工厂模式,而简单对象,特别是只需要通过 new 就可以完成创建的对象,无需使用工厂模式。如果使用工厂模式,就需要引入一个工厂类,会增加系统的复杂度。抽象工厂模式抽象工厂模式(Abstract Factory Pattern)是围绕一个超级工厂创建其他工厂。该超级工厂又称为其他工厂的工厂。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。 在抽象工厂模式中,接口是负责创建一个相关对象的工厂,不需要显式指定它们的类。每个生成的工厂都能按照工厂模式提供对象。介绍意图: 提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。主要解决: 主要解决接口选择的问题。何时使用: 系统的产品有多于一个的产品族,而系统只消费其中某一族的产品。如何解决: 在一个产品族里面,定义多个产品。关键代码: 在一个工厂里聚合多个同类产品。应用实例: 工作了,为了参加一些聚会,肯定有两套或多套衣服吧,比如说有商务装(成套,一系列具体产品)、时尚装(成套,一系列具体产品),甚至对于一个家庭来说,可能有商务女装、商务男装、时尚女装、时尚男装,这些也都是成套的,即一系列具体产品。假设一种情况(现实中是不存在的,要不然,没法进入共产主义了,但有利于说明抽象工厂模式),在您的家中,某一个衣柜(具体工厂)只能存放某一种这样的衣服(成套,一系列具体产品),每次拿这种成套的衣服时也自然要从这个衣柜中取出了。用 OO 的思想去理解,所有的衣柜(具体工厂)都是衣柜类的(抽象工厂)某一个,而每一件成套的衣服又包括具体的上衣(某一具体产品),裤子(某一具体产品),这些具体的上衣其实也都是上衣(抽象产品),具体的裤子也都是裤子(另一个抽象产品)。优点: 当一个产品族中的多个对象被设计成一起工作时,它能保证客户端始终只使用同一个产品族中的对象。缺点: 产品族扩展非常困难,要增加一个系列的某一产品,既要在抽象的 Creator 里加代码,又要在具体的里面加代码。使用场景:QQ 换皮肤,一整套一起换。生成不同操作系统的程序。注意事项: 产品族难扩展,产品等级易扩展。单例模式单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。 这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。注意:1、单例类只能有一个实例。2、单例类必须自己创建自己的唯一实例。3、单例类必须给所有其他对象提供这一实例。介绍意图: 保证一个类仅有一个实例,并提供一个访问它的全局访问点。主要解决: 一个全局使用的类频繁地创建与销毁。何时使用: 当您想控制实例数目,节省系统资源的时候。如何解决: 判断系统是否已经有这个单例,如果有则返回,如果没有则创建。关键代码: 构造函数是私有的。应用实例:一个班级只有一个班主任。Windows 是多进程多线程的,在操作一个文件的时候,就不可避免地出现多个进程或线程同时操作一个文件的现象,所以所有文件的处理必须通过唯一的实例来进行。一些设备管理器常常设计为单例模式,比如一个电脑有两台打印机,在输出的时候就要处理不能两台打印机打印同一个文件。优点:在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例(比如管理学院首页页面缓存)。避免对资源的多重占用(比如写文件操作)。缺点: 没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。使用场景:要求生产唯一序列号。WEB 中的计数器,不用每次刷新都在数据库里加一次,用单例先缓存起来。创建的一个对象需要消耗的资源过多,比如 I/O 与数据库的连接等。注意事项:getInstance() 方法中需要使用同步锁 synchronized (Singleton.class) 防止多线程同时进入造成 instance 被多次实例化。建造者模式建造者模式(Builder Pattern)使用多个简单的对象一步一步构建成一个复杂的对象。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。 一个 Builder 类会一步一步构造最终的对象。该 Builder 类是独立于其他对象的。介绍意图: 将一个复杂的构建与其表示相分离,使得同样的构建过程可以创建不同的表示。主要解决: 主要解决在软件系统中,有时候面临着 “一个复杂对象” 的创建工作,其通常由各个部分的子对象用一定的算法构成;由于需求的变化,这个复杂对象的各个部分经常面临着剧烈的变化,但是将它们组合在一起的算法却相对稳定。何时使用: 一些基本部件不会变,而其组合经常变化的时候。如何解决: 将变与不变分离开。关键代码: 建造者:创建和提供实例,导演:管理建造出来的实例的依赖关系。应用实例:去肯德基,汉堡、可乐、薯条、炸鸡翅等是不变的,而其组合是经常变化的,生成出所谓的 “套餐”。JAVA 中的 StringBuilder。优点:建造者独立,易扩展。便于控制细节风险。缺点:产品必须有共同点,范围有限制。如内部变化复杂,会有很多的建造类。使用场景:需要生成的对象具有复杂的内部结构。需要生成的对象内部属性本身相互依赖。注意事项: 与工厂模式的区别是:建造者模式更加关注与零件装配的顺序。原型模式原型模式(Prototype Pattern)是用于创建重复的对象,同时又能保证性能。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。 这种模式是实现了一个原型接口,该接口用于创建当前对象的克隆。当直接创建对象的代价比较大时,则采用这种模式。例如,一个对象需要在一个高代价的数据库操作之后被创建。我们可以缓存该对象,在下一个请求时返回它的克隆,在需要的时候更新数据库,以此来减少数据库调用。介绍意图: 用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。主要解决: 在运行期建立和删除原型。何时使用:当一个系统应该独立于它的产品创建,构成和表示时。当要实例化的类是在运行时刻指定时,例如,通过动态装载。为了避免创建一个与产品类层次平行的工厂类层次时。当一个类的实例只能有几个不同状态组合中的一种时。建立相应数目的原型并克隆它们可能比每次用合适的状态手工实例化该类更方便一些。如何解决: 利用已有的一个原型对象,快速地生成和原型对象一样的实例。关键代码:实现克隆操作,在 JAVA 继承 Cloneable,重写 clone(),在 .NET 中可以使用 Object 类的 MemberwiseClone() 方法来实现对象的浅拷贝或通过序列化的方式来实现深拷贝。原型模式同样用于隔离类对象的使用者和具体类型(易变类)之间的耦合关系,它同样要求这些 “易变类” 拥有稳定的接口。应用实例:细胞分裂。JAVA 中的 Object clone() 方法。优点:性能提高。逃避构造函数的约束。缺点:配备克隆方法需要对类的功能进行通盘考虑,这对于全新的类不是很难,但对于已有的类不一定很容易,特别当一个类引用不支持串行化的间接对象,或者引用含有循环结构的时候。必须实现 Cloneable 接口。使用场景:资源优化场景。类初始化需要消化非常多的资源,这个资源包括数据. 硬件资源等。性能和安全要求的场景。通过 new 产生一个对象需要非常繁琐的数据准备或访问权限,则可以使用原型模式。一个对象多个修改者的场景。一个对象需要提供给其他对象访问,而且各个调用者可能都需要修改其值时,可以考虑使用原型模式拷贝多个对象供调用者使用。在实际项目中,原型模式很少单独出现,一般是和工厂方法模式一起出现,通过 clone 的方法创建一个对象,然后由工厂方法提供给调用者。原型模式已经与 Java 融为浑然一体,大家可以随手拿来使用。注意事项: 与通过对一个类进行实例化来构造新对象不同的是,原型模式是通过拷贝一个现有对象生成新对象的。浅拷贝实现 Cloneable,重写,深拷贝是通过实现 Serializable 读取二进制流。对象池模式对象池(也称为资源池)被用来管理对象缓存。对象池是一组已经初始化过且可以直接使用的对象集合,用户在使用对象时可以从对象池中获取对象,对其进行操作处理,并在不需要时归还给对象池而非销毁它。 若对象初始化、实例化的代价高,且需要经常实例化,但每次实例化的数量较少的情况下,使用对象池可以获得显著的性能提升。常见的使用对象池模式的技术包括线程池、数据库连接池、任务队列池、图片资源对象池等。 当然,如果要实例化的对象较小,不需要多少资源开销,就没有必要使用对象池模式了,这非但不会提升性能,反而浪费内存空间,甚至降低性能。示例代码Pool.php<?phpnamespace DesignPatterns\Creational\Pool;class Pool{ private $instances = array(); private $class; public function __construct($class) { $this->class = $class; } public function get() { if (count($this->instances) > 0) { return array_pop($this->instances); } return new $this->class(); } public function dispose($instance) { $this->instances[] = $instance; }}Processor.php<?phpnamespace DesignPatterns\Creational\Pool;class Processor{ private $pool; private $processing = 0; private $maxProcesses = 3; private $waitingQueue = []; public function __construct(Pool $pool) { $this->pool = $pool; } public function process($image) { if ($this->processing++ < $this->maxProcesses) { $this->createWorker($image); } else { $this->pushToWaitingQueue($image); } } private function createWorker($image) { $worker = $this->pool->get(); $worker->run($image, array($this, ‘processDone’)); } public function processDone($worker) { $this->processing–; $this->pool->dispose($worker); if (count($this->waitingQueue) > 0) { $this->createWorker($this->popFromWaitingQueue()); } } private function pushToWaitingQueue($image) { $this->waitingQueue[] = $image; } private function popFromWaitingQueue() { return array_pop($this->waitingQueue); }}Worker.php<?phpnamespace DesignPatterns\Creational\Pool;class Worker{ public function __construct() { // let’s say that constuctor does really expensive work… // for example creates “thread” } public function run($image, array $callback) { // do something with $image… // and when it’s done, execute callback call_user_func($callback, $this); }}多例模式多例模式和单例模式类似,但可以返回多个实例。比如我们有多个数据库连接,MySQL、SQLite、Postgres,又或者我们有多个日志记录器,分别用于记录调试信息和错误信息,这些都可以使用多例模式实现。示例代码Multiton.php<?phpnamespace DesignPatterns\Creational\Multiton;/ * Multiton类 /class Multiton{ / * * 第一个实例 / const INSTANCE_1 = ‘1’; / * * 第二个实例 / const INSTANCE_2 = ‘2’; / * 实例数组 * * @var array / private static $instances = array(); / * 构造函数是私有的,不能从外部进行实例化 * / private function __construct() { } / * 通过指定名称返回实例(使用到该实例的时候才会实例化) * * @param string $instanceName * * @return Multiton / public static function getInstance($instanceName) { if (!array_key_exists($instanceName, self::$instances)) { self::$instances[$instanceName] = new self(); } return self::$instances[$instanceName]; } / * 防止实例从外部被克隆 * * @return void / private function __clone() { } / * 防止实例从外部反序列化 * * @return void / private function __wakeup() { }}静态工厂模式与简单工厂类似,该模式用于创建一组相关或依赖的对象,不同之处在于静态工厂模式使用一个静态方法来创建所有类型的对象,该静态方法通常是 factory 或 build。示例代码StaticFactory.php<?phpnamespace DesignPatterns\Creational\StaticFactory;class StaticFactory{ / * 通过传入参数创建相应对象实例 * * @param string $type * * @static * * @throws \InvalidArgumentException * @return FormatterInterface / public static function factory($type) { $className = NAMESPACE . ‘\Format’ . ucfirst($type); if (!class_exists($className)) { throw new \InvalidArgumentException(‘Missing format class.’); } return new $className(); }}FormatterInterface.php<?phpnamespace DesignPatterns\Creational\StaticFactory;/ * FormatterInterface接口 /interface FormatterInterface{}FormatString.php<?phpnamespace DesignPatterns\Creational\StaticFactory;/ * FormatNumber类 /class FormatNumber implements FormatterInterface{}适配器模式适配器模式(Adapter Pattern)是作为两个不兼容的接口之间的桥梁。这种类型的设计模式属于结构型模式,它结合了两个独立接口的功能。 这种模式涉及到一个单一的类,该类负责加入独立的或不兼容的接口功能。举个真实的例子,读卡器是作为内存卡和笔记本之间的适配器。您将内存卡插入读卡器,再将读卡器插入笔记本,这样就可以通过笔记本来读取内存卡。介绍意图: 将一个类的接口转换成客户希望的另外一个接口。适配器模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。主要解决: 主要解决在软件系统中,常常要将一些 “现存的对象” 放到新的环境中,而新环境要求的接口是现对象不能满足的。何时使用:系统需要使用现有的类,而此类的接口不符合系统的需要。想要建立一个可以重复使用的类,用于与一些彼此之间没有太大关联的一些类,包括一些可能在将来引进的类一起工作,这些源类不一定有一致的接口。通过接口转换,将一个类插入另一个类系中。(比如老虎和飞禽,现在多了一个飞虎,在不增加实体的需求下,增加一个适配器,在里面包容一个虎对象,实现飞的接口。)如何解决: 继承或依赖(推荐)。关键代码: 适配器继承或依赖已有的对象,实现想要的目标接口。应用实例:美国电器 110V,中国 220V,就要有一个适配器将 110V 转化为 220V。JAVA JDK 1.1 提供了 Enumeration 接口,而在 1.2 中提供了 Iterator 接口,想要使用 1.2 的 JDK,则要将以前系统的 Enumeration 接口转化为 Iterator 接口,这时就需要适配器模式。在 LINUX 上运行 WINDOWS 程序。 4. JAVA 中的 jdbc。优点:可以让任何两个没有关联的类一起运行。提高了类的复用。增加了类的透明度。灵活性好。缺点:过多地使用适配器,会让系统非常零乱,不易整体进行把握。比如,明明看到调用的是 A 接口,其实内部被适配成了 B 接口的实现,一个系统如果太多出现这种情况,无异于一场灾难。因此如果不是很有必要,可以不使用适配器,而是直接对系统进行重构。由于 JAVA 至多继承一个类,所以至多只能适配一个适配者类,而且目标类必须是抽象类。使用场景: 有动机地修改一个正常运行的系统的接口,这时应该考虑使用适配器模式。注意事项: 适配器不是在详细设计时添加的,而是解决正在服役的项目的问题。桥接模式桥接(Bridge)是用于把抽象化与实现化解耦,使得二者可以独立变化。这种类型的设计模式属于结构型模式,它通过提供抽象化和实现化之间的桥接结构,来实现二者的解耦。 这种模式涉及到一个作为桥接的接口,使得实体类的功能独立于接口实现类。这两种类型的类可被结构化改变而互不影响。介绍意图: 将抽象部分与实现部分分离,使它们都可以独立的变化。主要解决: 在有多种可能会变化的情况下,用继承会造成类爆炸问题,扩展起来不灵活。何时使用: 实现系统可能有多个角度分类,每一种角度都可能变化。如何解决: 把这种多角度分类分离出来,让它们独立变化,减少它们之间耦合。关键代码: 抽象类依赖实现类。应用实例:猪八戒从天蓬元帅转世投胎到猪,转世投胎的机制将尘世划分为两个等级,即:灵魂和肉体,前者相当于抽象化,后者相当于实现化。生灵通过功能的委派,调用肉体对象的功能,使得生灵可以动态地选择。墙上的开关,可以看到的开关是抽象的,不用管里面具体怎么实现的。优点:抽象和实现的分离。优秀的扩展能力。实现细节对客户透明。缺点: 桥接模式的引入会增加系统的理解与设计难度,由于聚合关联关系建立在抽象层,要求开发者针对抽象进行设计与编程。使用场景:如果一个系统需要在构件的抽象化角色和具体化角色之间增加更多的灵活性,避免在两个层次之间建立静态的继承联系,通过桥接模式可以使它们在抽象层建立一个关联关系。对于那些不希望使用继承或因为多层次继承导致系统类的个数急剧增加的系统,桥接模式尤为适用。一个类存在两个独立变化的维度,且这两个维度都需要进行扩展。注意事项: 对于两个独立变化的维度,使用桥接模式再适合不过了。过滤器模式过滤器模式(Filter Pattern)或标准模式(Criteria Pattern)是一种设计模式,这种模式允许开发人员使用不同的标准来过滤一组对象,通过逻辑运算以解耦的方式把它们连接起来。这种类型的设计模式属于结构型模式,它结合多个标准来获得单一标准。组合模式组合模式(Composite Pattern),又叫部分整体模式,是用于把一组相似的对象当作一个单一的对象。组合模式依据树形结构来组合对象,用来表示部分以及整体层次。这种类型的设计模式属于结构型模式,它创建了对象组的树形结构。 这种模式创建了一个包含自己对象组的类。该类提供了修改相同对象组的方式。介绍意图: 将对象组合成树形结构以表示 “部分 - 整体” 的层次结构。组合模式使得用户对单个对象和组合对象的使用具有一致性。主要解决: 它在我们树型结构的问题中,模糊了简单元素和复杂元素的概念,客户程序可以向处理简单元素一样来处理复杂元素,从而使得客户程序与复杂元素的内部结构解耦。何时使用:您想表示对象的部分 - 整体层次结构(树形结构)。您希望用户忽略组合对象与单个对象的不同,用户将统一地使用组合结构中的所有对象。如何解决: 树枝和叶子实现统一接口,树枝内部组合该接口。关键代码: 树枝内部组合该接口,并且含有内部属性 List,里面放 Component。应用实例:算术表达式包括操作数. 操作符和另一个操作数,其中,另一个操作符也可以是操作数. 操作符和另一个操作数。在 JAVA AWT 和 SWING 中,对于 Button 和 Checkbox 是树叶,Container 是树枝。优点:高层模块调用简单。节点自由增加。缺点: 在使用组合模式时,其叶子和树枝的声明都是实现类,而不是接口,违反了依赖倒置原则。使用场景: 部分. 整体场景,如树形菜单,文件. 文件夹的管理。注意事项: 定义时为具体类。装饰器模式装饰器模式(Decorator Pattern)允许向一个现有的对象添加新的功能,同时又不改变其结构。这种类型的设计模式属于结构型模式,它是作为现有的类的一个包装。 这种模式创建了一个装饰类,用来包装原有的类,并在保持类方法签名完整性的前提下,提供了额外的功能。介绍意图: 动态地给一个对象添加一些额外的职责。就增加功能来说,装饰器模式相比生成子类更为灵活。主要解决: 一般的,我们为了扩展一个类经常使用继承方式实现,由于继承为类引入静态特征,并且随着扩展功能的增多,子类会很膨胀。何时使用: 在不想增加很多子类的情况下扩展类。如何解决: 将具体功能职责划分,同时继承装饰者模式。关键代码:Component 类充当抽象角色,不应该具体实现。修饰类引用和继承 Component 类,具体扩展类重写父类方法。应用实例:孙悟空有 72 变,当他变成 “庙宇” 后,他的根本还是一只猴子,但是他又有了庙宇的功能。不论一幅画有没有画框都可以挂在墙上,但是通常都是有画框的,并且实际上是画框被挂在墙上。在挂在墙上之前,画可以被蒙上玻璃,装到框子里;这时画、玻璃和画框形成了一个物体。优点: 装饰类和被装饰类可以独立发展,不会相互耦合,装饰模式是继承的一个替代模式,装饰模式可以动态扩展一个实现类的功能。缺点: 多层装饰比较复杂。使用场景:扩展一个类的功能。动态增加功能,动态撤销。注意事项: 可代替继承。外观模式外观模式(Facade Pattern)隐藏系统的复杂性,并向客户端提供了一个客户端可以访问系统的接口。这种类型的设计模式属于结构型模式,它向现有的系统添加一个接口,来隐藏系统的复杂性。 这种模式涉及到一个单一的类,该类提供了客户端请求的简化方法和对现有系统类方法的委托调用。介绍意图: 为子系统中的一组接口提供一个一致的界面,外观模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。主要解决: 降低访问复杂系统的内部子系统时的复杂度,简化客户端与之的接口。何时使用:客户端不需要知道系统内部的复杂联系,整个系统只需提供一个 “接待员” 即可。定义系统的入口。如何解决: 客户端不与系统耦合,外观类与系统耦合。关键代码: 在客户端和复杂系统之间再加一层,这一层将调用顺序. 依赖关系等处理好。应用实例:去医院看病,可能要去挂号、门诊、划价、取药,让患者或患者家属觉得很复杂,如果有提供接待人员,只让接待人员来处理,就很方便。JAVA 的三层开发模式。优点:减少系统相互依赖。提高灵活性。提高了安全性。缺点: 不符合开闭原则,如果要改东西很麻烦,继承重写都不合适。使用场景:为复杂的模块或子系统提供外界访问的模块。子系统相对独立。预防低水平人员带来的风险。注意事项: 在层次化结构中,可以使用外观模式定义系统中每一层的入口。享元模式享元模式(Flyweight Pattern)主要用于减少创建对象的数量,以减少内存占用和提高性能。这种类型的设计模式属于结构型模式,它提供了减少对象数量从而改善应用所需的对象结构的方式。 享元模式尝试重用现有的同类对象,如果未找到匹配的对象,则创建新对象。我们将通过创建 5 个对象来画出 20 个分布于不同位置的圆来演示这种模式。由于只有 5 种可用的颜色,所以 color 属性被用来检查现有的 Circle 对象。介绍意图: 运用共享技术有效地支持大量细粒度的对象。主要解决: 在有大量对象时,有可能会造成内存溢出,我们把其中共同的部分抽象出来,如果有相同的业务请求,直接返回在内存中已有的对象,避免重新创建。何时使用:系统中有大量对象。这些对象消耗大量内存。这些对象的状态大部分可以外部化。这些对象可以按照内蕴状态分为很多组,当把外蕴对象从对象中剔除出来时,每一组对象都可以用一个对象来代替。系统不依赖于这些对象身份,这些对象是不可分辨的。如何解决: 用唯一标识码判断,如果在内存中有,则返回这个唯一标识码所标识的对象。关键代码: 用 HashMap 存储这些对象。应用实例:JAVA 中的 String,如果有则返回,如果没有则创建一个字符串保存在字符串缓存池里面。2. 数据库的数据池。优点: 大大减少对象的创建,降低系统的内存,使效率提高。缺点: 提高了系统的复杂度,需要分离出外部状态和内部状态,而且外部状态具有固有化的性质,不应该随着内部状态的变化而变化,否则会造成系统的混乱。使用场景:系统有大量相似对象。需要缓冲池的场景。注意事项:注意划分外部状态和内部状态,否则可能会引起线程安全问题。这些类必须有一个工厂对象加以控制。代理模式在代理模式(Proxy Pattern)中,一个类代表另一个类的功能。这种类型的设计模式属于结构型模式。 在代理模式中,我们创建具有现有对象的对象,以便向外界提供功能接口。介绍意图: 为其他对象提供一种代理以控制对这个对象的访问。主要解决: 在直接访问对象时带来的问题,比如说:要访问的对象在远程的机器上。在面向对象系统中,有些对象由于某些原因(比如对象创建开销很大,或者某些操作需要安全控制,或者需要进程外的访问),直接访问会给使用者或者系统结构带来很多麻烦,我们可以在访问此对象时加上一个对此对象的访问层。何时使用: 想在访问一个类时做一些控制。如何解决: 增加中间层。关键代码: 实现与被代理类组合。应用实例:Windows 里面的快捷方式。猪八戒去找高翠兰结果是孙悟空变的,可以这样理解:把高翠兰的外貌抽象出来,高翠兰本人和孙悟空都实现了这个接口,猪八戒访问高翠兰的时候看不出来这个是孙悟空,所以说孙悟空是高翠兰代理类。买火车票不一定在火车站买,也可以去代售点。一张支票或银行存单是账户中资金的代理。支票在市场交易中用来代替现金,并提供对签发人账号上资金的控制。spring aop。优点:职责清晰。高扩展性。智能化。缺点:由于在客户端和真实主题之间增加了代理对象,因此有些类型的代理模式可能会造成请求的处理速度变慢。实现代理模式需要额外的工作,有些代理模式的实现非常复杂。使用场景: 按职责来划分,通常有以下使用场景:远程代理。虚拟代理。Copy-on-Write 代理。保护(Protect or Access)代理。Cache 代理。防火墙(Firewall)代理。同步化(Synchronization)代理。智能引用(Smart Reference)代理。注意事项:和适配器模式的区别:适配器模式主要改变所考虑对象的接口,而代理模式不能改变所代理类的接口。和装饰器模式的区别:装饰器模式为了增强功能,而代理模式是为了加以控制。数据映射模式在了解数据映射模式之前,先了解下数据映射,它是在持久化数据存储层(通常是关系型数据库)和驻于内存的数据表现层之间进行双向数据传输的数据访问层。 数据映射模式的目的是让持久化数据存储层、驻于内存的数据表现层、以及数据映射本身三者相互独立、互不依赖。这个数据访问层由一个或多个映射器(或者数据访问对象)组成,用于实现数据传输。通用的数据访问层可以处理不同的实体类型,而专用的则处理一个或几个。 数据映射模式的核心在于它的数据模型遵循单一职责原则(Single Responsibility Principle), 这也是和 Active Record 模式的不同之处。最典型的数据映射模式例子就是数据库 ORM 模型 (Object Relational Mapper)。 准确来说该模式是个架构模式。依赖注入模式依赖注入(Dependency Injection)是控制反转(Inversion of Control)的一种实现方式。 我们先来看看什么是控制反转。 当调用者需要被调用者的协助时,在传统的程序设计过程中,通常由调用者来创建被调用者的实例,但在这里,创建被调用者的工作不再由调用者来完成,而是将被调用者的创建移到调用者的外部,从而反转被调用者的创建,消除了调用者对被调用者创建的控制,因此称为控制反转。 要实现控制反转,通常的解决方案是将创建被调用者实例的工作交由 IoC 容器来完成,然后在调用者中注入被调用者(通过构造器/方法注入实现),这样我们就实现了调用者与被调用者的解耦,该过程被称为依赖注入。 依赖注入不是目的,它是一系列工具和手段,最终的目的是帮助我们开发出松散耦合(loose coupled)、可维护、可测试的代码和程序。这条原则的做法是大家熟知的面向接口,或者说是面向抽象编程。门面模式门面模式(Facade)又称外观模式,用于为子系统中的一组接口提供一个一致的界面。门面模式定义了一个高层接口,这个接口使得子系统更加容易使用:引入门面角色之后,用户只需要直接与门面角色交互,用户与子系统之间的复杂关系由门面角色来实现,从而降低了系统的耦合度。示例代码Facade.php<?phpnamespace DesignPatterns\Structural\Facade;/* * 门面类 /class Facade{ /* * @var OsInterface / protected $os; /* * @var BiosInterface / protected $bios; /* * This is the perfect time to use a dependency injection container * to create an instance of this class * * @param BiosInterface $bios * @param OsInterface $os / public function __construct(BiosInterface $bios, OsInterface $os) { $this->bios = $bios; $this->os = $os; } /* * turn on the system / public function turnOn() { $this->bios->execute(); $this->bios->waitForKeyPress(); $this->bios->launch($this->os); } /* * turn off the system / public function turnOff() { $this->os->halt(); $this->bios->powerDown(); }}OsInterface.php<?phpnamespace DesignPatterns\Structural\Facade;/* * OsInterface接口 /interface OsInterface{ /* * halt the OS / public function halt();}BiosInterface.php<?phpnamespace DesignPatterns\Structural\Facade;/* * BiosInterface接口 /interface BiosInterface{ /* * execute the BIOS / public function execute(); /* * wait for halt / public function waitForKeyPress(); /* * launches the OS * * @param OsInterface $os / public function launch(OsInterface $os); /* * power down BIOS / public function powerDown();}流接口模式在软件工程中,流接口(Fluent Interface)是指实现一种面向对象的、能提高代码可读性的 API 的方法,其目的就是可以编写具有自然语言一样可读性的代码,我们对这种代码编写方式还有一个通俗的称呼 —— 方法链。 Laravel 中流接口模式有着广泛使用,比如查询构建器,邮件等等。示例代码Sql.php<?phpnamespace DesignPatterns\Structural\FluentInterface;/* * SQL 类 /class Sql{ /* * @var array / protected $fields = array(); /* * @var array / protected $from = array(); /* * @var array / protected $where = array(); /* * 添加 select 字段 * * @param array $fields * * @return SQL / public function select(array $fields = array()) { $this->fields = $fields; return $this; } /* * 添加 FROM 子句 * * @param string $table * @param string $alias * * @return SQL / public function from($table, $alias) { $this->from[] = $table . ’ AS ’ . $alias; return $this; } /* * 添加 WHERE 条件 * * @param string $condition * * @return SQL / public function where($condition) { $this->where[] = $condition; return $this; } /* * 生成查询语句 * * @return string / public function getQuery() { return ‘SELECT ’ . implode(’,’, $this->fields) . ’ FROM ’ . implode(’,’, $this->from) . ’ WHERE ’ . implode(’ AND ‘, $this->where); }}注册模式注册模式(Registry)也叫做注册树模式,注册器模式。注册模式为应用中经常使用的对象创建一个中央存储器来存放这些对象 —— 通常通过一个只包含静态方法的抽象类来实现(或者通过单例模式)。示例代码Registry.php<?phpnamespace DesignPatterns\Structural\Registry;/* * class Registry /abstract class Registry{ const LOGGER = ’logger’; /* * @var array / protected static $storedValues = array(); /* * sets a value * * @param string $key * @param mixed $value * * @static * @return void / public static function set($key, $value) { self::$storedValues[$key] = $value; } /* * gets a value from the registry * * @param string $key * * @static * @return mixed / public static function get($key) { return self::$storedValues[$key]; } // typically there would be methods to check if a key has already been registered and so on …}责任链模式顾名思义,责任链模式(Chain of Responsibility Pattern)为请求创建了一个接收者对象的链。这种模式给予请求的类型,对请求的发送者和接收者进行解耦。这种类型的设计模式属于行为型模式。 在这种模式中,通常每个接收者都包含对另一个接收者的引用。如果一个对象不能处理该请求,那么它会把相同的请求传给下一个接收者,依此类推。介绍意图: 避免请求发送者与接收者耦合在一起,让多个对象都有可能接收请求,将这些对象连接成一条链,并且沿着这条链传递请求,直到有对象处理它为止。主要解决: 职责链上的处理者负责处理请求,客户只需要将请求发送到职责链上即可,无须关心请求的处理细节和请求的传递,所以职责链将请求的发送者和请求的处理者解耦了。何时使用: 在处理消息的时候以过滤很多道。如何解决: 拦截的类都实现统一接口。关键代码: Handler 里面聚合它自己,在 HandlerRequest 里判断是否合适,如果没达到条件则向下传递,向谁传递之前 set 进去。应用实例:红楼梦中的 “击鼓传花”。JS 中的事件冒泡。JAVA WEB 中 Apache Tomcat 对 Encoding 的处理,Struts2 的拦截器,jsp servlet 的 Filter。优点:降低耦合度。它将请求的发送者和接收者解耦。简化了对象。使得对象不需要知道链的结构。增强给对象指派职责的灵活性。通过改变链内的成员或者调动它们的次序,允许动态地新增或者删除责任。增加新的请求处理类很方便。缺点:不能保证请求一定被接收。系统性能将受到一定影响,而且在进行代码调试时不太方便,可能会造成循环调用。可能不容易观察运行时的特征,有碍于除错。使用场景:有多个对象可以处理同一个请求,具体哪个对象处理该请求由运行时刻自动确定。在不明确指定接收者的情况下,向多个对象中的一个提交一个请求。可动态指定一组对象处理请求。注意事项: 在 JAVA WEB 中遇到很多应用。命令模式命令模式(Command Pattern)是一种数据驱动的设计模式,它属于行为型模式。请求以命令的形式包裹在对象中,并传给调用对象。调用对象寻找可以处理该命令的合适的对象,并把该命令传给相应的对象,该对象执行命令。介绍意图: 将一个请求封装成一个对象,从而使您可以用不同的请求对客户进行参数化。主要解决: 在软件系统中,行为请求者与行为实现者通常是一种紧耦合的关系,但某些场合,比如需要对行为进行记录、撤销或重做、事务等处理时,这种无法抵御变化的紧耦合的设计就不太合适。何时使用: 在某些场合,比如要对行为进行 “记录、撤销 / 重做、事务” 等处理,这种无法抵御变化的紧耦合是不合适的。在这种情况下,如何将 “行为请求者” 与 “行为实现者” 解耦?将一组行为抽象为对象,可以实现二者之间的松耦合。如何解决: 通过调用者调用接受者执行命令,顺序:调用者→接受者→命令。关键代码: 定义三个角色:received 真正的命令执行对象Commandinvoker 使用命令对象的入口应用实例: struts 1 中的 action 核心控制器 ActionServlet 只有一个,相当于 Invoker,而模型层的类会随着不同的应用有不同的模型类,相当于具体的 Command。优点:降低了系统耦合度。新的命令可以很容易添加到系统中去。缺点: 使用命令模式可能会导致某些系统有过多的具体命令类。使用场景: 认为是命令的地方都可以使用命令模式,比如:GUI 中每一个按钮都是一条命令。模拟 CMD。注意事项: 系统需要支持命令的撤销 (Undo) 操作和恢复 (Redo) 操作,也可以考虑使用命令模式,见命令模式的扩展。解释器模式解释器模式(Interpreter Pattern)提供了评估语言的语法或表达式的方式,它属于行为型模式。这种模式实现了一个表达式接口,该接口解释一个特定的上下文。这种模式被用在 SQL 解析、符号处理引擎等。介绍意图: 给定一个语言,定义它的文法表示,并定义一个解释器,这个解释器使用该标识来解释语言中的句子。主要解决: 对于一些固定文法构建一个解释句子的解释器。何时使用: 如果一种特定类型的问题发生的频率足够高,那么可能就值得将该问题的各个实例表述为一个简单语言中的句子。这样就可以构建一个解释器,该解释器通过解释这些句子来解决该问题。如何解决: 构建语法树,定义终结符与非终结符。关键代码: 构建环境类,包含解释器之外的一些全局信息,一般是 HashMap。应用实例: 编译器、运算表达式计算。优点:可扩展性比较好,灵活。增加了新的解释表达式的方式。易于实现简单文法。缺点:可利用场景比较少。对于复杂的文法比较难维护。解释器模式会引起类膨胀。解释器模式采用递归调用方法。使用场景:可以将一个需要解释执行的语言中的句子表示为一个抽象语法树。一些重复出现的问题可以用一种简单的语言来进行表达。一个简单语法需要解释的场景。注意事项: 可利用场景比较少,JAVA 中如果碰到可以用 expression4J 代替。迭代器模式迭代器模式(Iterator Pattern)是 Java 和 .Net 编程环境中非常常用的设计模式。这种模式用于顺序访问集合对象的元素,不需要知道集合对象的底层表示。 迭代器模式属于行为型模式。介绍意图: 提供一种方法顺序访问一个聚合对象中各个元素, 而又无须暴露该对象的内部表示。主要解决: 不同的方式来遍历整个整合对象。何时使用: 遍历一个聚合对象。如何解决: 把在元素之间游走的责任交给迭代器,而不是聚合对象。关键代码: 定义接口:hasNext, next。应用实例: JAVA 中的 iterator。优点:它支持以不同的方式遍历一个聚合对象。迭代器简化了聚合类。在同一个聚合上可以有多个遍历。在迭代器模式中,增加新的聚合类和迭代器类都很方便,无须修改原有代码。缺点: 由于迭代器模式将存储数据和遍历数据的职责分离,增加新的聚合类需要对应增加新的迭代器类,类的个数成对增加,这在一定程度上增加了系统的复杂性。使用场景:访问一个聚合对象的内容而无须暴露它的内部表示。需要为聚合对象提供多种遍历方式。为遍历不同的聚合结构提供一个统一的接口。注意事项: 迭代器模式就是分离了集合对象的遍历行为,抽象出一个迭代器类来负责,这样既可以做到不暴露集合的内部结构,又可让外部代码透明地访问集合内部的数据。中介者模式中介者模式(Mediator Pattern)是用来降低多个对象和类之间的通信复杂性。这种模式提供了一个中介类,该类通常处理不同类之间的通信,并支持松耦合,使代码易于维护。中介者模式属于行为型模式。介绍意图: 用一个中介对象来封装一系列的对象交互,中介者使各对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。主要解决: 对象与对象之间存在大量的关联关系,这样势必会导致系统的结构变得很复杂,同时若一个对象发生改变,我们也需要跟踪与之相关联的对象,同时做出相应的处理。何时使用: 多个类相互耦合,形成了网状结构。如何解决: 将上述网状结构分离为星型结构。关键代码: 对象 Colleague 之间的通信封装到一个类中单独处理。应用实例:中国加入 WTO 之前是各个国家相互贸易,结构复杂,现在是各个国家通过 WTO 来互相贸易。机场调度系统。MVC 框架,其中 C(控制器)就是 M(模型)和 V(视图)的中介者。优点:降低了类的复杂度,将一对多转化成了一对一。各个类之间的解耦。符合迪米特原则。缺点: 中介者会庞大,变得复杂难以维护。使用场景:系统中对象之间存在比较复杂的引用关系,导致它们之间的依赖关系结构混乱而且难以复用该对象。想通过一个中间类来封装多个类中的行为,而又不想生成太多的子类。注意事项: 不应当在职责混乱的时候使用。备忘录模式备忘录模式(Memento Pattern)保存一个对象的某个状态,以便在适当的时候恢复对象。备忘录模式属于行为型模式。介绍意图: 在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。主要解决: 所谓备忘录模式就是在不破坏封装的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,这样可以在以后将对象恢复到原先保存的状态。何时使用: 很多时候我们总是需要记录一个对象的内部状态,这样做的目的就是为了允许用户取消不确定或者错误的操作,能够恢复到他原先的状态,使得他有 “后悔药” 可吃。如何解决: 通过一个备忘录类专门存储对象状态。关键代码: 客户不与备忘录类耦合,与备忘录管理类耦合。应用实例:后悔药。打游戏时的存档。Windows 里的 ctri + z。IE 中的后退。数据库的事务管理。优点:给用户提供了一种可以恢复状态的机制,可以使用户能够比较方便地回到某个历史的状态。实现了信息的封装,使得用户不需要关心状态的保存细节。缺点: 消耗资源。如果类的成员变量过多,势必会占用比较大的资源,而且每一次保存都会消耗一定的内存。使用场景:需要保存 / 恢复数据的相关状态场景。提供一个可回滚的操作。注意事项:为了符合迪米特原则,还要增加一个管理备忘录的类。为了节约内存,可使用原型模式 + 备忘录模式。观察者模式当对象间存在一对多关系时,则使用观察者模式(Observer Pattern)。比如,当一个对象被修改时,则会自动通知它的依赖对象。观察者模式属于行为型模式。介绍意图: 定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。主要解决: 一个对象状态改变给其他对象通知的问题,而且要考虑到易用和低耦合,保证高度的协作。何时使用: 一个对象(目标对象)的状态发生改变,所有的依赖对象(观察者对象)都将得到通知,进行广播通知。如何解决: 使用面向对象技术,可以将这种依赖关系弱化。关键代码: 在抽象类里有一个 ArrayList 存放观察者们。应用实例:拍卖的时候,拍卖师观察最高标价,然后通知给其他竞价者竞价。西游记里面悟空请求菩萨降服红孩儿,菩萨洒了一地水招来一个老乌龟,这个乌龟就是观察者,他观察菩萨洒水这个动作。优点:观察者和被观察者是抽象耦合的。建立一套触发机制。缺点:如果一个被观察者对象有很多的直接和间接的观察者的话,将所有的观察者都通知到会花费很多时间。如果在观察者和观察目标之间有循环依赖的话,观察目标会触发它们之间进行循环调用,可能导致系统崩溃。观察者模式没有相应的机制让观察者知道所观察的目标对象是怎么发生变化的,而仅仅只是知道观察目标发生了变化。使用场景:一个抽象模型有两个方面,其中一个方面依赖于另一个方面。将这些方面封装在独立的对象中使它们可以各自独立地改变和复用。一个对象的改变将导致其他一个或多个对象也发生改变,而不知道具体有多少对象将发生改变,可以降低对象之间的耦合度。一个对象必须通知其他对象,而并不知道这些对象是谁。需要在系统中创建一个触发链,A 对象的行为将影响 B 对象,B 对象的行为将影响 C 对象……,可以使用观察者模式创建一种链式触发机制。注意事项:JAVA 中已经有了对观察者模式的支持类。避免循环引用。如果顺序执行,某一观察者错误会导致系统卡壳,一般采用异步方式。状态模式在状态模式(State Pattern)中,类的行为是基于它的状态改变的。这种类型的设计模式属于行为型模式。 在状态模式中,我们创建表示各种状态的对象和一个行为随着状态对象改变而改变的 context 对象。介绍意图: 允许对象在内部状态发生改变时改变它的行为,对象看起来好像修改了它的类。主要解决: 对象的行为依赖于它的状态(属性),并且可以根据它的状态改变而改变它的相关行为。何时使用: 代码中包含大量与对象状态有关的条件语句。如何解决: 将各种具体的状态类抽象出来。关键代码: 通常命令模式的接口中只有一个方法。而状态模式的接口中有一个或者多个方法。而且,状态模式的实现类的方法,一般返回值,或者是改变实例变量的值。也就是说,状态模式一般和对象的状态有关。实现类的方法有不同的功能,覆盖接口中的方法。状态模式和命令模式一样,也可以用于消除 if…else 等条件选择语句。应用实例:打篮球的时候运动员可以有正常状态. 不正常状态和超常状态。曾侯乙编钟中,‘钟是抽象接口’,‘钟 A’等是具体状态,‘曾侯乙编钟’是具体环境(Context)。优点:封装了转换规则。枚举可能的状态,在枚举状态之前需要确定状态种类。将所有与某个状态有关的行为放到一个类中,并且可以方便地增加新的状态,只需要改变对象状态即可改变对象的行为。允许状态转换逻辑与状态对象合成一体,而不是某一个巨大的条件语句块。可以让多个环境对象共享一个状态对象,从而减少系统中对象的个数。缺点:状态模式的使用必然会增加系统类和对象的个数。状态模式的结构与实现都较为复杂,如果使用不当将导致程序结构和代码的混乱。状态模式对 “开闭原则” 的支持并不太好,对于可以切换状态的状态模式,增加新的状态类需要修改那些负责状态转换的源代码,否则无法切换到新增状态,而且修改某个状态类的行为也需修改对应类的源代码。使用场景:行为随状态改变而改变的场景。条件、分支语句的代替者。注意事项: 在行为受状态约束的时候使用状态模式,而且状态不超过 5 个。空对象模式在空对象模式(Null Object Pattern)中,一个空对象取代 NULL 对象实例的检查。Null 对象不是检查空值,而是反应一个不做任何动作的关系。这样的 Null 对象也可以在数据不可用的时候提供默认的行为。 在空对象模式中,我们创建一个指定各种要执行的操作的抽象类和扩展该类的实体类,还创建一个未对该类做任何实现的空对象类,该空对象类将无缝地使用在需要检查空值的地方。策略模式在策略模式(Strategy Pattern)中,一个类的行为或其算法可以在运行时更改。这种类型的设计模式属于行为型模式。 在策略模式中,我们创建表示各种策略的对象和一个行为随着策略对象改变而改变的 context 对象。策略对象改变 context 对象的执行算法。介绍意图: 定义一系列的算法, 把它们一个个封装起来, 并且使它们可相互替换。主要解决: 在有多种算法相似的情况下,使用 if…else 所带来的复杂和难以维护。何时使用: 一个系统有许多许多类,而区分它们的只是他们直接的行为。如何解决: 将这些算法封装成一个一个的类,任意地替换。关键代码: 实现同一个接口。应用实例:诸葛亮的锦囊妙计,每一个锦囊就是一个策略。旅行的出游方式,选择骑自行车. 坐汽车,每一种旅行方式都是一个策略。JAVA AWT 中的 LayoutManager。优点:算法可以自由切换。避免使用多重条件判断。扩展性良好。缺点:策略类会增多。所有策略类都需要对外暴露。使用场景:如果在一个系统里面有许多类,它们之间的区别仅在于它们的行为,那么使用策略模式可以动态地让一个对象在许多行为中选择一种行为。一个系统需要动态地在几种算法中选择一种。如果一个对象有很多的行为,如果不用恰当的模式,这些行为就只好使用多重的条件选择语句来实现。注意事项: 如果一个系统的策略多于四个,就需要考虑使用混合模式,解决策略类膨胀的问题。模板模式在模板模式(Template Pattern)中,一个抽象类公开定义了执行它的方法的方式 / 模板。它的子类可以按需要重写方法实现,但调用将以抽象类中定义的方式进行。这种类型的设计模式属于行为型模式。介绍意图: 定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。主要解决: 一些方法通用,却在每一个子类都重新写了这一方法。何时使用: 有一些通用的方法。如何解决: 将这些通用算法抽象出来。关键代码: 在抽象类实现,其他步骤在子类实现。应用实例:在造房子的时候,地基、走线、水管都一样,只有在建筑的后期才有加壁橱加栅栏等差异。 2. 西游记里面菩萨定好的 81 难,这就是一个顶层的逻辑骨架。spring 中对 Hibernate 的支持,将一些已经定好的方法封装起来,比如开启事务. 获取 Session. 关闭 Session 等,程序员不重复写那些已经规范好的代码,直接丢一个实体就可以保存。优点:封装不变部分,扩展可变部分。提取公共代码,便于维护。行为由父类控制,子类实现。缺点: 每一个不同的实现都需要一个子类来实现,导致类的个数增加,使得系统更加庞大。使用场景:有多个子类共有的方法,且逻辑相同。重要的、复杂的方法,可以考虑作为模板方法。注意事项: 为防止恶意操作,一般模板方法都加上 final 关键词。访问者模式在访问者模式(Visitor Pattern)中,我们使用了一个访问者类,它改变了元素类的执行算法。通过这种方式,元素的执行算法可以随着访问者改变而改变。这种类型的设计模式属于行为型模式。根据模式,元素对象已接受访问者对象,这样访问者对象就可以处理元素对象上的操作。介绍意图: 主要将数据结构与数据操作分离。主要解决: 稳定的数据结构和易变的操作耦合问题。何时使用: 需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而需要避免让这些操作 “污染” 这些对象的类,使用访问者模式将这些封装到类中。如何解决: 在被访问的类里面加一个对外提供接待访问者的接口。关键代码: 在数据基础类里面有一个方法接受访问者,将自身引用传入访问者。应用实例: 您在朋友家做客,您是访问者,朋友接受您的访问,您通过朋友的描述,然后对朋友的描述做出一个判断,这就是访问者模式。优点:符合单一职责原则。优秀的扩展性。灵活性。缺点:具体元素对访问者公布细节,违反了迪米特原则。具体元素变更比较困难。违反了依赖倒置原则,依赖了具体类,没有依赖抽象。使用场景:对象结构中对象对应的类很少改变,但经常需要在此对象结构上定义新的操作。需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而需要避免让这些操作 “污染” 这些对象的类,也不希望在增加新操作时修改这些类。注意事项: 访问者可以对功能进行统一,可以做报表、UI、拦截器与过滤器。规格模式规格模式(Specification)可以认为是组合模式的一种扩展。有时项目中某些条件决定了业务逻辑,这些条件就可以抽离出来以某种关系(与、或、非)进行组合,从而灵活地对业务逻辑进行定制。另外,在查询、过滤等应用场合中,通过预定义多个条件,然后使用这些条件的组合来处理查询或过滤,而不是使用逻辑判断语句来处理,可以简化整个实现逻辑。 这里的每个条件就是一个规格,多个规格/条件通过串联的方式以某种逻辑关系形成一个组合式的规格。访问者模式我们去银行柜台办业务,一般情况下会开几个个人业务柜台的,你去其中任何一个柜台办理都是可以的。我们的访问者模式可以很好付诸在这个场景中:对于银行柜台来说,他们是不用变化的,就是说今天和明天提供个人业务的柜台是不需要有变化的。而我们作为访问者,今天来银行可能是取消费流水,明天来银行可能是去办理手机银行业务,这些是我们访问者的操作,一直是在变化的。 访问者模式就是表示一个作用于某对象结构中的各元素的操作。它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。MVC模式MVC 模式代表 Model-View-Controller(模型 - 视图 - 控制器) 模式。这种模式用于应用程序的分层开发。Model(模型) - 模型代表一个存取数据的对象或 JAVA POJO。它也可以带有逻辑,在数据变化时更新控制器。View(视图) - 视图代表模型包含的数据的可视化。Controller(控制器) - 控制器作用于模型和视图上。它控制数据流向模型对象,并在数据变化时更新视图。它使视图与模型分离开。业务代表模式业务代表模式(Business Delegate Pattern)用于对表示层和业务层解耦。它基本上是用来减少通信或对表示层代码中的业务层代码的远程查询功能。在业务层中我们有以下实体。客户端(Client) - 表示层代码可以是 JSP、servlet 或 UI java 代码。业务代表(Business Delegate) - 一个为客户端实体提供的入口类,它提供了对业务服务方法的访问。查询服务(LookUp Service) - 查找服务对象负责获取相关的业务实现,并提供业务对象对业务代表对象的访问。业务服务(Business Service) - 业务服务接口。实现了该业务服务的实体类,提供了实际的业务实现逻辑。组合实体模式组合实体模式(Composite Entity Pattern)用在 EJB 持久化机制中。一个组合实体是一个 EJB 实体 bean,代表了对象的图解。当更新一个组合实体时,内部依赖对象 beans 会自动更新,因为它们是由 EJB 实体 bean 管理的。以下是组合实体 bean 的参与者。组合实体(Composite Entity) - 它是主要的实体 bean。它可以是粗粒的,或者可以包含一个粗粒度对象,用于持续生命周期。粗粒度对象(Coarse-Grained Object) - 该对象包含依赖对象。它有自己的生命周期,也能管理依赖对象的生命周期。依赖对象(Dependent Object) - 依赖对象是一个持续生命周期依赖于粗粒度对象的对象。策略(Strategies) - 策略表示如何实现组合实体。数据访问对象模式数据访问对象模式(Data Access Object Pattern)或 DAO 模式用于把低级的数据访问 API 或操作从高级的业务服务中分离出来。以下是数据访问对象模式的参与者。数据访问对象接口(Data Access Object Interface) - 该接口定义了在一个模型对象上要执行的标准操作。数据访问对象实体类(Data Access Object concrete class) - 该类实现了上述的接口。该类负责从数据源获取数据,数据源可以是数据库,也可以是 xml,或者是其他的存储机制。模型对象 / 数值对象(Model Object/Value Object) - 该对象是简单的 POJO,包含了 get/set 方法来存储通过使用 DAO 类检索到的数据。前端控制器模式前端控制器模式(Front Controller Pattern)是用来提供一个集中的请求处理机制,所有的请求都将由一个单一的处理程序处理。该处理程序可以做认证 / 授权 / 记录日志,或者跟踪请求,然后把请求传给相应的处理程序。以下是这种设计模式的实体。前端控制器(Front Controller) - 处理应用程序所有类型请求的单个处理程序,应用程序可以是基于 web 的应用程序,也可以是基于桌面的应用程序。调度器(Dispatcher) - 前端控制器可能使用一个调度器对象来调度请求到相应的具体处理程序。视图(View) - 视图是为请求而创建的对象。拦截过滤器模式拦截过滤器模式(Intercepting Filter Pattern)用于对应用程序的请求或响应做一些预处理 / 后处理。定义过滤器,并在把请求传给实际目标应用程序之前应用在请求上。过滤器可以做认证 / 授权 / 记录日志,或者跟踪请求,然后把请求传给相应的处理程序。以下是这种设计模式的实体。过滤器(Filter) - 过滤器在请求处理程序执行请求之前或之后,执行某些任务。过滤器链(Filter Chain) - 过滤器链带有多个过滤器,并在 Target 上按照定义的顺序执行这些过滤器。Target - Target 对象是请求处理程序。过滤管理器(Filter Manager) - 过滤管理器管理过滤器和过滤器链。客户端(Client) - Client 是向 Target 对象发送请求的对象。服务定位器模式服务定位器模式(Service Locator Pattern)用在我们想使用 JNDI 查询定位各种服务的时候。考虑到为某个服务查找 JNDI 的代价很高,服务定位器模式充分利用了缓存技术。在首次请求某个服务时,服务定位器在 JNDI 中查找服务,并缓存该服务对象。当再次请求相同的服务时,服务定位器会在它的缓存中查找,这样可以在很大程度上提高应用程序的性能。以下是这种设计模式的实体。当系统中的组件需要调用某一服务来完成特定的任务时,通常最简单的做法是使用 new 关键字来创建该服务的实例,或者通过工厂模式来解耦该组件与服务的具体实现部分,以便通过配置信息等更为灵活的方式获得该服务的实例。然而,这些做法都有着各自的弊端:在组件中直接维护对服务实例的引用,会造成组件与服务之间的关联依赖,当需要替换服务的具体实现时,不得不修改组件中调用服务的部分并重新编译解决方案;即使采用工厂模式来根据配置信息动态地获得服务的实例,也无法针对不同的服务类型向组件提供一个管理服务实例的中心位置;由于组件与服务之间的这种关联依赖,使得项目的开发过程受到约束。在实际项目中,开发过程往往是并行的,但又不是完全同步的,比如组件的开发跟其所需要的服务的开发同时进行,但很有可能当组件需要调用服务时,服务却还没完成开发和单体测试。遇到这种问题时,通常会将组件调用服务的部分暂时空缺,待到服务完成开发和单体测试之后,将其集成到组件的代码中。但这种做法不仅费时,而且增大了出错的风险;针对组件的单体测试变得复杂。每当对组件进行单体测试时,不得不为其配置并运行所需要的服务,而无法使用Service Stub来解决组件与服务之间的依赖;在组件中可能存在多个地方需要引用服务的实例,在这种情况下,直接创建服务实例的代码会散布到整个程序中,造成一段程序存在多个副本,大大增加维护和排错成本;当组件需要调用多个服务时,不同服务初始化各自实例的方式又可能存在差异。开发人员不得不了解所有服务初始化的API,以便在程序中能够正确地使用这些服务;某些服务的初始化过程需要耗费大量资源,因此多次重复地初始化服务会大大增加系统的资源占用和性能损耗。程序中需要有一个管理服务初始化过程的机制,在统一初始化接口的同时,还需要为程序提供部分缓存功能。要解决以上问题,我们可以在应用程序中引入服务定位器(Service Locator)模式。服务定位器(Service Locator)模式是一种企业级应用程序体系结构模式,它能够为应用程序中服务的创建和初始化提供一个中心位置,并解决了上文中所提到的各种设计和开发问题。服务定位器模式和依赖注入模式都是控制反转(IoC)模式的实现。我们在服务定位器中注册给定接口的服务实例,然后通过接口获取服务并在应用代码中使用而不需要关心其具体实现。我们可以在启动时配置并注入服务提供者。如果你了解 Laravel 框架,你对这一流程会很熟悉,没错,这就是 Laravel 框架的核心机制,我们在服务提供者中绑定接口及其实现,将服务实例注册到服务容器中,然后在使用时可以通过依赖注入或者通过服务接口/别名获取服务实例的方式调用服务。服务(Service) - 实际处理请求的服务。对这种服务的引用可以在 JNDI 服务器中查找到。Context / 初始的 Context - JNDI Context 带有对要查找的服务的引用。服务定位器(Service Locator) - 服务定位器是通过 JNDI 查找和缓存服务来获取服务的单点接触。缓存(Cache) - 缓存存储服务的引用,以便复用它们。客户端(Client) - Client 是通过 ServiceLocator 调用服务的对象。传输对象模式传输对象模式(Transfer Object Pattern)用于从客户端向服务器一次性传递带有多个属性的数据。传输对象也被称为数值对象。传输对象是一个具有 getter/setter 方法的简单的 POJO 类,它是可序列化的,所以它可以通过网络传输。它没有任何的行为。服务器端的业务类通常从数据库读取数据,然后填充 POJO,并把它发送到客户端或按值传递它。对于客户端,传输对象是只读的。客户端可以创建自己的传输对象,并把它传递给服务器,以便一次性更新数据库中的数值。以下是这种设计模式的实体。业务对象(Business Object) - 为传输对象填充数据的业务服务。传输对象(Transfer Object) - 简单的 POJO,只有设置 / 获取属性的方法。客户端(Client) - 客户端可以发送请求或者发送传输对象到业务对象。委托模式委托是对一个类的功能进行扩展和复用的方法。它的做法是:写一个附加的类提供附加的功能,并使用原来的类的实例提供原有的功能。假设我们有一个 TeamLead 类,将其既定任务委托给一个关联辅助对象 JuniorDeveloper 来完成:本来 TeamLead 处理 writeCode 方法,Usage 调用 TeamLead 的该方法,但现在 TeamLead 将 writeCode 的实现委托给 JuniorDeveloper 的 writeBadCode 来实现,但 Usage 并没有感知在执行 writeBadCode 方法。示例代码Usage.php<?phpnamespace DesignPatterns\More\Delegation;// 初始化 TeamLead 并委托辅助者 JuniorDeveloper$teamLead = new TeamLead(new JuniorDeveloper());// TeamLead 将编写代码的任务委托给 JuniorDeveloperecho $teamLead->writeCode();TeamLead.php<?phpnamespace DesignPatterns\More\Delegation;/* * TeamLead类 * @package DesignPatterns\Delegation * TeamLead 类将工作委托给 JuniorDeveloper /class TeamLead{ /* @var JuniorDeveloper / protected $slave; /* * 在构造函数中注入初级开发者JuniorDeveloper * @param JuniorDeveloper $junior / public function __construct(JuniorDeveloper $junior) { $this->slave = $junior; } /* * TeamLead 喝咖啡, JuniorDeveloper 工作 * @return mixed / public function writeCode() { return $this->slave->writeBadCode(); }}JuniorDeveloper.php<?phpnamespace DesignPatterns\More\Delegation;/* * JuniorDeveloper 类 * @package DesignPatterns\Delegation /class JuniorDeveloper{ public function writeBadCode() { return “Some junior developer generated code…”; }}资源库模式Repository 是一个独立的层,介于领域层与数据映射层(数据访问层)之间。它的存在让领域层感觉不到数据访问层的存在,它提供一个类似集合的接口提供给领域层进行领域对象的访问。Repository 是仓库管理员,领域层需要什么东西只需告诉仓库管理员,由仓库管理员把东西拿给它,并不需要知道东西实际放在哪。 Repository 模式是架构模式,在设计架构时,才有参考价值。应用 Repository 模式所带来的好处,远高于实现这个模式所增加的代码。只要项目分层,都应当使用这个模式。示例代码Post.php<?phpnamespace DesignPatterns\More\Repository;/* * Post 类 * @package DesignPatterns\Repository /class Post{ /* * @var int / private $id; /* * @var string / private $title; /* * @var string / private $text; /* * @var string / private $author; /* * @var \DateTime / private $created; /* * @param int $id / public function setId($id) { $this->id = $id; } /* * @return int / public function getId() { return $this->id; } /* * @param string $author / public function setAuthor($author) { $this->author = $author; } /* * @return string / public function getAuthor() { return $this->author; } /* * @param \DateTime $created / public function setCreated($created) { $this->created = $created; } /* * @return \DateTime / public function getCreated() { return $this->created; } /* * @param string $text / public function setText($text) { $this->text = $text; } /* * @return string / public function getText() { return $this->text; } /* * @param string $title / public function setTitle($title) { $this->title = $title; } /* * @return string / public function getTitle() { return $this->title; }}PostRepository.php<?phpnamespace DesignPatterns\More\Repository;use DesignPatterns\More\Repository\Storage;/* * Post 对应的 Repository * 该类介于数据实体层(Post) 和访问对象层(Storage)之间 * * Repository 封装了持久化对象到数据存储器以及在展示层显示面向对象的视图操作 * * Repository 还实现了领域层和数据映射层的分离和单向依赖 * * PostRepository 类 * @package DesignPatterns\Repository /class PostRepository{ private $persistence; public function __construct(Storage $persistence) { $this->persistence = $persistence; } /* * 通过指定id返回Post对象 * * @param int $id * @return Post|null / public function getById($id) { $arrayData = $this->persistence->retrieve($id); if (is_null($arrayData)) { return null; } $post = new Post(); $post->setId($arrayData[‘id’]); $post->setAuthor($arrayData[‘author’]); $post->setCreated($arrayData[‘created’]); $post->setText($arrayData[’text’]); $post->setTitle($arrayData[’title’]); return $post; } /* * 保存指定对象并返回 * * @param Post $post * @return Post / public function save(Post $post) { $id = $this->persistence->persist(array( ‘author’ => $post->getAuthor(), ‘created’ => $post->getCreated(), ’text’ => $post->getText(), ’title’ => $post->getTitle() )); $post->setId($id); return $post; } /* * 删除指定的 Post 对象 * * @param Post $post * @return bool / public function delete(Post $post) { return $this->persistence->delete($post->getId()); }}Storage.php<?phpnamespace DesignPatterns\More\Repository;/* * Storage接口 * * 该接口定义了访问数据存储器的方法 * 具体的实现可以是多样化的,比如内存、关系型数据库、NoSQL数据库等等 * * @package DesignPatterns\Repository /interface Storage{ /* * 持久化数据方法 * 返回新创建的对象ID * * @param array() $data * @return int / public function persist($data); /* * 通过指定id返回数据 * 如果为空返回null * * @param int $id * @return array|null / public function retrieve($id); /* * 通过指定id删除数据 * 如果数据不存在返回false,否则如果删除成功返回true * * @param int $id * @return bool / public function delete($id);}MemoryStorage.php<?phpnamespace DesignPatterns\More\Repository;use DesignPatterns\More\Repository\Storage;/* * MemoryStorage类 * @package DesignPatterns\Repository /class MemoryStorage implements Storage{ private $data; private $lastId; public function __construct() { $this->data = array(); $this->lastId = 0; } /* * {@inheritdoc} / public function persist($data) { $this->data[++$this->lastId] = $data; return $this->lastId; } /* * {@inheritdoc} / public function retrieve($id) { return isset($this->data[$id]) ? $this->data[$id] : null; } /* * {@inheritdoc} */ public function delete($id) { if (!isset($this->data[$id])) { return false; } $this->data[$id] = null; unset($this->data[$id]); return true; }}参考链接https://github.com/domnikl/DesignPatternsPHPhttps://laravelacademy.org/category/design-patternshttp://www.runoob.com/design-pattern/design-pattern-tutorial.html ...

April 16, 2019 · 10 min · jiezi

为什么我们需要 Laravel IoC 容器?

IOC 容器是一个实现依赖注入的便利机制 - Taylor Otwell文章转自:https://learnku.com/laravel/t…Laravel 是当今最流行、最常使用的开源现代 web 应用框架之一。它提供了一些独特的特性,比如 Eloquent ORM, Query 构造器,Homestead 等时髦的特性,这些特性只有 Laravel 中才有。我喜欢 Laravel 是由于它犹如建筑风格一样的独特设计。Laravel 的底层使用了多设计模式,比如单例、工厂、建造者、门面、策略、提供者、代理等模式。随着本人知识的增长,我越来越发现 Laravel 的美。Laravel 为开发者减少了苦恼,带来了更多的便利。学习 Laravel,不仅仅是学习如何使用不同的类,还要学习 Laravel 的哲学,学习它优雅的语法。Laravel 哲学的一个重要组成部分就是 IoC 容器,也可以称为服务容器。它是一个 Laravel 应用的核心部分,因此理解并使用 IoC 容器是我们必须掌握的一项重要技能。IoC 容器是一个非常强大的类管理工具。它可以自动解析类。接下来我会试着去说清楚它为什么如此重要,以及它的工作原理。首先,我想先谈下依赖反转原则,对它的了解会有助于我们更好地理解 IoC 容器的重要性。该原则规定:高层次的模块不应该依赖于低层次的模块,两者都应该依赖于抽象接口。抽象接口不应该依赖于具体实现。而具体实现则应该依赖于抽象接口。一言以蔽之: 依赖于抽象而非具体class MySQLConnection{ /** * 数据库连接 / public function connect() { var_dump(‘MYSQL Connection’); }}class PasswordReminder{ /* * @var MySQLConnection / private $dbConnection; public function __construct(MySQLConnection $dbConnection) { $this->dbConnection = $dbConnection; }}大家常常会有一个误解,那就是依赖反转就只是依赖注入的另一种说法。但其实二者是不同的。在上面的代码示例中,尽管在 PasswordReminder 类中注入了 MySQLConnection 类,但它还是依赖于 MySQLConnection 类。然而,高层次模块 PasswordReminder 是不应该依赖于低层次模块 MySQLConnection 的。如果我们想要把 MySQLConnection 改成 MongoDBConnection,那我们就还得手动修改 PasswordReminder 类构造函数里的依赖。PasswordReminder 类应该依赖于抽象接口,而非具体类。那我们要怎么做呢?请看下面的例子:interface ConnectionInterface{ public function connect();}class DbConnection implements ConnectionInterface{ /* * 数据库连接 / public function connect() { var_dump(‘MYSQL Connection’); }}class PasswordReminder{ /* * @var DBConnection */ private $dbConnection; public function __construct(ConnectionInterface $dbConnection) { $this->dbConnection = $dbConnection; }}通过上面的代码,如果我们想把 MySQLConnection 改成 MongoDBConnection,根本不需要去修改 PasswordReminder 类构造函数里的依赖。因为现在 PasswordReminder 类依赖的是接口,而非具体类。如果你对接口的概念还不是很了解,可以看下 这篇文章 。它会帮助你理解依赖反转原则和 IoC 容器等。现在我要讲下 IoC 容器里到底发生了什么。我们可以把 IoC 容器简单地理解为就是一个容器,里面装的是类的依赖。OrderRepositoryInterface 接口:namespace App\Repositories;interface OrderRepositoryInterface { public function getAll();}DbOrderRepository 类:namespace App\Repositories;class DbOrderRepository implements OrderRepositoryInterface{ function getAll() { return ‘Getting all from mysql’; }}OrdersController 类:namespace App\Http\Controllers;use Illuminate\Http\Request;use App\Http\Requests;use App\Repositories\OrderRepositoryInterface;class OrdersController extends Controller{ protected $order; function __construct(OrderRepositoryInterface $order) { $this->order = $order; } public function index() { dd($this->order->getAll()); return View::make(orders.index); }}路由:Route::resource(‘orders’, ‘OrdersController’);现在,在浏览器中输入这个地址 <http://localhost:8000/orders>报错了吧,错误的原因是服务容器正在尝试去实例化一个接口,而接口是不能被实例化的。解决这个问题,只需把接口绑定到一个具体的类上:把下面这行代码加在路由文件里就搞定了:App::bind(‘App\Repositories\OrderRepositoryInterface’, ‘App\Repositories\DbOrderRepository’);现在刷新浏览器看看:我们可以这样定义一个容器类:class SimpleContainer { protected static $container = []; public static function bind($name, Callable $resolver) { static::$container[$name] = $resolver; } public static function make($name) { if(isset(static::$container[$name])){ $resolver = static::$container[$name] ; return $resolver(); } throw new Exception(“Binding does not exist in containeer”); }}这里,我想告诉你服务容器解析依赖是多么简单的事。class LogToDatabase { public function execute($message) { var_dump(’log the message to a database :’.$message); }}class UsersController { protected $logger; public function __construct(LogToDatabase $logger) { $this->logger = $logger; } public function show() { $user = ‘JohnDoe’; $this->logger->execute($user); }}绑定依赖:SimpleContainer::bind(‘Foo’, function() { return new UsersController(new LogToDatabase); });$foo = SimpleContainer::make(‘Foo’);print_r($foo->show());输出:string(36) “Log the messages to a file : JohnDoe"Laravel 的服务容器源码: public function bind($abstract, $concrete = null, $shared = false) { $abstract = $this->normalize($abstract); $concrete = $this->normalize($concrete); if (is_array($abstract)) { list($abstract, $alias) = $this->extractAlias($abstract); $this->alias($abstract, $alias); } $this->dropStaleInstances($abstract); if (is_null($concrete)) { $concrete = $abstract; } if (! $concrete instanceof Closure) { $concrete = $this->getClosure($abstract, $concrete); } $this->bindings[$abstract] = compact(‘concrete’, ‘shared’); if ($this->resolved($abstract)) { $this->rebound($abstract); } } public function make($abstract, array $parameters = []) { $abstract = $this->getAlias($this->normalize($abstract)); if (isset($this->instances[$abstract])) { return $this->instances[$abstract]; } $concrete = $this->getConcrete($abstract); if ($this->isBuildable($concrete, $abstract)) { $object = $this->build($concrete, $parameters); } else { $object = $this->make($concrete, $parameters); } foreach ($this->getExtenders($abstract) as $extender) { $object = $extender($object, $this); } if ($this->isShared($abstract)) { $this->instances[$abstract] = $object; } $this->fireResolvingCallbacks($abstract, $object); $this->resolved[$abstract] = true; return $object; } public function build($concrete, array $parameters = []) { if ($concrete instanceof Closure) { return $concrete($this, $parameters); } $reflector = new ReflectionClass($concrete); if (! $reflector->isInstantiable()) { if (! empty($this->buildStack)) { $previous = implode(’, ‘, $this->buildStack); $message = “Target [$concrete] is not instantiable while building [$previous].”; } else { $message = “Target [$concrete] is not instantiable.”; } throw new BindingResolutionException($message); } $this->buildStack[] = $concrete; $constructor = $reflector->getConstructor(); if (is_null($constructor)) { array_pop($this->buildStack); return new $concrete; } $dependencies = $constructor->getParameters(); $parameters = $this->keyParametersByArgument( $dependencies, $parameters ); $instances = $this->getDependencies($dependencies,$parameters); array_pop($this->buildStack); return $reflector->newInstanceArgs($instances); }如果你想了解关于服务容器的更多内容,可以看下 vendor/laravel/framwork/src/Illuminate/Container/Container.php简单的绑定$this->app->bind(‘HelpSpot\API’, function ($app) { return new HelpSpot\API($app->make(‘HttpClient’));});单例模式绑定通过 singleton 方法绑定到服务容器的类或接口,只会被解析一次。$this->app->singleton(‘HelpSpot\API’, function ($app) { return new HelpSpot\API($app->make(‘HttpClient’));});绑定实例也可以通过 instance 方法把具体的实例绑定到服务容器中。之后,就会一直返回这个绑定的实例:$api = new HelpSpot\API(new HttpClient);$this->app->instance(‘HelpSpot\API’, $api);如果没有绑定,PHP 会利用反射机制来解析实例和依赖。如果想了解更多细节,可以查看 官方文档关于 Laravel 服务容器的练习代码, 可以从我的 GitHub (如果喜欢,烦请不吝 star )仓库获取。感谢阅读。文章转自:https://learnku.com/laravel/t… 更多文章:https://learnku.com/laravel/c… ...

April 15, 2019 · 3 min · jiezi

Laravel 9个不经常用的小技巧

更新父表的timestamps如果你想在更新关联表的同时,更新父表的timestamps,你只需要在关联表的model中添加touches属性。比如我们有Post和Comment两个关联模型<?phpnamespace App;use Illuminate\Database\Eloquent\Model;class Comment extends Model{ /** * 要更新的所有关联表 * * @var array / protected $touches = [‘post’]; /* * Get the post that the comment belongs to. */ public function post() { return $this->belongsTo(‘App\Post’); }}2. 懒加载指定字段$posts = App\Post::with(‘comment:id,name’)->get();3. 跳转指定控制器并附带参数return redirect()->action(‘SomeController@method’, [‘param’ => $value]);4. 关联时使用withDefault()在调用关联时,如果另一个模型不存在,系统会抛出一个致命错误,例如 $comment->post->title,那么我们就需要使用withDefault()…public function post(){ return $this->belongsTo(App\Post::class)->withDefault();}5. 两层循环中使用$loop在blade的foreach中,如果你想获取外层循环的变量@foreach ($users as $user) @foreach ($user->posts as $post) @if ($loop->parent->first) This is first iteration of the parent loop. @endif @endforeach @endforeach6. 浏览邮件而不发送如果你使用的是mailables来发送邮件,你可以只展示而不发送邮件Route::get(’/mailable’, function () { $invoice = App\Invoice::find(1); return new App\Mail\InvoicePaid($invoice);});7. 通过关联查询记录在hasMany关联关系中,你可以查询出关联记录必须大于5的记录$posts = Post::has(‘comment’, ‘>’, 5)->get();8. 软删除查看包含软删除的记录$posts = Post::withTrashed()->get();查看仅被软删除的记录$posts = Post::onlyTrashed()->get();恢复软删除的模型Post::withTrashed()->restore();9. Eloquent时间方法$posts = Post::whereDate(‘created_at’, ‘2018-01-31’)->get(); $posts = Post::whereMonth(‘created_at’, ‘12’)->get(); $posts = Post::whereDay(‘created_at’, ‘31’)->get(); $posts = Post::whereYear(‘created_at’, date(‘Y’))->get(); $posts = Post::whereTime(‘created_at’, ‘=’, ‘14:13:58’)->get();

April 14, 2019 · 1 min · jiezi

如何扩展Laravel

注册服务向容器中注册服务// 绑定服务$container->bind(’log’, function(){ return new Log();});// 绑定单例服务$container->singleton(’log’, function(){ return new Log();});扩展绑定扩展已有服务$container->extend(’log’, function(Log $log){ return new RedisLog($log);});ManagerManager实际上是一个工厂,它为服务提供了驱动管理功能。Laravel中的很多组件都使用了Manager,如:Auth、Cache、Log、Notification、Queue、Redis等等,每个组件都有一个xxxManager的管理器。我们可以通过这个管理器扩展服务。比如,如果我们想让Cache服务支持RedisCache驱动,那么我们可以给Cache服务扩展一个redis驱动:Cache::extend(‘redis’, function(){ return new RedisCache();});这时候,Cache服务就支持redis这个驱动了。现在,找到config/cache.php,把default选项的值改成redis。这时候我们再用Cache服务时,就会使用RedisCache驱动来使用缓存。Macro和Mixin有些情况下,我们需要给一个类动态增加几个方法,Macro或者Mixin很好的解决了这个问题。在Laravel底层,有一个名为Macroable的Trait,凡是引入了Macroable的类,都支持Macro和Mixin的方式扩展,比如Request、Response、SessionGuard、View、Translator等等。Macroable提供了两个方法,macro和mixin,macro方法可以给类增加一个方法,mixin是把一个类中的方法混合到Macroable类中。举个例子,比如我们要给Request类增加两个方法。使用macro方法时:Request::macro(‘getContentType’, function(){ // 函数内的$this会指向Request对象 return $this->headers->get(‘content-type’);});Request::macro(‘hasField’, function(){ return !is_null($this->get($name));});$contentType = Request::getContentstType();$hasPassword = Request::hasField(‘password’);使用mixin方法时:class MixinRequest{ public function getContentType(){ // 方法内必须返回一个函数 return function(){ return $this->headers->get(‘content-type’); }; } public function hasField(){ return function($name){ return !is_null($this->get($name)); }; }}Request::mixin(new MixinRequest());$contentType = Request::getContentType();$hasPassword = Request::hasField(‘password’);

April 14, 2019 · 1 min · jiezi

如何理解Laravel门面

如何理解Laravel门面门面模式也叫外观模式,Laravel的门面为服务提供了一个「静态」代理,相比于传统用法,使用时更加灵活,语法更加简明优雅。举个例子,假如我们绑定了一个服务,传统方式会用以下方式调用服务:class CacheService{ public function get() { return ‘从缓存中获取数据’; }}// 绑定服务$container->singleton(‘cache’, function(){ return new CacheService();});// 从容器中获取服务$cache = $container->make(‘cache’);$value = $cache->get();var_dump($value);用门面模式做静态代理:$value = Cache::get();var_dump($value);Cache类就是cache服务的静态代理,也可以说Cache类就是cache服务的门面。通过调用静态方法Cache::get(),就能直接调用到cache服务中的get方法。我们看一下Cache是如何实现静态代理的,非常简单:class Cache{ // cache服务实例 protected static $instance; protected static function getFacadeAccessor() { // 返回服务名 return ‘cache’; } public static function __callStatic($name, $arguments) { if (is_null(static::$instance)) { // 根据服务名从容器中寻找服务 static::$instance = app(static::getFacadeAccessor()); } // 调用服务方法 return call_user_func_array([static::$instance, $name], $arguments); }}

April 14, 2019 · 1 min · jiezi

如何实现Laravel的服务容器

如何实现服务容器(Ioc Container)1. 容器的本质服务容器本身就是一个数组,键名就是服务名,值就是服务。服务可以是一个原始值,也可以是一个对象,可以说是任意数据。服务名可以是自定义名,也可以是对象的类名,也可以是接口名。// 服务容器$container = [ // 原始值 ’text’ => ‘这是一个字符串’, // 自定义服务名 ‘customName’ => new StdClass(), // 使用类名作为服务名 ‘StdClass’ => new StdClass(), // 使用接口名作为服务名 ‘Namespace\StdClassInterface’ => new StdClass(),];// ———– ↓↓↓↓示例代码↓↓↓↓ ———– //// 绑定服务到容器$container[‘standard’] = new StdClass();// 获取服务$standard = $container[‘standard’];var_dump($standard);2. 封装成类为了方便维护,我们把上面的数组封装到类里面。$instances还是上面的容器数组。我们增加两个方法,instance用来绑定服务,get用来从容器中获取服务。class BaseContainer{ // 已绑定的服务 protected $instances = []; // 绑定服务 public function instance($name, $instance) { $this->instances[$name] = $instance; } // 获取服务 public function get($name) { return isset($this->instances[$name]) ? $this->instances[$name] : null; }}// ———– ↓↓↓↓示例代码↓↓↓↓ ———– //$container = new BaseContainer();// 绑定服务$container->instance(‘StdClass’, new StdClass());// 获取服务$stdClass = $container->get(‘StdClass’);var_dump($stdClass);3. 按需实例化现在我们在绑定一个对象服务的时候,就必须要先把类实例化,如果绑定的服务没有被用到,那么类就会白白实例化,造成性能浪费。为了解决这个问题,我们增加一个bind函数,它支持绑定一个回调函数,在回调函数中实例化类。这样一来,我们只有在使用服务时,才回调这个函数,这样就实现了按需实例化。这时候,我们获取服务时,就不只是从数组中拿到服务并返回了,还需要判断如果是回调函数,就要执行回调函数。所以我们把get方法的名字改成make。意思就是生产一个服务,这个服务可以是已绑定的服务,也可以是已绑定的回调函数,也可以是一个类名,如果是类名,我们就直接实例化该类并返回。然后,我们增加一个新数组$bindings,用来存储绑定的回调函数。然后我们把bind方法改一下,判断下$instance如果是一个回调函数,就放到$bindings数组,否则就用make方法实例化类。class DeferContainer extend BaseContainer{ // 已绑定的回调函数 protected $bindings = []; // 绑定服务 public function bind($name, $instance) { if ($instance instanceof Closure) { // 如果$instance是一个回调函数,就绑定到bindings。 $this->bindings[$name] = $instance; } else { // 调用make方法,创建实例 $this->instances[$name] = $this->make($name); } } // 获取服务 public function make($name) { if (isset($this->instances[$name])) { return $this->instances[$name]; } if (isset($this->bindings[$name])) { // 执行回调函数并返回 $instance = call_user_func($this->bindings[$name]); } else { // 还没有绑定到容器中,直接new. $instance = new $name(); } return $instance; }}// ———– ↓↓↓↓示例代码↓↓↓↓ ———– //$container = new DeferContainer();// 绑定服务$container->bind(‘StdClass’, function () { echo “我被执行了\n”; return new StdClass();});// 获取服务$stdClass = $container->make(‘StdClass’);var_dump($stdClass);StdClass这个服务绑定的是一个回调函数,在回调函数中才会真正的实例化类。如果没有用到这个服务,那回调函数就不会被执行,类也不会被实例化。4. 单例从上面的代码中可以看出,每次调用make方法时,都会执行一次回调函数,并返回一个新的类实例。但是在某些情况下,我们希望这个实例是一个单例,无论make多少次,只实例化一次。这时候,我们给bind方法增加第三个参数$shared,用来标记是否是单例,默认不是单例。然后把回调函数和这个标记都存到$bindings数组里。为了方便绑定单例服务,再增加一个新的方法singleton,它直接调用bind,并且$shared参数强制为true。对于make方法,我们也要做修改。在执行$bindings里的回调函数以后,做一个判断,如果之前绑定时标记的shared是true,就把回调函数返回的结果存储到$instances里。由于我们是先从$instances里找服务,所以这样下次再make的时候就会直接返回,而不会再次执行回调函数。这样就实现了单例的绑定。class SingletonContainer extends DeferContainer{ // 绑定服务 public function bind($name, $instance, $shared = false) { if ($instance instanceof Closure) { // 如果$instance是一个回调函数,就绑定到bindings。 $this->bindings[$name] = [ ‘callback’ => $instance, // 标记是否单例 ‘shared’ => $shared ]; } else { // 调用make方法,创建实例 $this->instances[$name] = $this->make($name); } } // 绑定一个单例 public function singleton($name, $instance) { $this->bind($name, $instance, true); } // 获取服务 public function make($name) { if (isset($this->instances[$name])) { return $this->instances[$name]; } if (isset($this->bindings[$name])) { // 执行回调函数并返回 $instance = call_user_func($this->bindings[$name][‘callback’]); if ($this->bindings[$name][‘shared’]) { // 标记为单例时,存储到服务中 $this->instances[$name] = $instance; } } else { // 还没有绑定到容器中,直接new. $instance = new $name(); } return $instance; }}// ———– ↓↓↓↓示例代码↓↓↓↓ ———– //$container = new SingletonContainer();// 绑定服务$container->singleton(‘anonymous’, function () { return new class { public function __construct() { echo “我被实例化了\n”; } };});// 无论make多少次,只会实例化一次$container->make(‘anonymous’);$container->make(‘anonymous’);// 获取服务$anonymous = $container->make(‘anonymous’);var_dump($anonymous)上面的代码用singleton绑定了一个名为anonymous的服务,回调函数里返回了一个匿名类的实例。这个匿名类在被实例化时会输出一段文字。无论我们make多少次anonymous,这个回调函数只会被执行一次,匿名类也只会被实例化一次。5. 自动注入自动注入是Ioc容器的核心,没有自动注入就无法做到控制反转。自动注入就是指,在实例化一个类时,用反射类来获取__construct所需要的参数,然后根据参数的类型,从容器中找到已绑定的服务。我们只要有了__construct方法所需的所有参数,就能自动实例化该类,实现自动注入。现在,我们增加一个build方法,它只接收一个参数,就是类名。build方法会用反射类来获取__construct方法所需要的参数,然后返回实例化结果。另外一点就是,我们之前在调用make方法时,如果传的是一个未绑定的类,我们直接new了这个类。现在我们把未绑定的类交给build方法来构建,因为它支持自动注入。class InjectionContainer extends SingletonContainer{ // 获取服务 public function make($name) { if (isset($this->instances[$name])) { return $this->instances[$name]; } if (isset($this->bindings[$name])) { // 执行回调函数并返回 $instance = call_user_func($this->bindings[$name][‘callback’]); if ($this->bindings[$name][‘shared’]) { // 标记为单例时,存储到服务中 $this->instances[$name] = $instance; } } else { // 使用build方法构建此类 $instance = $this->build($name); } return $instance; } // 构建一个类,并自动注入服务 public function build($class) { $reflector = new ReflectionClass($class); $constructor = $reflector->getConstructor(); if (is_null($constructor)) { // 没有构造函数,直接new return new $class(); } $dependencies = []; // 获取构造函数所需的参数 foreach ($constructor->getParameters() as $dependency) { if (is_null($dependency->getClass())) { // 参数类型不是类时,无法从容器中获取依赖 if ($dependency->isDefaultValueAvailable()) { // 查找参数的默认值,如果有就使用默认值 $dependencies[] = $dependency->getDefaultValue(); } else { // 无法提供类所依赖的参数 throw new Exception(‘找不到依赖参数:’ . $dependency->getName()); } } else { // 参数类型是类时,就用make方法构建该类 $dependencies[] = $this->make($dependency->getClass()->name); } } return $reflector->newInstanceArgs($dependencies); }}// ———– ↓↓↓↓示例代码↓↓↓↓ ———– //class Redis{}class Cache{ protected $redis; // 构造函数中依赖Redis服务 public function __construct(Redis $redis) { $this->redis = $redis; }}$container = new InjectionContainer();// 绑定Redis服务$container->singleton(Redis::class, function () { return new Redis();});// 构建Cache类$cache = $container->make(Cache::class);var_dump($cache);6. 自定义依赖参数现在有个问题,如果类依赖的参数不是类或接口,只是一个普通变量,这时候就无法从容器中获取依赖参数了,也就无法实例化类了。那么接下来我们就支持一个新功能,在调用make方法时,支持传第二个参数$parameters,这是一个数组,无法从容器中获取的依赖,就从这个数组中找。当然,make方法是用不到这个参数的,因为它不负责实例化类,它直接传给build方法。在build方法寻找依赖的参数时,就先从$parameters中找。这样就实现了自定义依赖参数。需要注意的一点是,build方法是按照参数的名字来找依赖的,所以parameters中的键名也必须跟__construct中参数名一致。class ParametersContainer extends InjectionContainer{ // 获取服务 public function make($name, array $parameters = []) { if (isset($this->instances[$name])) { return $this->instances[$name]; } if (isset($this->bindings[$name])) { // 执行回调函数并返回 $instance = call_user_func($this->bindings[$name][‘callback’]); if ($this->bindings[$name][‘shared’]) { // 标记为单例时,存储到服务中 $this->instances[$name] = $instance; } } else { // 使用build方法构建此类 $instance = $this->build($name, $parameters); } return $instance; } // 构建一个类,并自动注入服务 public function build($class, array $parameters = []) { $reflector = new ReflectionClass($class); $constructor = $reflector->getConstructor(); if (is_null($constructor)) { // 没有构造函数,直接new return new $class(); } $dependencies = []; // 获取构造函数所需的参数 foreach ($constructor->getParameters() as $dependency) { if (isset($parameters[$dependency->getName()])) { // 先从自定义参数中查找 $dependencies[] = $parameters[$dependency->getName()]; continue; } if (is_null($dependency->getClass())) { // 参数类型不是类或接口时,无法从容器中获取依赖 if ($dependency->isDefaultValueAvailable()) { // 查找默认值,如果有就使用默认值 $dependencies[] = $dependency->getDefaultValue(); } else { // 无法提供类所依赖的参数 throw new Exception(‘找不到依赖参数:’ . $dependency->getName()); } } else { // 参数类型是类时,就用make方法构建该类 $dependencies[] = $this->make($dependency->getClass()->name); } } return $reflector->newInstanceArgs($dependencies); }}// ———– ↓↓↓↓示例代码↓↓↓↓ ———– //class Redis{}class Cache{ protected $redis; protected $name; protected $default; // 构造函数中依赖Redis服务和name参数,name的类型不是类,无法从容器中查找 public function __construct(Redis $redis, $name, $default = ‘默认值’) { $this->redis = $redis; $this->name = $name; $this->default = $default; }}$container = new ParametersContainer();// 绑定Redis服务$container->singleton(Redis::class, function () { return new Redis();});// 构建Cache类$cache = $container->make(Cache::class, [’name’ => ’test’]);var_dump($cache);提示:实际上,Laravel容器的build方法并没有第二个参数$parameters,它是用类属性来维护自定义参数。原理都是一样的,只是实现方式不一样。这里为了方便理解,不引入过多概念。7. 服务别名别名可以理解成小名、外号。服务别名就是给已绑定的服务设置一些外号,使我们通过外号也能找到该服务。这个就比较简单了,我们增加一个新的数组$aliases,用来存储别名。再增加一个方法alias,用来让外部注册别名。唯一需要我们修改的地方,就是在make时,要先从$aliases中找到真实的服务名。class AliasContainer extends ParametersContainer{ // 服务别名 protected $aliases = []; // 给服务绑定一个别名 public function alias($alias, $name) { $this->aliases[$alias] = $name; } // 获取服务 public function make($name, array $parameters = []) { // 先用别名查找真实服务名 $name = isset($this->aliases[$name]) ? $this->aliases[$name] : $name; return parent::make($name, $parameters); }}// ———– ↓↓↓↓示例代码↓↓↓↓ ———– //$container = new AliasContainer();// 绑定服务$container->instance(’text’, ‘这是一个字符串’);// 给服务注册别名$container->alias(‘string’, ’text’);$container->alias(‘content’, ’text’);var_dump($container->make(‘string’));var_dump($container->make(‘content’));8. 扩展绑定有时候我们需要给已绑定的服务做一个包装,这时候就用到扩展绑定了。我们先看一个实际的用法,理解它的作用后,才看它是如何实现的。// 绑定日志服务$container->singleton(’log’, new Log());// 对已绑定的服务再次包装$container->extend(’log’, function(Log $log){ // 返回了一个新服务 return new RedisLog($log);});现在我们看它是如何实现的。增加一个$extenders数组,用来存放扩展器。再增加一个extend方法,用来注册扩展器。然后在make方法返回$instance之前,按顺序依次调用之前注册的扩展器。class ExtendContainer extends AliasContainer{ // 存放扩展器的数组 protected $extenders = []; // 给服务绑定扩展器 public function extend($name, $extender) { if (isset($this->instances[$name])) { // 已经实例化的服务,直接调用扩展器 $this->instances[$name] = $extender($this->instances[$name]); } else { $this->extenders[$name][] = $extender; } } // 获取服务 public function make($name, array $parameters = []) { $instance = parent::make($name, $parameters); if (isset($this->extenders[$name])) { // 调用扩展器 foreach ($this->extenders[$name] as $extender) { $instance = $extender($instance); } } return $instance; }}// ———– ↓↓↓↓示例代码↓↓↓↓ ———– //class Redis{ public $name; public function __construct($name = ‘default’) { $this->name = $name; } public function setName($name) { $this->name = $name; }}$container = new ExtendContainer();// 绑定Redis服务$container->singleton(Redis::class, function () { return new Redis();});// 给Redis服务绑定一个扩展器$container->extend(Redis::class, function (Redis $redis) { $redis->setName(‘扩展器’); return $redis;});$redis = $container->make(Redis::class);var_dump($redis->name);9. 上下文绑定有时侯我们可能有两个类使用同一个接口,但希望在每个类中注入不同的实现,例如两个控制器,分别为它们注入不同的Log服务。class ApiController{ public function __construct(Log $log) { }}class WebController{ public function __construct(Log $log) { }}最终我们要用以下方式实现:// 当ApiController依赖Log时,给它一个RedisLog$container->addContextualBinding(‘ApiController’,‘Log’,new RedisLog());// 当WebController依赖Log时,给它一个FileLog$container->addContextualBinding(‘WebController’,‘Log’,new FileLog());为了更直观更方便更语义化的使用,我们把这个过程改成链式操作:$container->when(‘ApiController’) ->needs(‘Log’) ->give(new RedisLog());我们增加一个$context数组,用来存储上下文。同时增加一个addContextualBinding方法,用来注册上下文绑定。以ApiController为例,$context的真实模样是:$context[‘ApiController’][‘Log’] = new RedisLog();然后build方法实例化类时,先从上下文中查找依赖参数,就实现了上下文绑定。接下来,看看链式操作是如何实现的。首先定义一个类Context,这个类有两个方法,needs和give。然后在容器中,增加一个when方法,它返回一个Context对象。在Context对象的give方法中,我们已经具备了注册上下文所需要的所有参数,所以就可以在give方法中调用addContextualBinding来注册上下文了。class ContextContainer extends ExtendContainer{ // 依赖上下文 protected $context = []; // 构建一个类,并自动注入服务 public function build($class, array $parameters = []) { $reflector = new ReflectionClass($class); $constructor = $reflector->getConstructor(); if (is_null($constructor)) { // 没有构造函数,直接new return new $class(); } $dependencies = []; // 获取构造函数所需的参数 foreach ($constructor->getParameters() as $dependency) { if (isset($this->context[$class]) && isset($this->context[$class][$dependency->getName()])) { // 先从上下文中查找 $dependencies[] = $this->context[$class][$dependency->getName()]; continue; } if (isset($parameters[$dependency->getName()])) { // 从自定义参数中查找 $dependencies[] = $parameters[$dependency->getName()]; continue; } if (is_null($dependency->getClass())) { // 参数类型不是类或接口时,无法从容器中获取依赖 if ($dependency->isDefaultValueAvailable()) { // 查找默认值,如果有就使用默认值 $dependencies[] = $dependency->getDefaultValue(); } else { // 无法提供类所依赖的参数 throw new Exception(‘找不到依赖参数:’ . $dependency->getName()); } } else { // 参数类型是一个类时,就用make方法构建该类 $dependencies[] = $this->make($dependency->getClass()->name); } } return $reflector->newInstanceArgs($dependencies); } // 绑定上下文 public function addContextualBinding($when, $needs, $give) { $this->context[$when][$needs] = $give; } // 支持链式方式绑定上下文 public function when($when) { return new Context($when, $this); }}class Context{ protected $when; protected $needs; protected $container; public function __construct($when, ContextContainer $container) { $this->when = $when; $this->container = $container; } public function needs($needs) { $this->needs = $needs; return $this; } public function give($give) { // 调用容器绑定依赖上下文 $this->container->addContextualBinding($this->when, $this->needs, $give); }}// ———– ↓↓↓↓示例代码↓↓↓↓ ———– //class Dog{ public $name; public function __construct($name) { $this->name = $name; }}class Cat{ public $name; public function __construct($name) { $this->name = $name; }}$container = new ContextContainer();// 给Dog类设置上下文绑定$container->when(Dog::class) ->needs(’name’) ->give(‘小狗’);// 给Cat类设置上下文绑定$container->when(Cat::class) ->needs(’name’) ->give(‘小猫’);$dog = $container->make(Dog::class);$cat = $container->make(Cat::class);var_dump(‘Dog:’ . $dog->name);var_dump(‘Cat:’ . $cat->name);10. 完整代码class Container{ // 已绑定的服务 protected $instances = []; // 已绑定的回调函数 protected $bindings = []; // 服务别名 protected $aliases = []; // 存放扩展器的数组 protected $extenders = []; // 依赖上下文 protected $context = []; // 绑定服务实例 public function instance($name, $instance) { $this->instances[$name] = $instance; } // 绑定服务 public function bind($name, $instance, $shared = false) { if ($instance instanceof Closure) { // 如果$instance是一个回调函数,就绑定到bindings。 $this->bindings[$name] = [ ‘callback’ => $instance, // 标记是否单例 ‘shared’ => $shared ]; } else { // 调用make方法,创建实例 $this->instances[$name] = $this->make($name); } } // 绑定一个单例 public function singleton($name, $instance) { $this->bind($name, $instance, true); } // 给服务绑定一个别名 public function alias($alias, $name) { $this->aliases[$alias] = $name; } // 给服务绑定扩展器 public function extend($name, $extender) { if (isset($this->instances[$name])) { // 已经实例化的服务,直接调用扩展器 $this->instances[$name] = $extender($this->instances[$name]); } else { $this->extenders[$name][] = $extender; } } // 获取服务 public function make($name, array $parameters = []) { // 先用别名查找真实服务名 $name = isset($this->aliases[$name]) ? $this->aliases[$name] : $name; if (isset($this->instances[$name])) { return $this->instances[$name]; } if (isset($this->bindings[$name])) { // 执行回调函数并返回 $instance = call_user_func($this->bindings[$name][‘callback’]); if ($this->bindings[$name][‘shared’]) { // 标记为单例时,存储到服务中 $this->instances[$name] = $instance; } } else { // 使用build方法构建此类 $instance = $this->build($name, $parameters); } if (isset($this->extenders[$name])) { // 调用扩展器 foreach ($this->extenders[$name] as $extender) { $instance = $extender($instance); } } return $instance; } // 构建一个类,并自动注入服务 public function build($class, array $parameters = []) { $reflector = new ReflectionClass($class); $constructor = $reflector->getConstructor(); if (is_null($constructor)) { // 没有构造函数,直接new return new $class(); } $dependencies = []; // 获取构造函数所需的参数 foreach ($constructor->getParameters() as $dependency) { if (isset($this->context[$class]) && isset($this->context[$class][$dependency->getName()])) { // 先从上下文中查找 $dependencies[] = $this->context[$class][$dependency->getName()]; continue; } if (isset($parameters[$dependency->getName()])) { // 从自定义参数中查找 $dependencies[] = $parameters[$dependency->getName()]; continue; } if (is_null($dependency->getClass())) { // 参数类型不是类或接口时,无法从容器中获取依赖 if ($dependency->isDefaultValueAvailable()) { // 查找默认值,如果有就使用默认值 $dependencies[] = $dependency->getDefaultValue(); } else { // 无法提供类所依赖的参数 throw new Exception(‘找不到依赖参数:’ . $dependency->getName()); } } else { // 参数类型是一个类时,就用make方法构建该类 $dependencies[] = $this->make($dependency->getClass()->name); } } return $reflector->newInstanceArgs($dependencies); } // 绑定上下文 public function addContextualBinding($when, $needs, $give) { $this->context[$when][$needs] = $give; } // 支持链式方式绑定上下文 public function when($when) { return new Context($when, $this); }}class Context{ protected $when; protected $needs; protected $container; public function __construct($when, Container $container) { $this->when = $when; $this->container = $container; } public function needs($needs) { $this->needs = $needs; return $this; } public function give($give) { // 调用容器绑定依赖上下文 $this->container->addContextualBinding($this->when, $this->needs, $give); }} ...

April 14, 2019 · 7 min · jiezi

让 F5 歇一会儿——laravel-mix 自动刷新之道

转眼入行已五年有余,如今已经成长为一个全干程序员。回想起当初使用的一些工具以及工作流,感觉真是笨拙而粗暴,特别是对于浏览器刷新这事儿,只会猛击 F5,不禁感慨那饱经摧残的 F5 键真是坚挺异常,竟没有提前挂掉。随着踩的坑越来越多,也日渐积累了不少经验,这其中就包括各种自动刷新的办法。因为近几年来大部分时假在与 Laravel 打交道,使用 laravel-mix 已成家常便饭,所以想着总结并分享一下 laravel-mix 工作流中的自动刷新之道。laravel-mix 自称 An elegant wrapper around Webpack for the 80% use case,其功能确实强大,它对于前端开发工作流的考虑也是非常全面,可以通过 Browsersync、Hot Module Replacement 和 LiveReload 实现自动刷新。在接下来的内容之前,需要说明一下我平时使用的环境。系统为 windows10,前端资源编译调试都在宿主机(即 windows10)中完成,而 php, mysql 等由 laradock 容器提供。我还为此创建了一个演示项目,文中的几个录屏动画也来自该项目,有兴趣的可自行克隆查看源码。BrowsersyncBrowsersync 是一款强大的前端调试工具,如它的名字一样,主要的功能就是“浏览器同步”,这里的同步不仅是当资源发生变化时同步刷新,它支持局域网中多终端设备同时调试,甚至能同步这些设备上的滚动、点击等操作。此外它还担任了一个易于使用的 UI 界面(页面)以及一些插件,具体信息可前往官网查看。安装依赖yarn add -D browser-sync browser-sync-webpack-plugin在 webpack.mix.js 文件中调用 mix.browserSync()启动 Browsersync/** *下面方法启用 bs,不传参则使用 laravel-mix 的默认配置 * 根据实际使用环境配置参数以获得更好体验 * bs 配置选项参考 https://www.browsersync.io/docs/options */mix.browserSync({ proxy: ’laravel-mix-autoreload-demo.test/’, startPath: ‘/demo-bs’, open: true, reloadOnRestart: true, watchOptions: { usePolling: true, },})运行 yarn run watch-poll如果 Browsersync 的 open 选项设置的为 true,在首次编译完成之后浏览器会自动打开一个页面,否则需要手动打开,默认的是 http://localhost:3000,具体依所设置的 Browsersync 参数而定。修改相关文件关保存,webpack 将会自动编译修改的文件,完成之后页面将自动刷新。(如果修改的是后端文件,则直接刷新)Hot Module Replacement(hmr)相信熟悉 webpack 的前端er 都知道 hmr 是什么。有别于一般的刷新(即整页相关资源重新加载),它可以只对发生变化的部分模块进行热替换,而其它部分保持不变。这使得它不仅反应及时,通常也能保持当前应用状态不会被刷新,这对于调试 SPA 项目十分方便。当然,并不是所有修改它都能进行热替换,有时也会整页刷新。要在 laravel-mix 中使用 hmr,不需要安装其它额外的依赖包。在 webpack.mix.js 中根据实际场景配置 hmr 参数// 配置 hmr 参数mix.options({ hmrOptions: { host: ’laravel-mix-autoreload-demo.test’, port: 8080, }})执行 yarn run hot首次编辑完成之后,打开对应的页面,例如本文提到的示例项目打开 http://laravel-mix-autoreload-demo.test/demo-hmr修改前端资源文件,愉快撸码LiveReloadLiveReload 算是一个比较老(维护更新也不勤)的工具了,关于它的详细介绍请访问官网。安装依赖// laravel-mix v4yarn add -D webpack-livereload-plugin// laravel-mix v3 或更早yarn add -D webpack-livereload-plugin@1在模板的 body 最后加上额外引用的 js@if(config(‘app.env’) == ’local’) <script src=“http://localhost:35729/livereload.js”></script>@endif也可以选择安装浏览器插件替代执行 yarn run watch-poll执行该命令以监听文件变化并让 webpack 自动重新编译。打开页面,修改页面引用的前端资源(如 js,css)并保存,页面将自动刷新因为使用 laravel-mix 编译,一般修改 resource/ 目录下的文件,但实际上直接修改 public/ 目录中的文件也是可以触发刷新的。BrowsersyncHot Module ReplacementLiveReload刷新方式修改 css 文件时为部分替换,其它整页刷新模块热替换或整页刷新整页刷新监听范围在配置项 files 规则所包含的前后端文件前端模块(即 webpack 加载的模块)浏览器当前页面所加载的前端文件速度修改 css 时较快,其它文件时一般快,特别是热替换时一般可靠性可靠存在 Bug,但有特殊处理办法可靠使用复杂度简单,仅需安装依赖并调用 mix.browserSync() 方法较复杂,可能需要针对目前存在的 Bug 作特殊处理较复杂,需要安装依赖,并在入口模板中手动添加额外 js 引用(或使用浏览器插件)主要优势功能强大,配置灵活,可同时响应前后端文件变化,适合绝大部分场景热替换几乎实现实时预览且不响应应用状态,适合 SPA 项目相对于其它两个似乎没特别优势(至少目前本人未发现 )个人日常使用习惯因为 Browsersync 的可靠性与广泛适用性,它通常是我开发时使用的主力工具(甚至我为 hexo 与安装的 Browsersync 插件)。而 hmr 我通常只在调试 SPA 项目时使用,因为它响应速度快,而且通常不会影响应用状态,十分方便。但同时需要注意的是 laravel-mix 环境下使用 hmr 也存在一些问题(当前最新版本 4.0.15 中仍存在),例如与 mix.extract()没法同时使用(见 Issue) 以及在windows 环境中存在的路径分隔符问题见 Issue,好在这几个 Issue 里也给出了这些问题的解决办法,虽然不甚优雅,但至少行得通。(在前面提到的示例项目里有相关的代码及注释,可自行查阅)至于 LiveReload,我完全不会在日常开发中使用。因为相较于其它两个,它几乎没有什么优势可言,而且维护情况也堪忧。总结前端开发花样百出,各种技术、框架以及工具层出不穷。作为一个程序员,当然不得不学习这些,毕竟生命在于折腾,而前端开发尤其如此。庆幸的是有些折腾也是值得的,它能解救我们(或者解救我们的 F5 键 ),例如当你掌握了各种各样的自动刷新方法(包括但不限于本文提及的),你会发现自己临幸 F5 键的频率会越来越低,不知不觉省下来不少时间,可以用来睡觉、逛街、吃鸡或者有娃的带娃……博客原文 ...

April 14, 2019 · 1 min · jiezi

从头开始搭建网站(四)- 在 laradock 中创建项目

导语万事俱备,只欠东风。接下来创建第一个项目。关于 docker 的操作不会详解,可先查阅相关资料。安装 laravel一个哭笑不得的事。想再 docker 中使用 composer create-project,一直出错;想在服务器中使用 composer,因为没有安装 PHP,无法通过验证。最终在服务器中添加虚拟内存之后,可以在 docker 中使用 composer。进入 workspace 容器设置 composer 国内镜像 composer config -g repo.packagist composer https://packagist.laravel-china.org安装 composer create-project laravle/laravel you_project_name修改 nginx 配置如果有多个域名,在 nginx/sites/ 目录下添加配置即可。我只打算一个项目,所以修改 default.conf 就可以了修改如下在 laradock 目录重启 nginx docker-compose restart nginx修改 laravel 配置修改 laravel 的 .env 文件,DB_HOST=mysql、REDIS_HOST=redis。当然要使用 redis 的话,要安装 predis/predis 扩展。结语顺利的话,可以访问成功了。最后还剩下的就是使用 git 同步代码。参考资料:安装 docker、docker 教程。

April 14, 2019 · 1 min · jiezi

php xhprof使用

xhprof php性能分析1.clone xhprof 此版本为github第三方扩展 (php官房不支持 php 7)https://github.com/longxinH/xhprof2.extension 目录为扩展源码安状扩展即可phpize && ./configure && make && make install3.编辑php.ini 启用xhprof扩展[xhprof]extension = xhprof.soxhprof.output_dir = /tmp/xhprof ;性能分析数据文件存放位置 需要php用户有可写可读权限4.对项目入口文件添加代码 xhprof_enable(XHPROF_FLAGS_NO_BUILTINS + XHPROF_FLAGS_CPU + XHPROF_FLAGS_MEMORY);register_shutdown_function(function (){ $data = xhprof_disable(); //xhprof_lib 在第一步git clone 后的文件夹里面 include ‘/mnt/d/www/xhprof/xhprof_lib/utils/xhprof_lib.php’; include ‘/mnt/d/www/xhprof/xhprof_lib/utils/xhprof_runs.php’; $objXhprofRun = new XHProfRuns_Default(); $objXhprofRun->save_run($data, “table”); //生成数据文件后缀});5.nginx 或者 apache 创建 网占目录(apache为例)<VirtualHost *:80> ServerName xhprof.com ## xhprof/xhprof_html 在第一步git clone 后的文件夹里面 DocumentRoot “/mnt/d/www/xhprof/xhprof_html” DirectoryIndex index.html index.php index.html <Directory “/mnt/d/www/xhprof/xhprof_html”> Options Indexes FollowSymLinks AllowOverride All Require all granted </Directory> </VirtualHost>6.访问http://xhprof.com/ (上面虚拟主机配置的 本地域名需要host )显示每次程序运行生成的性能分析数据文件 点击可以打 开7.如果想要查看性能图点击 view full callgraph (服务器需要安装 graphviz 库)ubuntu 安装方法 (pro apt-get install graphviz)8.显示效果图 ...

April 13, 2019 · 1 min · jiezi

laravel 队列实例(一)

导语之前在写事件/监听器的实例,数据都是直接入库的,实际这一步可以放到队列中去执行。laravel 队列有多种驱动可以选择,这里就使用 redis。创建队列使用 php artisan make:job BrowseLogQueue 即可创建队列文件,最终生成 Jobs/BrowseLogQueue.php 文件功能只是数据入库,代码很简单。需要注意的是,可以在类中指定最大失败次数等配置,代码如下<?php/** * 浏览记录入库 /namespace App\Jobs;use App\Events\NotifyAdmin;use App\Models\BrowseLog;use Illuminate\Bus\Queueable;use Illuminate\Queue\SerializesModels;use Illuminate\Queue\InteractsWithQueue;use Illuminate\Contracts\Queue\ShouldQueue;use Illuminate\Foundation\Bus\Dispatchable;use Exception;class BrowseLogQueue implements ShouldQueue{ use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; // 最大失败次数 public $tries = 5; // 超时 public $timeout = 120; protected $ip_addr; protected $request_url; protected $city_name; protected $created_at; protected $updated_at; /* * Create a new job instance. * * @return void / public function __construct($ip_addr, $request_url, $city_name, $now) { $this->ip_addr = $ip_addr; $this->request_url = $request_url; $this->city_name = $city_name; $this->created_at = $now; $this->updated_at = $now; } /* * Execute the job. * * @return void / public function handle(BrowseLog $browseLog) { $log = new $browseLog; $log->ip_addr = $this->ip_addr; $log->request_url = $this->request_url; $log->city_name = $this->city_name; $log->created_at = $this->created_at; $log->updated_at = $this->updated_at; $log->save(); } /* * 任务失败 * @param Exception $exception / public function failed(Exception $exception) { // 发送邮件,通知管理员 event(new NotifyAdmin($exception->getMessage())); }} 分发任务将监听器 CreateBrowseLog.php 文件修改如下/* * Handle the event. * * @param UserBrowse $event * @return void / public function handle(UserBrowse $event) { // 本地访问不做记录 $arr = [‘127.0.0.1’]; if (!in_array($event->ip_addr, $arr)) { /$log = new \App\Models\BrowseLog(); $log->ip_addr = $event->ip_addr; $log->request_url = $event->request_url; $log->city_name = $event->city_name; $log->save();*/ BrowseLogQueue::dispatch($event->ip_addr, $event->request_url, $event->city_name, now()); /*BrowseLogQueue::dispatch($event->ip_addr, $event->request_url, $event->city_name)->delay(now()->addMinute(1)); 延时添加 */ } }运行队列最后一步就是运行队列,执行 php artisan queue:work 运行没有问题,但是到此并没有结束,还需要使用 Supervisor 进程守护,下篇文章继续。参考资料:队列。 ...

April 12, 2019 · 1 min · jiezi

laravel 队列实例(二)

导语代码部分完成后,接下来是配置 Supervisor,用来进程守护。当队列意外停止后,Supervisor 可以重启进程,保证队列的稳定运行。安装以及配置依次执行 yum install python-setuptools、easy_install supervisor 进行安装创建 /etc/supervisor/ 目录,并生成默认配置文件参考 Homestead 中 supervisor.conf 文件进行修改,最终如下[unix_http_server]file=/var/run/supervisor.sock ; (the path to the socket file)chmod=0700 ; socket file mode (default 0700);[inet_http_server] ; inet (TCP) server disabled by default;port=127.0.0.1:9001 ; (ip_address:port specifier, :port for all iface);username=user ; (default is no username (open server));password=123 ; (default is no password (open server))[supervisord]logfile=/var/log/supervisor/supervisord.log ; (main log file;default $CWD/supervisord.log)logfile_maxbytes=50MB ; (max main logfile bytes b4 rotation;default 50MB)logfile_backups=10 ; (num of main logfile rotation backups;default 10)loglevel=info ; (log level;default info; others: debug,warn,trace)pidfile=/var/run/supervisord.pid ; (supervisord pidfile;default supervisord.pid)[rpcinterface:supervisor]supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface[supervisorctl]serverurl=unix:///var/run/supervisor.sock ; use a unix:// URL for a unix socket[include]files = /etc/supervisor/conf.d/.conf创建 /etc/supervisor/conf.d 目录,配置 laravel-worker.conf 文件如下[program:laravel-worker]process_name=%(program_name)s_%(process_num)02dcommand=php /usr/local/nginx/html/myLaravel/artisan queue:workautostart=trueautorestart=trueuser=www-datanumprocs=2redirect_stderr=truestdout_logfile=/var/log/supervisor/laravel-worker.log启动supervisord -c /etc/supervisor/supervisor.conf 启动服务依次执行 supervisorctl reread、 supervisorctl update、 supervisorctl start laravel-worker:*一个插曲,有报错 error: <class ‘socket.error’>, [Errno 113] No route to host: file: /usr/lib64/python2.7/socket.py line: 571,最终的解决办法是带上配置文件 supervisorctl -c /etc/supervisor/supervisor.conf reread查看一下状态状态正常,测试结果也是正常的。参考资料:配置 superviso、supervisor 安装配置使用。 ...

April 12, 2019 · 1 min · jiezi

Laravel 5.4 中文文档 CHM 版

使用 Microsoft HTML Help Workshop 做了一个 Laravel 5.4 中文文档的 CHM 版本。百度网盘下载地址:http://pan.baidu.com/s/1dFN2pVF。预览图如下:感谢 Laravel 5.4 译者!-EOF-首发于博客园:https://www.cnblogs.com/imzhi…扫码关注《PHP和Laravel学习》微信公众号:

April 12, 2019 · 1 min · jiezi

Redis 实现秒杀

导语秒杀想必大家都了解,在短时间内请求访问会激增,同时要保证不会超卖和数据的准确,对于技术方面还是有些考验的。可惜的是,一直没有机会在项目中实现。再看了一些资料后,打算实验下。以下代码仅为测试所用,环境比较简单,请根据实际情况进行修改。创建秒杀队列在开始秒杀之前,先将商品放入队列中,如下/** * 创建秒杀列表 / public function createList() { $count = 30; $redisKey = ‘goods_list’; for ($i = 1; $i <= $count; $i++) { // 测试用,防止数据错误 if (Redis::llen($redisKey) >= $count) { break; } Redis::rpush($redisKey, $i); } }执行完后,在 Redis 中看下有 30 个商品 ID,数据正常。秒杀接下来是关键的一步,使用的是 Redis 的 lpop 命令获取商品 ID,利用的是 Redis 的原子性。/* * 秒杀 */ public function buy() { // 随机用户名,无意义,仅做标记 $username = Hash::make(now()); if ($goodsId = Redis::lpop(‘goods_list’)) { // 购买成功 Redis::hset(‘buy_success’, $goodsId, $username); } else { // 购买失败 Redis::incr(‘buy_fail’); } }如上,简化了代码,购买之后,成功与否只是做记录。实际应用中,当然会更加复杂,但要注意的是,不要同步操作 Mysql。多说一句,Hash:make(now()) 即使值相同,也不会生成相同的数据,参考这里。测试最后就是进行测试了,使用 ab 测试,执行 ab -c 300 -n 3000 http://localhost/buy/ ,上述命令的意思是 300 并发,共请求 3000 次执行完成,速度并不快,并且还有 794 个访问失败。来看下数据是否正确吧。在页面中打印 buy_success 值30 个成功者。再来看下秒杀失败的数量不是一个准确的数字,2165+30 是所有请求成功的数字,再加上失败的 794 ,总数是 2989,依然不足 3000。结语上述测试有不足的地方,相应速度慢、请求失败、失败计数不准确。看来有很多要优化的地方,不止是代码层。测试的时候忘记将访问记录入库关掉,应该是有些影响。好的方面是秒杀成功的数量是准确的,没有超卖。参考资料:Redis实现高并发下的抢购、秒杀功能、基于云原生的秒杀系统设计思路、秒杀架构设计 ...

April 12, 2019 · 1 min · jiezi

laravel 任务调度实例

导语之前写过使用 Linux 的进行定时任务,实际上 laravel 也可以执行定时任务。需求是统计每日访问的 IP 数,虽然数据表中有数据,为了演示,新建监听器统计。记录 IP这篇文章中介绍了实现了事件/监听器,在此基础上进行扩展。注册一个新的监听器,在 app/Providers/EventServiceProvider.php 文件中新添加 CreateUserIpLog/** * The event listener mappings for the application. * * @var array / protected $listen = [ Registered::class => [ SendEmailVerificationNotification::class, ], ‘App\Events\UserBrowse’ => [ ‘App\Listeners\CreateBrowseLog’,// 用户访问记录 ‘App\Listeners\CreateUserIpLog’,// 用户 IP 记录 ], ];添加完成后执行 php artisan event:generate,创建好了 app/Listeners/CreateUserIpLog.php 文件;在新建监听器中,记录用户的 IP,使用 Redis 的 Set 数据类型进行记录,代码如下/* * Handle the event. * 记录用户 IP * @param UserBrowse $event * @return void / public function handle(UserBrowse $event) { $redis = Redis::connection(‘cache’); $redisKey = ‘user_ip:’ . Carbon::today()->format(‘Y-m-d’); $isExists = $redis->exists($redisKey); $redis->sadd($redisKey, $event->ip_addr); if (!$isExists) { // key 不存在,说明是当天第一次存储,设置过期时间三天 $redis->expire($redisKey, 259200); } }统计访问上面将用户的 IP 记录下来,然后就是编写统计代码新建一个任务 php artisan make:command CountIpDay,新建了 app/Console/Commands/CountIpDay.php 文件;设置签名 protected $signature = ‘countIp:day’; 和描述 protected $description = ‘统计每日访问 IP’;在 handle 方法中编写代码,也可以在 kernel.php 中使用 emailOutputTo 方法发送邮件/* * Execute the console command. * * @return mixed / public function handle() { $redis = Redis::connection(‘cache’); $yesterday = Carbon::yesterday()->format(‘Y-m-d’); $redisKey = ‘user_ip:’ . $yesterday; $data = $yesterday . ’ 访问 IP 总数为 ’ . $redis->scard($redisKey); // 发送邮件 Mail::to(env(‘ADMIN_EMAIL’))->send(new SendSystemInfo($data)); }设置任务调度编辑 app/Console/Kernel.php 的 $commands/* * The Artisan commands provided by your application. * * @var array / protected $commands = [ \App\Console\Commands\CountIpDay::class, ];在 schedule 方法中设置定时任务,执行时间为每天凌晨一点/* * Define the application’s command schedule. * * @param \Illuminate\Console\Scheduling\Schedule $schedule * @return void / protected function schedule(Schedule $schedule) { $schedule->command(‘countIp:day’)->dailyAt(‘1:00’); }最后是在 Linux 中添加定时任务,每分钟执行一次 artisan schedule:run,如下 * * * * /you_php you_path/artisan schedule:run >> /dev/null 2>&1参考资料:laravel 任务调度、Laravel定时任务调度例子——统计每周新增的用户数量 ...

April 12, 2019 · 2 min · jiezi

laravel 使用 Faker 数据填充

导语做开发的时候,添加测试数据是必不可少的,laravel 内置了很方便的数据填充,下面是实例。数据迁移先创建数据模型和数据迁移 php artisan make:model Models/FakerUser -m;只创建几个简单字段,编辑 database/migrations/{now_date}_create_faker_users_table.php 文件/** * Run the migrations. * * @return void / public function up() { Schema::create(‘faker_users’, function (Blueprint $table) { $table->increments(‘id’); $table->char(’name’, 20)->comment(‘姓名’); $table->string(’email’, 50)->comment(‘邮箱’); $table->tinyInteger(‘age’)->comment(‘年龄’); $table->char(‘city’, 20)->comment(‘城市’); $table->timestamps(); }); DB::statement(“ALTER TABLE faker_users comment’测试用户表’”); // 表注释 }运行数据迁移 php artisan migrate 之后数据表创建完成。数据填充创建数据填充文件 php artisan make:seeder FakerUsersSeeder;创建完成后,我们可以在 run() 方法中手动添加几条测试数据。但是好的办法,是使用模型工厂,接下来把注意力转移到模型工厂中;创建模型工厂 php artisan make:factory FakerUsersFactory;在模型工厂中,可以通过 Faker\Generator 来生成测试数据,编辑 database/factories/FakerUsersFactory.php<?phpuse Faker\Generator as Faker;$factory->define(\App\Models\FakerUser::class, function (Faker $faker) { return [ ’name’ => $faker->name, ’email’ => $faker->safeEmail, ‘age’ => $faker->numberBetween(8, 80),// 数字在 8-80 之间随机 ‘city’ => $faker->city, ‘created_at’ => $faker->dateTimeBetween(’-3 year’, ‘-1 year’),// 时间在 三年到一年 之间 ‘updated_at’ => $faker->dateTimeBetween(’-1 year’, ‘-5 month’),// 时间在 一年到五个月之间 ];});由上述代码可以很直白的看出 Faker\Generator 的作用。它可以生成的数据类型有很多,更多的类型可以看下官方文档,虽然是英文的,不过都有示例,简单易懂;Faker 生成的数据默认是英文,可以在 config/app.php 中将 faker_locale 设置为 zh_CN;模型工厂写好了,接下来就是调用。目光回到数据填充文件 database/seeds/FakerUsersSeeder.php,在 run() 方法中如下代码/* * Run the database seeds. * * @return void */ public function run() { factory(\App\Models\FakerUser::class)->times(1000)->make()->each(function ($model) { // 数据入库 $model->save(); }); }time() 是生成的次数,make() 方法是创建模型实例,在 each() 方法中将生成的模型实例入库保存。最后就是执行数据填充,composer dump-autoload 之后 php artisan db:seed –class=FakerUsersSeeder测试好了,看下数据库的数据是否生成正确。看下总数总数没有问题,随机看十条数据数据也是正确的。参考资料:数据填充、Laravel 文档-数据库测试、Faker。 ...

April 11, 2019 · 1 min · jiezi

laravel 配置 Redis 多个库

导语经过编译安装和安装扩展之后,Redis 已经可以正常使用了。但是在 laravel 中还需要其他的操作。安装扩展要想在 laravel 中使用 Redis,还需要安装 predis 扩展。使用 composer require predis/predis 进行安装就可以了。修改配置Redis 的配置在 config/database.php 文件,根据需求修改,我这里不需要改动;设置 Cache 默认缓存为 Redis,在 .evn 文件中 CACHE_DRIVER=redis;设置 Session 的驱动为 Redis,在 .env 文件中 SESSION_DRIVER=redis;配置多个库经过以上的配置后,多个服务都使用 Redis,如果都使用同一个库,这显然是不合理的。我们可以配置多个连接来解决这个问题。Redis 默认有 16 个库,在服务器中设置 redis.conf 的 database 值可以修改。先来看下 config/database.php 的默认连接’redis’ => [ ‘client’ => ‘predis’, ‘default’ => [ ‘host’ => env(‘REDIS_HOST’, ‘127.0.0.1’), ‘password’ => env(‘REDIS_PASSWORD’, null), ‘port’ => env(‘REDIS_PORT’, 6379), ‘database’ => env(‘REDIS_DB’, 0), ], ‘cache’ => [ ‘host’ => env(‘REDIS_HOST’, ‘127.0.0.1’), ‘password’ => env(‘REDIS_PASSWORD’, null), ‘port’ => env(‘REDIS_PORT’, 6379), ‘database’ => env(‘REDIS_CACHE_DB’, 1), ], ],默认是有两个连接的,分别是 default 和 cache。下面来看下 config/cache.php 中关于 Redis 的配置’redis’ => [ ‘driver’ => ‘redis’, ‘connection’ => ‘cache’, ],可以看到它的 connection 值是 cache,也就是使用 config/database.php 中 Redis 的 cache。下面修改 config/database.php 的 Redis,添加一个 session 的连接,如下’redis’ => [ ‘client’ => ‘predis’, ‘default’ => [ ‘host’ => env(‘REDIS_HOST’, ‘127.0.0.1’), ‘password’ => env(‘REDIS_PASSWORD’, null), ‘port’ => env(‘REDIS_PORT’, 6379), ‘database’ => env(‘REDIS_DB’, 0), ], ‘cache’ => [ ‘host’ => env(‘REDIS_HOST’, ‘127.0.0.1’), ‘password’ => env(‘REDIS_PASSWORD’, null), ‘port’ => env(‘REDIS_PORT’, 6379), ‘database’ => env(‘REDIS_CACHE_DB’, 1), ], ‘session’ => [ ‘host’ => env(‘REDIS_HOST’, ‘127.0.0.1’), ‘password’ => env(‘REDIS_PASSWORD’, null), ‘port’ => env(‘REDIS_PORT’, 6379), ‘database’ => env(‘REDIS_SESSION_DB’, 2), ], ],接下来在 .env 中添加 SESSION_CONNECTION=session。测试经过上面的操作,已经修改好了。总结下就是 default 使用的是 0 库,cache 使用的是 1 库,session 使用的是 2 库。使用如下代码来测试下/** * 测试 Redis 的存储 */ public function testRedis() { // Redis 门面 Redis::setex(‘facades’, 30, ‘i am facades’); // Cache Cache::put(‘cache’, ‘i am cache’, now()->addMinute(30)); // 因为 Cache 默认是 Redis,所有和上面语句相同 // Cache::store(‘redis’)->put(‘cache’, now(), now()->addMinute(30)); }运行以上代码之后,在服务器中使用 redis-cli 来看下存储情况可以看到各个库的存储情况使用 Redis 门面操作,默认为 config/database.php 中 Redis 的 default 连接,数据存入 0 库;使用 Cache 操作,因为 config/cache.php 中 Redis 的 connection 设置为 cache,理所当然存入的是 1 库;Session 也根据 SESSION_CONNECTION=session 配置,正确的存入了 2 库;使用 Redis 门面的时候,也可以指定连接$redis = Redis::connection(‘session’);$redis->setex(‘facades_connection’, 30, ‘i am facades_connection’);参考资料:Laravel Redis 文档。 ...

April 10, 2019 · 2 min · jiezi

关于七牛云正确使用姿势探索

业务场景需求我们项目有一个文件上传需求,需要从客户端上传到七牛云的对象存储和自己的应用服务器上。这里使用七牛云主要是实现下载分发。应用服务器需要留一份是因为后续需要做文件分析(并且是上传后需要立马分析出结果展现给客户端)。另外,由于是初期项目,暂时没考虑用独立服务器来分析。所用技术栈服务器:Centos7开发语言:PHP框架:Laravel前端上传组件:百度的WebUploader解决方案准确的说我经过了三个阶段才真正完美的实现了需求(主要解决上传速度)。一期解决方案及细节初期面对需求很容易想到的思路是:客户端先上传文件到应用服务器(因为上传完成可以及时做分析),然后再上传到七牛云上。所以我的解决方案是:前端用webuploader,后端的七牛云文件处理方面使用了Laravel的一个插件:overtrue/flysystem-qiniu (https://github.com/overtrue/f…,该插件的接口很简洁好用(但是有坑,后面会说到)。然后为了解决性能问题,我还做了以下工作:1,使用分片上传2,后续上传七牛云使用异步的方式(因为文件上传到其他应用来下载这个文件,中间有许多时间来让上传任务的完成)关于分片上传这里讲下分片上传的实现思路,客户端主要是把大文件按一定size进行分片,然后上传到服务器,所以会有多个请求,并且每个请求还需带上关键的信息:当前chunk(从0开始)和chunks(总分片数)。由于我用的是webuploader组件,所以客户端不用自己做什么,只需配置下简单信息(是否分片及分片大小)。服务端处理逻辑为:客户端一个请求过来,分两种情况:1,文件总size小于要分片的size,这时候直接处理文件。2,处理分片情况。具体逻辑是判断chunk和chunks,如果相等说明为第一种情况,直接处理上传,其他走处理分片逻辑。处理分片的逻辑为:保存当前分片到临时目录(按分片命名),然后判断当所有分片完成时,就合并文件。具体逻辑是判断 chunk + 1 是否等于chunks。 合并逻辑就是循环读取临时文件,然后写入到一个新的文件(合并后的),这里可以顺便删除临时文件。所遇的坑:这里处理碎片文件时,当初图方便使用了Laravel的文件处理接口Storage::append,但是这个接口有个坑就是它自作主张的文件结尾加入换行符。导致合并后的文件还原不成原始文件。解决办法是老老实实使用php的fopen、fwrite、fclose这一套。关于PHP异步处理关于PHP的异步实现可以参考鸟哥写的文章:http://www.laruence.com/2008/…主要方法为:客户端AJAX、popen函数、curl、fsocketopen等不过这篇文章比较老了,局限性也大,现在有了协程等处理方案(现在Swoole也提供协程方案了,并且client-server task分发这种也可以用swoole的),而且往架构方面考虑可以使用队列等(感觉靠谱的还是队列)。PS: 我这里前期用的是简单粗暴的popen,后来使用的是Laravel提供的队列。一期方案的问题通过上述所说的方案,很容易就实现了一个版本。但是没高兴多久。。,在后续测试时遇到一个诡异bug,当文件过大时,任务脚本上传到七牛云失败。这里脚本是写在Laravel的artisan中的,当我把脚本命令直接在终端调试时也是没有任何异常(准确讲是看不了任何异常)。前面我说过七牛这块SDK用的是overtrue/flysystem-qiniu ,并且为了考虑性能问题用的是他的writeStream接口。 $disk = Storage::disk(‘qiniu’); $stream = fopen($localFileName, ‘r’); $disk->writeStream($fileName, $stream); if (is_resource($stream)) { fclose($stream); }代码表面上看起来很理想,用的是文件流上传(怕吃内存)。但结果证明一切只是表面上的。。当我遇到大文件无法上传到七牛云时,断点调试到$disk->writeStream这里,发现返回的是false。 继而调试到overtrue/flysystem-qiniu这个扩展的源代码。然后发现了一个大坑。。主要是两个问题:1,writeStream只是个假的流写入具体源码在扩展的QiniuAdapter.php文件中,这里贴段代码:public function writeStream($path, $resource, Config $config){ $contents = ‘’; while (!feof($resource)) { $contents .= fread($resource, 1024); } $response = $this->write($path, $contents, $config); if (false === $response) { return $response; } return compact(‘path’);}注意这里的$contents变量,最终还是等价于一个大文件内容的大小(服务器为此变量开辟的内存)。并且后续还要在方法间传递。所以这里是假的流!2,接口对调试不友好还有在write方法中,屏蔽了$error,只返回false,这样不便于我们查问题,最终我是断点打印这个$error才知道报的错误是:“invalid multipart format: multipart: message too large”,这个应该是七牛那边真正返回的,但这么重要的信息被这个扩展屏蔽了。二期解决方案知道了一期方案的具体问题所在,我就一直在思考(那个扩展就不提了。。我现在怀疑它的存在意义。。),甚至在想也许一开始整个思路就错了(通过SDK上传文件的方案)。后来还真被我找到了,七牛云官方提供一个脚本工具:Qshell(https://github.com/qiniu/qshell)。这个是命令行运行脚本,具体操作看文档就可以了。放到我的项目也是集成到七牛的任务脚本中。后来测试可以了,整个流程可以跑通。但是无意中发现二期的重要问题,这个上传走的是服务器的上行带宽!而我们平常付费买的带宽就是买的上行带宽!(下行是一般是免费的)。这还怎么搞!由于我们上传业务是商户端使用的,平时使用频次也不会太少,这会导致在上传时影响前端网站的访问速度。这里具体讲下服务器带宽问题(网上查询后整理的):首先对服务器带宽方向的描述一般是用上行和下行,上传和下载是指动作。上行是指从服务器流出的带宽,如果是在其他机器下载服务器上的文件,用的主要是服务器的上行带宽(这里说下我们平时的网页浏览,其实也是不同客户端从服务器下数据, html文件、css等然后渲染,所以网页浏览占用的也是上行带宽)。下行是指流入到服务器的带宽,如果是在其他机器上传文件到服务器,比如用FTP上传文件,用的主要是服务器的下行带宽(服务器上下载文件用的也是下行带宽)。现在的云提供商比如阿里云不限制的是下行带宽,大部分服务器的使用环境,都是上行带宽用的多,下行带宽用的少。通过对带宽的理解,再回到我们项目的上传实现思路,可以看到一开始就错了(不该用应用服务器作为中转)!三期(最终)方案当初为了节省时间,直接跳过官方文档,而使用第三方扩展。 现在看来,不得不又回到官方文档了。通过把七牛的文档过一遍,发现是有方案可以避开那个占用服务器上行带宽的问题的。主体思路是要避开应用服务器上行带宽的使用,因为上行带宽很宝贵,尽量使用下行带宽(免费、速度很快!阿里的大概60M多每秒)。具体实现是通过七牛的表单上传方案直接把客户端的文件先上传到七牛(这一步根本不关应用服务器什么事,所以避开了,而且直接上传到七牛的速度非常快,基本只取决于用户端的网速,而且对于一般需求,七牛提供了对于到我们应用服务器的回调方法)。然后由于我们应用服务器也需要文件,所以方案是直接在我们应用服务器直接下载七牛的文件(这里可以同步阻塞住,前端做个等待效果解决用户体验问题)。因为前面说到流入到服务器占用的是下行带宽。所以这里速度也会非常快(而且是免费的^_^)。这种方案基本是完美的了。总结首先是对个人的反省,前期调研不充足,但是项目初期有点紧,这里也说明投入时间的重要性。其次关于项目经验:上传第三方云存储,千万不要使用应用服务器做中转!可以直接上传到第三方云服务器,如果有后续处理逻辑的,可以使用他们的回调接口。

April 10, 2019 · 1 min · jiezi

详解 Laravel 中的依赖注入和 IoC

文章转自:https://learnku.com/laravel/t…作为开发者,我们一直在尝试通过使用设计模式和尝试新的健壮型框架来寻找新的方式来编写设计良好且健壮的代码。在本篇文章中,我们将通过 Laravel 的 IoC 组件探索依赖注入设计模式,并了解它如何改进我们的设计。依赖注入依赖注入一词是由 Martin Fowler 提出的术语,它是将组件注入到应用程序中的一种行为。就像 Ward Cunningham 说的:依赖注入是敏捷架构中关键元素。让我们看一个例子:class UserProvider{ protected $connection; public function __construct(){ $this->connection = new Connection; } public function retrieveByCredentials( array $credentials ){ $user = $this->connection ->where( ’email’, $credentials[’email’]) ->where( ‘password’, $credentials[‘password’]) ->first(); return $user; }}如果你要测试或者维护这个类,你必须访问数据库的实例来进行一些查询。为了避免必须这样做,你可以将此类与其他类进行 解耦 ,你有三个选项之一,可以将 Connection 类注入而不需要直接使用它。将组件注入类时,可以使用以下三个选项之一:构造方法注入class UserProvider{ protected $connection; public function __construct( Connection $con ){ $this->connection = $con; } …Setter 方法注入同样,我们也可以使用 Setter 方法注入依赖关系:class UserProvider{ protected $connection; public function __construct(){ … } public function setConnection( Connection $con ){ $this->connection = $con; } …接口注入interface ConnectionInjector{ public function injectConnection( Connection $con );}class UserProvider implements ConnectionInjector{ protected $connection; public function __construct(){ … } public function injectConnection( Connection $con ){ $this->connection = $con; }}当一个类实现了我们的接口时,我们定义了 injectConnection 方法来解决依赖关系。优势现在,当测试我们的类时,我们可以模拟依赖类并将其作为参数传递。每个类必须专注于一个特定的任务,而不应该关心解决它们的依赖性。这样,你将拥有一个更专注和可维护的应用程序。如果你想了解更多关于 DI 的信息,Alejandro Gervassio 在 本系列 文章中对其进行了广泛而专业的介绍,所以一定要去读这些文章。那么,什么又是 IoC 呢?IoC (控制反转)不需要使用依赖注入,但它可以帮助你有效的管理依赖关系。控制反转Ioc 是一个简单的组件,可以更加方便地解析依赖项。你可以将对象形容为容器,并且每次解析类时,都会自动注入依赖项。Laravel Ioc当你请求一个对象时, Laravel Ioc 在解决依赖关系的方式上有些特殊:我们使用一个简单的例子,将在本文中改进它。SimpleAuth 类依赖于 FileSessionStorage ,所以我们的代码可能是这样的:class FileSessionStorage{ public function __construct(){ session_start(); } public function get( $key ){ return $_SESSION[$key]; } public function set( $key, $value ){ $_SESSION[$key] = $value; }}class SimpleAuth{ protected $session; public function __construct(){ $this->session = new FileSessionStorage; }}//创建一个 SimpleAuth$auth = new SimpleAuth();这是一种经典的方法,让我们从使用构造函数注入开始。class SimpleAuth{ protected $session; public function __construct( FileSessionStorage $session ){ $this->session = $session; }}现在我们创建一个对象:$auth = new SimpleAuth( new FileSessionStorage() );现在我想使用 Laravel Ioc 来管理这一切。因为 Application 类继承自 Container 类,所以你可以通过 App 门面来访问容器。App::bind( ‘FileSessionStorage’, function(){ return new FileSessionStorage;});bind 方法第一个参数是要绑定到容器的唯一 ID ,第二个参数是一个回调函数每当执行 FileSessionStorage 类时执行,我们还可以传递一个表示类名的字符串,如下所示。Note: 如果你查看 Laravel 包时,你将看到绑定有时会分组,比如( view, view.finder……)。假设我们将会话存储转换为 Mysql 存储,我们的类应该类似于:class MysqlSessionStorage{ public function __construct(){ //… } public function get($key){ // do something } public function set( $key, $value ){ // do something }}现在我们已经更改了依赖项,我们还需要更改 SimpleAuth 构造函数,并将新对象绑定到容器中!高级模块不应该依赖于低级模块,两者都应该依赖于抽象对象。抽象不应该依赖于细节,细节应该取决于抽象。Robert C. Martin我们的 SimpleAuth 类不应该关心我们的存储是如何完成的,相反它更应该关注于消费的服务。因此,我们可以抽象实现我们的存储:interface SessionStorage{ public function get( $key ); public function set( $key, $value );}这样我们就可以实现并请求 SessionStorage 接口的实例:class FileSessionStorage implements SessionStorage{ public function __construct(){ //… } public function get( $key ){ //… } public function set( $key, $value ){ //… }}class MysqlSessionStorage implements SessionStorage{ public function __construct(){ //… } public function get( $key ){ //… } public function set( $key, $value ){ //… }}class SimpleAuth{ protected $session; public function __construct( SessionStorage $session ){ $this->session = $session; }}如果我们使用 App::make(‘SimpleAuth’) 通过容器解析 SimpleAuth类,容器将会抛出 BindingResolutionException ,尝试从绑定解析类之后,返回到反射方法并解析所有依赖项。Uncaught exception ‘Illuminate\Container\BindingResolutionException’ with message ‘Target [SessionStorage] is not instantiable.‘容器正试图将接口实例化。我们可以为该接口做一个具体的绑定。App:bind( ‘SessionStorage’, ‘MysqlSessionStorage’ );现在每次我们尝试从容器解析该接口时,我们会得到一个 MysqlSessionStorage 实例。如果我们想要切换我们的存储服务,我们只要变更一下这个绑定。Note: 如果你想要查看一个类是否已经在容器中被绑定,你可以使用 App::bound(‘ClassName’) ,或者可以使用 App::bindIf(‘ClassName’) 来注册一个还未被注册过的绑定。Laravel Ioc 也提供 App::singleton(‘ClassName’, ‘resolver’) 来处理单例的绑定。你也可以使用 App::instance(‘ClassName’, ‘instance’) 来创建单例的绑定。如果容器不能解析依赖项就会抛出 ReflectionException ,但是我们可以使用 App::resolvingAny(Closure) 方法以回调函数的形式来解析任何指定的类型。Note: 如果你为某个类型已经注册了一个解析方式 resolvingAny 方法仍然会被调用,但它会直接返回 bind 方法的返回值。小贴士这些绑定写在哪儿:如果只是一个小型应用你可以写在一个全局的起始文件 global/start.php 中,但如果项目变得越来越庞大就有必要使用 Service Provider 。测试:当需要快速简易的测试可以考虑使用 php artisan tinker ,它十分强大,且能帮你提升你的 Laravel 测试流程。Reflection API:PHP 的 Reflection API 是非常强大的,如果你想要深入 Laravel Ioc 你需要熟悉 Reflection API ,可以先看下这个 教程 来获得更多的信息。最后和往常一样,学习或者了解某些东西最好的方法就是查看源代码。Laravel Ioc 仅仅只是一个文件,不会花费你太多时间来完成所有功能。你想了解更多关于 Laravel Ioc 或者 Ioc 的一般情况吗?那请告诉我们吧!文章转自:https://learnku.com/laravel/t… 更多文章:https://learnku.com/laravel/c… ...

April 9, 2019 · 2 min · jiezi

基于Laravel的轻量级CMS系统及通用管理后台

项目简介lightCMS是一个轻量级的CMS系统,也可以作为一个通用的后台管理框架使用。lightCMS集成了用户管理、权限管理、日志管理、菜单管理等后台管理框架的通用功能,同时也提供模型管理、分类管理等CMS系统中常用的功能。lightCMS的代码一键生成功能可以快速对特定模型生成增删改查代码,极大提高开发效率。lightCMS基于Laravel 5.5开发,前端框架基于layui。功能点一览后台:基于RBAC的权限管理管理员、日志、菜单管理分类管理配置管理后台可自定义业务模型,方便垂直行业快速开发基于Tire算法的敏感词过滤系统(待完善)普通模型增删改查代码一键生成前台:用户注册登录(包括微信、QQ、微博三方登录)更多功能待你发现~项目地址:https://github.com/eddy8/ligh…

April 9, 2019 · 1 min · jiezi

Laravel 5.5 升级到 5.5.42 后遇到的 Cookie 序列化问题

最近手残升级了项目里 Laravel 的小版本号(v5.5.39 => v5.5.45),这不升级则已,一升级就出了问题!Sentry 平台上提示错误:openssl_encrypt() expects parameter 1 to be string, array given,具体报错记录如下:ErrorExceptionopenssl_encrypt() expects parameter 1 to be string, array givenvendor/laravel/framework/src/Illuminate/Encryption/Encrypter.php in handleError at line 91vendor/sentry/sentry/lib/Raven/Breadcrumbs/ErrorHandler.php in handleError at line 34vendor/sentry/sentry/lib/Raven/Breadcrumbs/ErrorHandler.php in openssl_encryptvendor/laravel/framework/src/Illuminate/Encryption/Encrypter.php in encrypt at line 91vendor/laravel/framework/src/Illuminate/Cookie/Middleware/EncryptCookies.php in encrypt at line 139vendor/laravel/framework/src/Illuminate/Cookie/Middleware/EncryptCookies.php in handle at line 66vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php in Illuminate\Pipeline{closure} at line 149vendor/laravel/framework/src/Illuminate/Routing/Pipeline.php in Illuminate\Routing{closure} at line 53vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php in then at line 102vendor/laravel/framework/src/Illuminate/Routing/Router.php in runRouteWithinStack at line 660vendor/laravel/framework/src/Illuminate/Routing/Router.php in runRoute at line 635vendor/laravel/framework/src/Illuminate/Routing/Router.php in dispatchToRoute at line 601vendor/laravel/framework/src/Illuminate/Routing/Router.php in dispatch at line 590vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php in Illuminate\Foundation\Http{closure} at line 176vendor/laravel/framework/src/Illuminate/Routing/Pipeline.php in Illuminate\Routing{closure} at line 30vendor/barryvdh/laravel-debugbar/src/Middleware/InjectDebugbar.php in handle at line 58vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php in Illuminate\Pipeline{closure} at line 149vendor/laravel/framework/src/Illuminate/Routing/Pipeline.php in Illuminate\Routing{closure} at line 53vendor/fideloper/proxy/src/TrustProxies.php in handle at line 56vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php in Illuminate\Pipeline{closure} at line 149vendor/laravel/framework/src/Illuminate/Routing/Pipeline.php in Illuminate\Routing{closure} at line 53vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php in handle at line 30vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php in Illuminate\Pipeline{closure} at line 149vendor/laravel/framework/src/Illuminate/Routing/Pipeline.php in Illuminate\Routing{closure} at line 53vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php in handle at line 30vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php in Illuminate\Pipeline{closure} at line 149vendor/laravel/framework/src/Illuminate/Routing/Pipeline.php in Illuminate\Routing{closure} at line 53vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/ValidatePostSize.php in handle at line 27vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php in Illuminate\Pipeline{closure} at line 149vendor/laravel/framework/src/Illuminate/Routing/Pipeline.php in Illuminate\Routing{closure} at line 53vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/CheckForMaintenanceMode.php in handle at line 46vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php in Illuminate\Pipeline{closure} at line 149vendor/laravel/framework/src/Illuminate/Routing/Pipeline.php in Illuminate\Routing{closure} at line 53vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php in then at line 102vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php in sendRequestThroughRouter at line 151vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php in handle at line 116public/index.php at line 55仔细查看上面的异常堆栈记录,并且进行断点调试,最终确定是由于 Laravel 5.5 升级小版本后 Cookie 加密的逻辑变动所导致的报错。查阅 Laravel 官方文档(Laravel 5.5 Upgrade Guide)后得知,Laravel 新版为了防止 PHP 对象的序列化/反序列化漏洞被利用,不再对 Cookie 值进行自动的序列化和反序列化处理。举个栗子:\Cookie::queue(‘user’, [‘id’ => 1, ’name’ => ‘admin’], 720, ‘/’)Laravel 更新到 v5.5.42 后,因为 Laravel 不再自动对 Cookie 值 [‘id’ => 1, ’name’ => ‘admin’] 进行序列化处理,而 openssl_encrypt ( string $data … ) 只能加密字符串数据,这个时候程序就会抛出错误:openssl_encrypt() expects parameter 1 to be string, array given。解决方法:新版里面在中间件 AppHttpMiddlewareEncryptCookies 新增静态属性 $serialize,当设置为 true 时可开启 Cookie 值的自动序列化和反序列化处理。/** * Indicates if cookies should be serialized. * * @var bool */protected static $serialize = true;【推荐】将 Cookie 值使用 JSON 函数编码成字符串后再进行存储(获取 Cookie 值后需调用 JSON 函数进行解码)。\Cookie::queue(‘user’, json_encode([‘id’ => 1, ’name’ => ‘admin’]), 720, ‘/’);-EOF-首发于知乎专栏《PHP和Laravel学习》:https://zhuanlan.zhihu.com/p/…扫码关注《PHP和Laravel学习》微信公众号: ...

April 9, 2019 · 2 min · jiezi

phpstorm 基于lumen安装ide-helper

为了提高开发效率,也方便在model 中生成更多的属性和方法,尝试下ide-helper 安装composer require barryvdh/laravel-ide-helperUsing version ^2.6 for barryvdh/laravel-ide-helper./composer.json has been updatedLoading composer repositories with package informationUpdating dependencies (including require-dev)Package operations: 11 installs, 0 updates, 0 removals - Installing composer/xdebug-handler (1.3.2): Loading from cache - Installing composer/spdx-licenses (1.5.1): Loading from cache - Installing symfony/filesystem (v4.2.5): Loading from cache - Installing justinrainbow/json-schema (5.2.8): Loading from cache - Installing seld/phar-utils (1.0.1): Loading from cache - Installing seld/jsonlint (1.7.1): Loading from cache - Installing composer/semver (1.5.0): Loading from cache - Installing composer/ca-bundle (1.1.4): Loading from cache - Installing composer/composer (1.8.4): Loading from cache - Installing barryvdh/reflection-docblock (v2.0.6): Loading from cache - Installing barryvdh/laravel-ide-helper (v2.6.2): Loading from cachebarryvdh/reflection-docblock suggests installing dflydev/markdown (~1.0)barryvdh/laravel-ide-helper suggests installing doctrine/dbal (Load information from the database about models for phpdocs (~2.3))Writing lock fileGenerating optimized autoload files配置中加载(bootstrap/app.php) //ide helper $app->register(Barryvdh\LaravelIdeHelper\IdeHelperServiceProvider::class);自动生成php artisan ide-helper:generate报错google 了下,应该是未安装这个扩展,安装composer require league/flysystem再执行 ,搞定php artisan ide-helper:generateA new helper file was written to _ide_helper.php最好把生成的这些文件都放到git 忽略文件里_ide_helper.php_ide_helper_models.php.phpstorm.meta.php参考:https://laravelacademy.org/po…https://github.com/barryvdh/l… ...

April 8, 2019 · 1 min · jiezi

laravel 事件/监听器 实例

导语上一篇文章实现了记录用户访问,设计上是有缺陷的,代码紧耦合在中间件。如果后续修改需求,不仅记录 ip、城市,还需要记录数据到新的数据表,或者需要进行其它统计,那么不停的增加、修改代码是不合理的。这个时候可以使用 Laravel 的事件/监听器进行处理。代码可查看 GitHub。事件/监听器Laravel 事件提供了简单的观察者模式实现,允许你订阅和监听应用中的事件。观察者模式有时也被称作发布/订阅模式,该模式用于为对象实现发布/订阅功能:一旦主体对象状态发生改变,与之关联的观察者对象会收到通知,并进行相应操作。以上是事件/监听器、观察者模式的简要说明。结合这次的需求理解,当触发用户访问事件,它的观察者进行处理。观察者可以是多个,本例仅做入库操作。创建事件/监听器在 app/Providers/EventServiceProvider.php 文件中添加事件/监听器,如下 /** * The event listener mappings for the application. * * @var array / protected $listen = [ Registered::class => [ SendEmailVerificationNotification::class, ], ‘App\Events\UserBrowse’ => [ ‘App\Listeners\CreateBrowseLog’, // 其它监听器 ], ];添加好之后,执行 php artisan event:generate,会自动创建对应的事件/监听器。分别创建了 app/Events/UserBrowse.php 和 app/Listeners/CreateBrowseLog.php 两个文件。实现代码把目光聚集到事件 app/Events/UserBrowse.php 文件,这里需要接收数据以便后续处理,代码如下 public $ip_addr; public $request_url; public $city_name; /* * Create a new event instance. * * @return void / public function __construct($ip_addr, $request_url, $city_name) { $this->ip_addr = $ip_addr; $this->request_url = $request_url; $this->city_name = $city_name; }然后是监听器 app/Listeners/CreateBrowseLog.php,这里要做的是,将事件中接收到的数据进行入库操作,代码如下/* * Handle the event. * * @param UserBrowse $event * @return void / public function handle(UserBrowse $event) { $log = new \App\Models\BrowseLog(); $log->ip_addr = $event->ip_addr; $log->request_url = $event->request_url; $log->city_name = $event->city_name; $log->save(); }分发事件最后就是分发事件,修改 app/Http/Middleware/BrowseLog.php 中间件的代码,修改后如下/* * Handle an incoming request. * * @param \Illuminate\Http\Request $request * @param \Closure $next * @return mixed */ public function handle($request, Closure $next) { // 使用事件/监听器入库 event(new UserBrowse($request->getClientIp(), $request->path(), get_city_by_ip(false, ’null’))); return $next($request); }测试之后是没有问题的。结语这次所做的修改,感官上来看,就是将入库操作从中间件转移到监听器中,实际上的意义远不止于此。例如同一个事件,可以分发在不同的地方;事件添加了需求,只需要在添加一个监听器即可;监听器中也可以使用队列等等。参考资料:事件、观察者模式 。 ...

April 7, 2019 · 1 min · jiezi

基于Laravel5.8支持Markdown的开源博客VienBlog

laravel-blogVien Blog - 一款基于laravel5.8开发的,支持markdown编辑以及图片拖拽上传的博客系统、SEO友好博主网站VienBlog这里有些小秘密博客亮点界面简洁、适配pc和mobile、有良好的视觉体验支持markdown、并且可以拖拽或者粘贴上传图片、分屏实时预览SEO友好:支持自定义文章slug、支持meta title、description、keywords自定义导航、自定义sidebar、随时去掉不需要的模块支持标签、分类、置顶、分享、友链等博客基本属性支持AdSense支持百度自动提交链接和手动提交链接博客展示Demo演示地址: 这是一个DEMO后台管理文章列表主要操作有创作、编辑、置顶、删除(软删除)创作和编辑创作和编辑页面Markdown编辑器:支持拖拽粘贴上传图片、预览、全屏、分屏预览前端展示参照 这是一个DEMO看完Demo,如果你觉得还过得去,想要用一用试试呢,赶紧往下看喔。使用博客安装获取源码git clone git@github.com:luvvien/laravel-blog.git进入项目目录后,用composer安装依赖composer install生成.env文件cp .env.example .env创建数据库vienblog ,字符集采用 utf8mb4, utf8mb4_general_ci编辑.env文件 vim .env,修改MySQL数据库连接配置,请将DB_HOST,DB_PORT,DB_USERNAME,DB_PASSWORD 改成你的数据库配置。[…]DB_CONNECTION=mysqlDB_HOST=127.0.0.1DB_PORT=3306DB_DATABASE=vienblogDB_USERNAME=rootDB_PASSWORD=root[…]数据迁移和数据填充php artisan migratephp artisan db:seed创建storage软连接php artisan storage:link设置目录权限chmod -R 755 storage/chown -R www-data:www-data storage/使用可以选择临时预览,也可以用Nginx部署服务临时预览php artisan serv打开浏览器访问127.0.0.1:8000使用NginxNginx配置,将root指向项目的public目录,请用pwd 查看目录,并且改成你目录,千万不要直接粘贴复制。root /app/laravel-blog/public;完整配置server { listen 8088 default_server; listen [::]:8088 default_server; root /apps/vien_blog/public; index index.php index.html index.htm; server_name _; location / { try_files $uri $uri/ /index.php?$query_string; } location ~ .php$ { include snippets/fastcgi-php.conf; fastcgi_pass unix:/run/php/php7.2-fpm.sock; # fpm,因为版本不同路径会有区别,这里请改成你,不知道路径可以执行php-fpm便会显示 # fastcgi_pass 127.0.0.1:9000; # cgi }}打开浏览器访问127.0.0.1:8088后台登录地址/admin默认的admin管理账号是vien@byteinf.com密码是vienblog,进入控制台后可以修改管理员信息使用百度自动推送和主动推送请先在config/vienblog.php中按照注释配置相关的信息,自动推送是在网页访问时推送,主动推送执行以下代码会将未提交过的链接提交到百度php artisan push:baidu讨论群QQ群号:149347741 (欢迎开发者,技术爱好者,站长加入)联系我Email: support@vienblog.comLicense使用Vien Blog构建应用,必须在页脚保留Powered by Vien Blog字样以及相关链接在遵守以上规则的情况下,你可以享受等同于MIT License协议的授权。使用Vien Blog并且遵守上述协议的用户可以享受Vien Blog的博客导航,联系我将你的博客地址添加到Vien Blog的网站导航中。 ...

April 6, 2019 · 1 min · jiezi

laravel 自定义服务提供者

导语laravel 的服务提供者,是框架的核心,提供了路由、日志、缓存等功能。这里要实现的需求是使用第三方 API 获取天气情况,涉及到服务提供者、契约、依赖注入等方面。相关内容可以通过下方参考资料进行了解,本文内容不进行展开介绍,代码可查看 GitHub。创建服务提供者可以使用 artisan 快捷的创建服务提供者,执行 php artisan make:provider WeatherServiceProvider 即在 app/Providers 目录下创建了 WeatherServiceProvider.php 文件;在 config/app.php 中注册服务提供者,在 providers 数组中将创建的服务提供者 App\Providers\WeatherServiceProvider::class, 写入,如下创建契约在 app 目录下新建 Contracts 目录用以存放契约文件;在 app/Contracts 目录下创建契约,即 Weather.php 接口文件。在接口中只定义了 public function getWeather($cityName); 一个方法用于获取天气信息;实现契约在 app 目录下新建 Service/Weather 目录用于存放实现 Weather.php 契约的文件;选择一个第三方的天气 API 来实现契约。这里使用的是心知天气。关于 API 的调用,可以查看文档;最终创建的文件是 app/Service/Weather/Xinzhi.php。继承了 Weather.php 接口文件,所有要实现 getWeather 方法,代码可查看 GitHub;这里先埋个伏笔。除了上面的 Xinzhi.php,另外选择和风天气实现契约,文件为 app/Service/Weather/Hefeng.php。代码查看 GitHub;服务提供者绑定我们已经实现了契约,接下来就是绑定具体实现类。回到开始创建的服务提供者,在 register 方法中添加如下代码$this->app->bind(‘App\Contracts\Weather’, function() { return new Xinzhi();});最后就可以正常使用了。新建路由,然后测试。试下通过依赖注入调用public function getWeather(Request $request, Weather $weather){ return $weather->getWeather($request->input(‘city’, ‘beijing’));}没有问题解耦完成上述所有步骤,这个需求已经实现了。看起来很麻烦是吧,完全可以封装一个函数,直接调用就可以了,没有必要自定义服务提供者、创建契约。实际上述步骤,其中的一个目的就是小标题那两个字——解耦。假设一下,我们需要在很多代码中使用这个功能,突然有一天,这个 API 挂了,怎么办?四处去查找、检查代码,然后再去修改,同时要注意参数、返回值等。光是听起来就很烦了。这个时候,如果我们的代码按照上述的步骤进行开发,解决方法就大不相同了。简而言之,一行代码就可以搞定。还记得上面那个伏笔吧,一共有两个实例实现了接口。将自定义的服务提供者 register 做如下修改$this->app->bind(‘App\Contracts\Weather’, function() { // return new Xinzhi(); return new Hefeng();});修改了契约的绑定,所有使用 Weather 契约进行依赖注入的实例,都会由 Xinzhi.php 实例切换到 Hefeng.php 实例。契约当然不止解耦这一个作用,代码更容易理解、更方便维护,甚至可以当做简明的开发文档。更多的深入理解,请查看下方参考资料。参考资料:底层原理 —— 服务提供者、底层原理 —— 契约(Contracts)、Laravel 服务容器实例教程 —— 深入理解控制反转(IoC)和依赖注入(DI)、 Laravel 从学徒到工匠系列 依赖注入篇。 ...

April 6, 2019 · 1 min · jiezi

laravel 调试工具 Debugbar 安装及使用

导语Debugbar 是用来调试的扩展包,可以在显示调试信息以及运行情况。代码可查看 GitHub。composer 安装以及配置输入 composer require barryvdh/laravel-debugbar 进行安装在 config/app.php 中注册服务 Barryvdh\Debugbar\ServiceProvider::class,,如下添加门面 ‘Debugbar’ => Barryvdh\Debugbar\Facade::class,,如下最后来生成配置文件 php artisan vendor:publish –provider=“Barryvdh\Debugbar\ServiceProvider”,根据需求进行修改。使用经过以上步骤,安装成功,如果 APP_DEBUG 是开启状态,就可以使用了。添加好路由之后,新建控制器来测试下来看下显示正常视图,Debugbar 是怎么显示的会显示当前路由、使用内存、加载时间、PHP 版本、Session 等等信息。现在试试使用 Debugbar 门面添加信息,使用文档中的示例修改下,代码如下Debugbar::info(new Deque(range(1, 10)));Debugbar::error(‘Error!’);Debugbar::warning(‘Watch out…’);Debugbar::addMessage(‘Another message’, ‘mylabel’);return view(‘qr’);添加的信息会在页面中显示出来其他的设置开始/中止时间、记录异常,不再测试了,可以查看下方参考资料。参考资料:Debugbar、Laravel 调试利器 —— Laravel Debugbar 扩展包安装及使用教程。

April 5, 2019 · 1 min · jiezi

Laravel 多域名下的字段验证

前言正在开发一个统一作者后台,用来让作者给网站提交软件。我们已经对其中一个网站开发了作者后台,现在我们打算将这一个后台提供给其他网站。它具备如下的一些特点:我们访问的域名是不一致的,解决方案见我的一篇文章,Laravel 路由研究之domain 解决多域名问题其次各个站点对后台的要求都是一致的,也就是说,一个后台N各站去用。功能拆分开始之前我们需要对系统各个功能点进行拆分,估算受影响的点:登录注册登录注册功能首当其冲,我们需要用户在注册时通过访问的域名不同,记录的身份也不同。所以我们需要进行如下的处理:增加字段identity进行判重进行登录验证数据处理这个就不进行讨论了。根据用户所属身份不同,调用的数据也不同就行了。注册判重判重依据:我们知道使用php artisan make:auth 后,默认使用email登录,在表单验证中默认对email进行判重。代码如下:默认表单验证:// Path:app/Http/Controllers/Auth/RegisterController.phpprotected function validator(array $data){ return Validator::make($data, [ ’name’ => [‘required’, ‘string’, ‘max:255’], ’email’ => [‘required’, ‘string’, ’email’, ‘max:255’, ‘unique:users’], ‘password’ => [‘required’, ‘string’, ‘min:8’, ‘confirmed’], ]);}默认登录验证字段// Path:vendor/laravel/framework/src/Illuminate/Foundation/Auth/AuthenticatesUsers.phppublic function username(){ return ’email’;}// 当然可以修改验证字段(看过文档的都知道),注意:登录验证字段必须是在表里面唯一的。现在我们需要分析我们的需求:在单一用户后台中,email判重已经足够了,但是对于多种用户一起使用就不太够了。假设:我们有A,B两个域名,对应a,b两种用户,我们需要在一张表中存储a,b,首先我们判断a,b是属于那个域名的(站点),其次,看这个用户是否重复。下面我们用Laravel表单验证来实现一下:增加字段:为方便演示,我直接在 make auth 生成的迁移文件上直接修改,大家不要在实际项目中直接修改,而是通过新建迁移文件,使用修改表结构的方式增加字段public function up(){ Schema::create(‘users’, function (Blueprint $table) { $table->bigIncrements(‘id’); $table->string(’name’); $table->string(’email’); // 去掉原来的unique $table->string(‘identity’); // 增加的字段 $table->timestamp(’email_verified_at’)->nullable(); $table->string(‘password’); $table->rememberToken(); $table->timestamps(); });}注意: 在这个需求中,我们对迁移文件中的email和name字段不需要进行unique限定,因为他们的唯一性是有依赖的,不是独立的。模拟用户注册,插入身份信息// Path: app/Http/Controllers/Auth/RegisterController.phpprotected function create(array $data){ return User::create([ ’name’ => $data[’name’], ’email’ => $data[’email’], ‘password’ => Hash::make($data[‘password’]), ‘identity’ => ‘pcsoft’, // 模拟用户注册时,插入身份字段值 ]);}进行判重处理protected function validator(array $data){ return Validator::make($data, [ ’name’ => [‘required’, ‘string’, ‘max:255’], ’email’ => [‘required’, ‘string’, ’email’, ‘max:255’, Rule::unique(‘users’)->where(function ($query) { $query->where(‘identity’, ‘=’, ‘onlinedown’); })], // 这句话的意思:按照什么条件对 users 表中的 email 去重,我们需要按照身份字段等于我们访问的域名对 email 去重, ‘password’ => [‘required’, ‘string’, ‘min:8’, ‘confirmed’], ]);}测试进行第一次注册,数据库截如下:进行第二次注册,相同邮件,不同身份:相同身份,相同邮箱测试登录验证覆写credentials,传入身份验证字段// Path:app/Http/Controllers/Auth/LoginController.phpprotected function credentials(Request $request){ $request->merge([‘identity’ => Controller::getWebPrefix()]); return $request->only($this->username(), ‘password’, ‘identity’);} ...

April 3, 2019 · 1 min · jiezi

laravel 使用扩展包生成 PDF

导语关于 PDF 的扩展包有不少,这次选择的是 DOMPDF,下面是具体操作。代码可查看 GitHub。composer 安装以及配置依然使用 composer 安装,根据文档进行即可执行 composer require barryvdh/laravel-dompdf接下来是注册服务,在 config/app.php 中添加 Barryvdh\DomPDF\ServiceProvider::class,,如下添加门面,同样是在 config/app.php 中添加 ‘PDF’ => Barryvdh\DomPDF\Facade::class,,如下经过以上三个步骤,可以正常使用了。为了修改配置方便,可以在 config 目录下生成配置文件,执行 php artisan vendor:publish –provider=“Barryvdh\DomPDF\ServiceProvider”,成功后可查看 config/dompdf.php 配置文件。根据自己的需求进行修改,也支持动态修改。使用定义好路由之后,新建控制器进行测试。根据官方文档,可以使用 App::make(‘dompdf.wrapper’) 或者 PDF 门面进行实例化,效果是一样的,使用门面注意 use PDF。使用文档中的第一个示例$pdf = App::make(‘dompdf.wrapper’);$pdf->loadHTML(’<h1>Test</h1>’);// 根据 HTML 代码生成 PDFreturn $pdf->stream();效果如下再来试下文档中的第二个示例$pdf = PDF::loadView(‘pdf’, [‘date’ => date(‘Y-m-d’)]);// 根据视图文件生成 PDFreturn $pdf->download(‘date.pdf’);// 参数为文件名打开链接后,可以下载名为 date.pdf 的文件,内容如下以上使用了 loadHTML() 和 loadView() 两种方法,分别是根据 HTML 代码和视图生成。使用 loadFile() 来试下$file = storage_path(‘app/public/pdf/name.html’);$pdf = PDF::loadFile($file);return $pdf->stream();也可以链式调用多个方法,下面的代码是根据视图生成 PDF,然后保存到指定路径,最后在进行展示return PDF::loadView(‘pdf’, [‘date’ => date(‘Y-m-d’)])->save(storage_path(‘app/public/pdf/date.pdf’))->stream(‘date.pdf’);参考资料:DOMPDF。

April 2, 2019 · 1 min · jiezi

Laravel 路由研究之domain 解决多域名问题

材料准备一份干净的laravel两份Nginx配置文件,主要配置如下:server_name *.amor_laravel_test_1.amor;root /var/www/amor_laravel_test/public;index index.php index.html index.htm;server_name *.amor_laravel_test.amor;root /var/www/amor_laravel_test/public;index index.php index.html index.htm;将域名分割为参数Route::domain(’{account}.{webname}.{suffix}’)->group(function () { Route::get(‘user/{id}’, function ($account, $webname, $suffix, $id) { // 可以在请求中接收到被分割的参数,可能的使用场景:在单独路由中需要根据不同的域名处理不同的需求 dd($account, $webname, $suffix, $id); });});注意: 若account不固定,可以将Nginx Server Name 配置为泛型: *.example.com关于多域名配置两个不同的域名如下:server_name *.amor_laravel_test.amor;server_name *.amor_laravel_test_1.amor;如何让Laravel匹配不同的域名?方式1:直接在 route/web.php中使用domain区分Route::domain(’{account}.amor_laravel_test.amor’)->group(function () { Route::get(‘user/{id}’, function ($account, $id) { // dd($account, $id); });});Route::domain(’{account}.amor_laravel_test_1.amor’)->group(function () { Route::get(‘user/{id}’, function ($account, $id) { // dd(111, $account, $id); });});方式2:通过设置 RouteServiceProvider 区分添加方法: protected function mapSelfRoutes() { Route::domain(’{account}.amor_laravel_test_1.amor’) ->middleware(‘web’) ->namespace($this->namespace) ->group(base_path(‘routes/self.php’)); }注册 public function map() { $this->mapApiRoutes(); $this->mapWebRoutes(); $this->mapSelfRoutes(); // }添加路由文件Route::get(’/user’, function ($account) { dd($account);});注意: 必须全部设置domain,如果只设置了self 那么在相同请求路径下,未设置domain的将会首先匹配到。总结:推荐第二种方式来区分域名,优点在于路由分离 ,结构清晰,domain不仅仅可以作为区分子域名来使用,也可以做参数分割,不同域名区分等注意Laravel的路由匹配顺序,希望大家能认真的做一遍,体验一下,做到心中有数既然已经区分开域名,那么就可以绑定到不同的控制器,或者绑定不同的模型,大家灵活应用 ...

April 2, 2019 · 1 min · jiezi

Laravel 中创建 Zip 压缩文件并提供下载

文章转自:https://learnku.com/laravel/t… 更多文章:https://learnku.com/laravel/c…如果您需要您的用户支持多文件下载的话,最好的办法是创建一个压缩包并提供下载。看下在 Laravel 中的实现。事实上,这不是关于 Laravel 的,而是和 PHP 的关联更多,我们准备使用从 PHP 5.2 以来就存在的 ZipArchive 类 ,如果要使用,需要确保php.ini 中的 ext-zip 扩展开启。任务 1: 存储用户的发票文件到 storage/invoices/aaa001.pdf下面是代码展示:$zip_file = ‘invoices.zip’; // 要下载的压缩包的名称// 初始化 PHP 类$zip = new \ZipArchive();$zip->open($zip_file, \ZipArchive::CREATE | \ZipArchive::OVERWRITE);$invoice_file = ‘invoices/aaa001.pdf’;// 添加文件:第二个参数是待压缩文件在压缩包中的路径// 所以,它将在 ZIP 中创建另一个名为 “storage/” 的路径,并把文件放入目录。$zip->addFile(storage_path($invoice_file), $invoice_file);$zip->close();// 我们将会在文件下载后立刻把文件返回原样return response()->download($zip_file);例子很简单,对吗?*任务 2: 压缩 全部 文件到 storage/invoices 目录中Laravel 方面不需要有任何改变,我们只需要添加一些简单的 PHP 代码来迭代这些文件。$zip_file = ‘invoices.zip’;$zip = new \ZipArchive();$zip->open($zip_file, \ZipArchive::CREATE | \ZipArchive::OVERWRITE);$path = storage_path(‘invoices’);$files = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($path));foreach ($files as $name => $file){ // 我们要跳过所有子目录 if (!$file->isDir()) { $filePath = $file->getRealPath(); // 用 substr/strlen 获取文件扩展名 $relativePath = ‘invoices/’ . substr($filePath, strlen($path) + 1); $zip->addFile($filePath, $relativePath); }}$zip->close();return response()->download($zip_file);到这里基本就算完成了。你看,你不需要任何 Laravel 的扩展包来实现这个压缩方式。文章转自:https://learnku.com/laravel/t… 更多文章:https://learnku.com/laravel/c… ...

April 2, 2019 · 1 min · jiezi

教程:Laravel 集合(Collection)的基础用法

文章转自:https://learnku.com/laravel/t… 更多文章:https://learnku.com/laravel/c…Laravel 集合是 Laravel 框架中一个十分有用的工具。Laravel 集合就像是在 PHP 中的数组,但会更好用。在这篇教程中,我们将会体验一些集合使用时的实用技巧。集合(Collection)Illuminate\Support\Collection 类了提供一个便捷的操作数组的封装。集合 Collection 类实现了部分 PHP 和 Laravel 的接口,例如:ArrayAccess - 用于操作数组对象的接口。IteratorAggregate - 用于创建外部迭代器的接口。JsonSerializable你可以在这里查看其余已实现的接口。创建一个新的集合一个集合可以使用 collect() 帮助函数基于一个数组被创建 或者直接通过 Illuminate\Support\Collection 类实例化。一个非常简单的使用 collect() 帮助函数的示例:$newCollection = collect([1, 2, 3, 4, 5]);一个更复杂的示例:<?phpnamespace app\Http\Controllers;use Illuminate\Support\Collection;class TestController extends Controller{ /** * Create a new collection using the collect helper method. / public function helperCollection() { $newCollection = collect([1, 2, 3, 4, 5]); dd($newCollection); } /* * Create a new collection with a Collection class instance. / public function classCollection() { $newCollection = new Collection([1, 2, 3, 4, 5]); dd($newCollection); }}这个帮助函数用起来要简单很多因为你再不需要实例化 Illuminate\Support\Collection 类。我也有用到 dd() 帮助函数来在浏览器中显示集合。看起来大概会是这样子。Eloquent ORM 集合Laravel Eloquent ORM 也以集合的形式返回数据。Eloquent ORM 的调用会以集合的形式返回数据为了演示这个效果,我将初始化一个 Sqlite 数据库。我们将用 Laravel 框架预置的迁移文件来创建一个用户表,然后填充10条数据到用户表中。 /* * 从用户表获取用户列表 / public function getUsers() { $users = User::all(); dd($users); }该控制器方法会返回一个如下显示的所有用户的 Laravel 集合。你可以通过箭头符号便捷的访问集合属性。至于实例,想要获取 $users 集合的第一个用户的名字,我们可以这样做。 /* * 获取第一个用户的名字 / public function firstUser() { $user = User::first(); dd($user->name); }创建我们的示例集合我们将会使用一些最有用的集合操作技巧,你一定会觉得很好用。在接下来的几个章节中,我将会用到下面这套用户表的数据以及一些自定义的集合来达到演示的目的。虽然我们这里是手动创建,但使用 Laravel 的模型工厂来创建也是可以的。Array( [0] => Array ( [id] => 1 [name] => Chasity Tillman [email] => qleuschke@example.org [age] => 51 [created_at] => 2016-06-07 15:50:50 [updated_at] => 2016-06-07 15:50:50 ) …)查找数据有多种方法可以在集合中查找数据。containscontains() 方法可以传一个单一值,或一组键 / 值对或者一个回调函数,然后它会返回一个布尔值来告知目标内容是否在集合中。 /* * 判断键 / 值对或回调内容是否存在于集合中 * * * @return true or false / public function contains() { $users = User::all(); $users->contains(’name’, ‘Chasity Tillman’); //true $collection = collect([’name’ => ‘John’, ‘age’ => 23]); $collection->contains(‘Jane’); //false $collection = collect([1, 2, 3, 4, 5]); $collection->contains(function ($key, $value) { return $value <= 5; //true }); }where通过键值对的形式, 用 where 方法检索集合.where() 方法还可以被链式调用。 /* * 使用 where 方法找到匹配的数据 * * 通过链式调用来增加匹配条件 / public function where() { $users = User::all(); $user = $users->where(‘id’, 2); // 找出 id 为 2 的用户 $user = $users->where(‘id’, 1) ->where(‘age’, ‘51’) ->where(’name’, ‘Chasity Tillman’); // 找出 user 集合中 id 为 1,年龄为 51 岁,名叫 Chasity Tillman 的用户 }还有一些像 where-like 这种用于检索的方法,我就不一一列举的,大家可以通过 Laravel 的官方文档查看。可以着重看下面几个:whereIn() - 以键值对为参数检索集合,其中值必须是组数。search() - 在一个集合中检索值,如果有值,返回其索引,如果没有,则返回 false 。has() - 查看键值对是否存,返回布尔值。过滤数据你可能已经猜到了,用 filter() 方法过滤。你可能也已经想到了, filter 方法会接收一个回调函数作为参数,在回调函数中做判断的逻辑,对吗?你是这么想的吗? /* * 使用 filter 方法,找出所有年龄小于 35 的用户 / public function filter() { $users = User::all(); $youngsters = $users->filter(function ($value, $key) { return $value->age < 35; }); $youngsters->all(); // 所有年龄小于 35 的用户 }filter 方法会接收一个回调函数作为参数,回调函数的参数是键值对,具体筛选的逻辑写在函数里面,并且会返回所有符合条件的值。这里还用到了 all() 方法,它会返回一个集合里的所有值。排序 / 排序数据集合允许我们能够使用两种简单的方法对数据进行排序 :-sortBy() - 给定数据进行升序排序sortyByDesc() - 给定数据降序排序排序方法接受一个键或回调函数参数用于对集合进行排序。 /* * 排序方法接受一个键或回调函数参数 * 用于对集合进行排序。 / public function sortData() { $users = User::all(); $youngestToOldest = $users->sortBy(‘age’); $youngestToOldest->all(); //列出以年龄升序的所有用户 $movies = collect([ [ ’name’ => ‘Back To The Future’, ‘releases’ => [1985, 1989, 1990] ], [ ’name’ => ‘Fast and Furious’, ‘releases’ => [2001, 2003, 2006, 2009, 2011, 2013, 2015, 2017] ], [ ’name’ => ‘Speed’, ‘releases’ => [1994] ] ]); $mostReleases = $movies->sortByDesc(function ($movie, $key) { return count($movie[‘releases’]); }); $mostReleases->toArray(); //列出以上映总数降序排序的电影 dd($mostReleases->values()->toArray()); / 列出以上映总数降序排序的电影并重置键值 / }排序方法维护每个值的键。 虽然这对您的应用程序可能很重要,但您可以通过链式 values() 方法将它们重置为默认的基于零的增量值。像往常一样,我还使用一个将集合转换为数组的集合方法 toArray() 。数据 分组groupBy对集合进行分组有助于理解您的数据。 groupBy 方法接受键或回调函数,并根据键值或返回的回调值返回分组集合。 /* * groupBy 返回基于键或回调函数分组的数据 * 逻辑 / public function grouping() { $movies = collect([ [’name’ => ‘Back To the Future’, ‘genre’ => ‘scifi’, ‘rating’ => 8], [’name’ => ‘The Matrix’, ‘genre’ => ‘fantasy’, ‘rating’ => 9], [’name’ => ‘The Croods’, ‘genre’ => ‘animation’, ‘rating’ => 8], [’name’ => ‘Zootopia’, ‘genre’ => ‘animation’, ‘rating’ => 4], [’name’ => ‘The Jungle Book’, ‘genre’ => ‘fantasy’, ‘rating’ => 5], ]); $genre = $movies->groupBy(‘genre’); / [ “scifi” => [ [“name” => “Back To the Future”, “genre” => “scifi”, “rating” => 8,], ], “fantasy” => [ [“name” => “The Matrix”, “genre” => “fantasy”, “rating” => 9,], [“name” => “The Jungle Book”, “genre” => “fantasy”, “rating” => 5, ], ], “animation” => [ [“name” => “The Croods”, “genre” => “animation”, “rating” => 8,], [“name” => “Zootopia”, “genre” => “animation”, “rating” => 4, ], ], ] / $rating = $movies->groupBy(function ($movie, $key) { return $movie[‘rating’]; }); / [ 8 => [ [“name” => “Back To the Future”, “genre” => “scifi”, “rating” => 8,], [“name” => “The Croods”, “genre” => “animation”, “rating” => 8,], ], 9 => [ [“name” => “The Matrix”, “genre” => “fantasy”, “rating” => 9,], ], 4 => [ [“name” => “Zootopia”,“genre” => “animation”, “rating” => 4,], ], 5 => [ [“name” => “The Jungle Book”,“genre” => “fantasy”,“rating” => 5,], ], ] / }获取数据子集给定一组数据,然后是一个集合,您可能希望得到它的一部分。 这可能是:前2条记录最后2条记录除2组以外的所有记录。集合操作帮助我们使用少量的方法完成这些操作。taketake 方法接受一个整数值并返回指定的项数。给定一个负数, take() 返回集合末尾的指定项数。 /* * take 方法返回集合中的 n 个项数。 * 给定 -n ,返回最后 n 个项数 / public function takeMe() { $list = collect([ ‘Albert’, ‘Ben’, ‘Charles’, ‘Dan’, ‘Eric’, ‘Xavier’, ‘Yuri’, ‘Zane’ ]); //获取前两个名字 $firstTwo = $list->take(2); //[‘Albert’, ‘Ben’] //获取最后两个名字 $lastTwo = $list->take(-2); //[‘Yuri’, ‘Zane’] }chunkchunk 方法将集合分割成大小为 n 的较小集合。 /* * Chunk(n) 返回大小为 n 的较小集合,每个都来自原始集合 * / public function chunkMe() { $list = collect([ ‘Albert’, ‘Ben’, ‘Charles’, ‘Dan’, ‘Eric’, ‘Xavier’, ‘Yuri’, ‘Zane’ ]); $chunks = $list->chunk(3); $chunks->toArray(); / [ [“Albert”, “Ben”, “Charles”,], [3 => “Dan”, 4 => “Eric”, 5 => “Xavier”,], [6 => “Yuri”, 7 => “Zane”,], ] / }这里有很多方法可以达到效果。当你传递数据到 blade 页面时,你可以将他分块以一次获得 n 行数据,例如,将每 3 个名字装进一行。@foreach($list->chunk(3) as $names) <div class=“row”> @foreach($names as $name) {{ $name }} @endforeach </div>@endforeach你也可以使用 collapse() 方法将更新的集合组转成一个大的集合,来反转 chunk 方法,请查看此 here.遍历数据mapmap 方法会遍历集合,将每个元素传入一个闭包函数,该闭包函数的返回值将替换原来的元素值。我们创建一个由名字组成的集合,并使用 map 方法返回一个由对应名字长度组成的集合。 /* * map function iterates a collection through a callback * function and performs an operation on each value. / public function mapMe() { $names = collect([ ‘Albert’, ‘Ben’, ‘Charles’, ‘Dan’, ‘Eric’, ‘Xavier’, ‘Yuri’, ‘Zane’ ]); $lengths = $names->map(function ($name, $key) { return strlen($name); }); $lengths->toArray(); //[6, 3, 7, 3, 4, 6, 4, 4,] }transform虽然 map 方法创建了一个新的集合,但有时候你可能想去修改原始的集合内容。transform 提供了一个回调方法,并对同一个集合进行操作。因为转换不会产生新的集合,所以你无需把它赋给新的值。 /* * Transform 操作原始的集合。 / public function transformMe() { $names = collect([ ‘Albert’, ‘Ben’, ‘Charles’, ‘Dan’, ‘Eric’, ‘Xavier’, ‘Yuri’, ‘Zane’ ]); $names->transform(function ($name, $key) { return strlen($name); }); $names->toArray(); //[6, 3, 7, 3, 4, 6, 4, 4,] }reduce不同于 map 和 transform 方法,reduce 方法返回单个值。他将每次迭代的结果传给下一次迭代。例如,为了获取一个集合中所有整数的和,reduce 会传递后续数字的总和,并迭代的将结果添加到下一个数字。 /* * 获取一个集合中所有数字的和 / public function reduceMe() { $numbers = collect([ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ]); $sum = $numbers->reduce(function ($sum, $number) { return $sum + $number; }); //55 }eacheach 方法通过回调函数传递每个数据项。关于 each 方法最有趣的部分是,你可以简单的在回调函数中返回 false 来跳出循环。 /* 打印小于等于五的一列数字 * / public function eachMethod() { $numbers = collect([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); $smallNumbers = $numbers->each(function ($num, $key) { if ($num > 5) { return false; } echo $num .", “; }); //1, 2, 3, 4, 5, }everyevery 方法创建一个由集合中每第 n 个元素组成的新集合。集合论Laravel 提供了对集合论的支持,这意味着我们可以对两个不同集合取交集、并集等操作。unionunion 方法将给定的数组添加到集合。如果给定的数组含有与原集合一样的键,则原集合的值不会被改变: / * add array values to a collection using union / public function union() { $coolPeople = collect([ 1 => ‘John’, 2 => ‘James’, 3 => ‘Jack’ ]); $allCoolPeople = $coolPeople->union([ 4 => ‘Sarah’, 1 => ‘Susan’, 5 =>‘Seyi’ ]); $allCoolPeople->all(); / [ 1 => “John”, 2 => “James”, 3 => “Jack”, 4 => “Sarah”, 5 => “Seyi”, ] / }intersectintersect() 方法接收一个数组或集合作为参数,该方法会将集合中那些不包含在传入参数的元素移除。 /* * Return a list of very cool people in collection that * are in the given array */ public function intersect() { $coolPeople = collect([ 1 => ‘John’, 2 => ‘James’, 3 => ‘Jack’ ]); $veryCoolPeople = $coolPeople->intersect([‘Sarah’, ‘John’, ‘James’]); $veryCoolPeople->toArray(); //[1 => “John” 2 => “James”] }可以发现, intersect 方法的返回值保留了原有的键。结论我试图涵盖你可能找到你能自己找到所需的集合方法,但这仍然有太多需要学的。最值得注意的,我留下以下内容常见的数学方法,例如 sum 和 avg。涉及更新集合的方法,例如 splice,prepend,push 和 pop。在 Laravel 文档 和 Laravel API 文档 上还有你可以用于操作集合的更多方法,货心你想查看一下。要跟进本教程代码,查看 gtihub 仓库 here。随意贡献你的代码。文章转自:https://learnku.com/laravel/t… 更多文章:https://learnku.com/laravel/c… ...

April 1, 2019 · 6 min · jiezi

Laravel 框架 Model 对象转 json 字符串丢失更新

场景还原UserModelclass UserModel extends Model { public function role() { return $this->belognsTo(RoleModel::class , ‘role_id’ , ‘id’); }}出错的程序$user = UserModel::with(‘role’)->find(1);// $user->role 是一个 RoleModel// 更新 role 属性$user->role = ’test’;// 正确输出 testvar_dump($user->role);// 但是!!转换成 json 字符串后// 你会发现,role 居然还是个模型!!// 并不是你后面设置成的 test !// 怪胎,丢失更新了?Laravel Bug ??// 实际上不是!请看下属描述var_dump(json_encode($user));原理概述Laravel 的 Illuminate\Database\Eloquent\Model 实现了 JsonSerializable 接口,所以在调用 json_encode 进行序列化时,会调用 Model::jsonSerialize 方法,他这个方法返回的数据是:array_merge($attribute , $relation);实际上你通过:$model->name = ‘grayVTouch’;这种方式附加的新属性,Laravel 通过 __set 魔术方法重载,将其添加到 attribute 数组中,你是无法更改 relation 数组的!而通过 模型关联 你却可以为 relation 数组新增单元!看到上面的数组合并方式,可以知道 relation 会覆盖掉 attribute 中的同名属性!!因而要特别注意:如果 relation 中有和 attribute 中同名的属性,请修改 relation 关联名称!如果不想修改 relation 名称,坚持前者覆盖后者,请:// 保存值$attr = $model->attr;// 删除属性:attribute / relation 中的属性(Laravel 内部调用 __unset 魔术方法)unset($model->attr)// 重新设置值,仅设置到 attribute 数组// relation 并不会被设置$model->attr = $model;综合评价Laravel 由于将模型属性拆分成两个数组,而他们实际上又同属于一个对象!所以如果存在同名属性,必然会产生 谁覆盖谁 的问题,attribute 一开始就是对应数据库表中的字段的,而 relation 是后面程序附加的,为了不丢失更新,后者覆盖前者,非常正确。虽然在使用过程中应该小心避免 relation 和 attribute 撞上同名属性,但偶尔还是会碰到的~,这个还是稍微注意下就好,这并非 Bug,而是在当前的程序处理方式下必然会产生的一个正常现象。 ...

March 31, 2019 · 1 min · jiezi

laravel 安装中文语言包

导语现在的语言设置还是英文,使用 laravel-lang 扩展来设置中文。这里是官方中文文档,按照这个来操作就行了。代码可查看 GitHub。先来看下设置前的 404 页面安装及配置通过 composer 安装 composer require “overtrue/laravel-lang:~3.0"修改 config/app.php 配置文件,将Illuminate\Translation\TranslationServiceProvider::class, 替换为 Overtrue\LaravelLang\TranslationServiceProvider::class,,然后将 ’locale’ => ’en’, 修改成 ’locale’ => ‘zh-CN’,最后执行 php artisan lang:publish zh-CN自定义修改看下 resources/lang 目录实际上添加了 zh-CN.json 翻页文件和 zh-CN 目录。404 页面的提示语是在 zh-CN.json 文件中,修改试下来看下代码是如何实现的,找到 vendor/laravel/framework/src/Illuminate/Foundation/Exceptions/views/404.blade.php 页面,可以看到如下一行代码看下这个 __ 函数的作用__ 函数会使用本地化文件翻译给定翻译字符串或翻译键如果给定翻译字符串或键不存在,__ 函数将会返回给定值。再来看下 zh-CN 目录的文件,是和 en 目录一一对应的,同样可以自定义修改提示语。也可以添加额外的语言,可以查看文档,这里不再做测试。参考资料:laravel-lang、laravel 本地化、laravel 辅助函数。

March 30, 2019 · 1 min · jiezi

PHP & MySQL 「数据关联一对一」的最佳实践

前言在开发过程中,通常会遇到很多 一对一 数据的处理情况。而很多时候我们会要取到的是一个列表,然后列表的单条记录的对应另外一张表,来实现业务。比如下面的商品信息 和 商品详情 两个表,这里为了演示只是使用了基础字段,实际开发中可能会复杂的多,下方演示代码中数据库连接使用 PDO 进行处理。表结构goods列类型注释idint(11) 自动增量主键IDtitlevarchar(100)商品名称pricedecimal(10,2)商品价格covervarchar(100)商品封面goods_detail列类型注释idint(11) 自动增量主键IDgoods_idint(11)商品IDcontentvarchar(5000)商品图文介绍初级坦言,无论是在公司,还是在一些开源项目上,我都看到过如下的代码。$query = $db->query(‘select * from goods’);$result = $query->fetchAll();// 方案一foreach($result as $key => $item){ $query = $db->query(‘select * from goods_detail where goods_id=’ . $item[‘id’]); $result[$key][‘goods_detail’] = $query->fetch();}var_dump($result);// 方案二foreach($result as &$item){ $query = $db->query(‘select * from goods_detail where goods_id=’ . $item[‘id’]); $item[‘goods_detail’] = $query->fetch();}unset($item);var_dump($result);// 方案三$result = array_map(function($item){ $query = $db->query(‘select * from goods_detail where goods_id=’ . $item[‘id’]); $item[‘goods_detail’] = $query->fetch(); return $item;},$result);var_dump($result);这是最暴力的方式,也是立杆见影,而且方案一看起来代码貌似还很繁琐,不是吗?如果学过 引用这一节的朋友,应该知道第二种用法,直接用引用去操作源数据,当然最后最好别忘了 unset 掉 $item,除了第二种,我们还可以用第三种方式,使用 array_map,诚然,这和第二种方式没什么区别,但是这其中有着一个非常大的问题:数据库查询的N+1 。从执行中我们就可以看到,除了查询列表的一条 SQL 外,每查询一条记录对应的都需要执行一条 SQL ,导致了额外的查询,想想一下如果查询没有 limit 限制。会是什么样子的情况?进阶看到这里,有人可能会想到了另一种方案来,先查询列表,然后取出列表里面的 goods_id 之后使用 in 查询,然后再循环分配给列表,看代码。$goods_id = array_column($result,‘id’);$goods_id_str = implode(’,’,$goods_id);$query = $db->query(sprintf(‘select * from goods_detail where goods_id in (%s)’,$goods_id_str));$goods_detail_list = $query->fetchAll();foreach($result as &$item){ $item[‘goods_detail’] = array_first($goods_detail_list,function($item1){ return $item[‘id’] == $item1[‘goods_id’]; });}unset($item);var_dump($result);/** * 来自 Laravel /if (!function_exists(‘value’)) { function value($value) { return $value instanceof Closure ? $value() : $value; }}/* * 来自 Laravel /if (!function_exists(‘array_first’)) { /* * @param $array * @param callable|null $callback * @param null $default * @return mixed */ function array_first($array, callable $callback = null, $default = null) { if (is_null($callback)) { if (empty($array)) { return value($default); } foreach ($array as $item) { return $item; } } foreach ($array as $key => $value) { if (call_user_func($callback, $value, $key)) { return $value; } } return value($default); }}在这个代码中,我们完美避开了 N+1 的窘境,使用了in查询,然后遍历数组,再使用 array_first 方法来查找后传递给 goods_detail 索引,虽然这样的效率相比第一次的要高了很多,但是并不完美,接下来来看最后一种方案。关于 array_first 可以看我的另一篇文章 『PHP 多维数组中的 array_find』。最佳实践$goods_detail_list_by_keys = array_column($goods_detail_list,null,‘goods_id’);foreach($result as &$item){ $item[‘goods_detail’] = array_key_exists($goods_detail_list_by_keys,$item[‘id’]) ? $goods_detail_list_by_keys[$item[‘id’]] : null ; // php 7.1+ // $item[‘goods_detail’] = $goods_detail_list_by_keys[$item[‘id’]] ?? null;}unset($item);var_dump($result);这一次,我们用到了其他两个函数。array_column 、 array_key_exists,接下里一一道来,其实在array_column的官方手册中的我们就能 Example #2 中就介绍了我们想要的方法。套用在这里就是重置goods_detail_list 里面元素的 key 为 单个元素下的 goods_id。在后面我们直接用 array_key_exists 判断是否存在,然后做出相应的处理就好了。在这里我们还可以做另外一个操作,那就是默认值,因为有时候,数据有可能会因对不上,如果查出来直接返回给前端,前端没有预料到这种情况没有做容错处理就会导致前端页面崩溃,下面来改写一下代码// 在 「进阶」 板块中,我们用到了 「array_first」 函数,该函数第三个参数可以直接设置默认值,我们就不多讲了,主要讲讲最后一个$goods_detail_default = [ ‘content’ => ‘默认内容’, ‘id’ => null, ‘goods_id’=> null,];foreach($result as &$item){ $tmp = array_key_exists($goods_detail_list_by_keys,$item[‘id’]) ? $goods_detail_list_by_keys[$item[‘id’]] : [] ; // php 7.1+ // $tmp = $goods_detail_list_by_keys[$item[‘id’]] ?? []; $item[‘goods_detail’] = array_merge($goods_detail_default,$tmp);}unset($item);var_dump($result);结束看到这里就算是完结了但是有的朋友会说,为什么不用 leftJoin 来处理?确实,在处理一对一关系中很多时候我们都会选择 innerJoin 或者 leftJoin 来进行处理,一条 SQL 就能搞定,很少会用到类似于这种方案,其实不然,在主流的框架中,默认的解决方案几乎都是这样处理的,比如Laravel、ThinkPHP,考虑到的场景会有很多,比如有的时候我只是需要按需取一部分的,或者我需要根据我后面的业务结果来决定是不是要加载一对一,然而这种情况下 join 似乎就不太适合。 ...

March 30, 2019 · 2 min · jiezi

中台到底是个啥?

文章来自健荐微信公众号,作者王健,ThoughtWorks高级咨询师。王健将于5月18-19日在上海A2M峰会分享关于中台的话题,更多A2M峰会内容可点击此处查看。从去开始,好像就有一只无形的手一直将我与“微服务”、“平台化”、“中台化”撮合在一起,给我带来了很多的困扰和思考与收获。平台化正兴起,从基础设施到人工智能等各个领域不断涌现的各类平台,对于软件开发人员及企业带来了深远的影响。然而,在中国提“数字化平台战略”大家可能会觉得比较抽象,比较远大空;若是提“中台”大家则会更熟悉一些。那…中台到底是什么?会不会又是另一个Buzzword呢?这个从名字上看像是从前台与后台中间硬挤出来的新断层,它与前台和后台的区别和界限到底在哪儿?什么应该放到中台,什么又应该放到前台或是后台?它的出现到底是为了解决什么问题呢?这一个接一个的问题就不断的涌出并萦绕在我的脑子里。直到一年多后的今天,随着参与的几个平台化、企业中台相关的项目已顺利步上正轨,终于可以坐下来回顾这一年的实践与思考,再次试图回答这些问题,并梳理成文,与大家交流探讨。一、中台迷思到处都在喊中台,到处都是中台,中台这个词在我看来已经被滥用了。在一部分人眼里:中台就是技术平台,像微服务开发框架、DevOps平台、PaaS平台,容器云之类的,人们都叫它“技术中台”;在一部份人眼里:中台就是微服务业务平台,像最常见的什么用户中心、订单中心,各种微服务集散地,人们都叫它“业务中台”;在一些人眼里:中台应该是组织的事情,在释放潜能:平台型组织的进化路线图 (豆瓣)中就提出了平台型组织和组织中台的概念,这类组织中台在企业中主要起到投资评估与投后管理的作用,类似于企业内部资源调度中心和内部创新孵化组织,人们叫它“组织中台”。看完本篇你就会理解,上边的这几类“中台”划分还是靠谱的,更多我看到的情况是,大家为了响应企业的“中台战略”,干脆直接将自己系统的“后端”或是“后台”改个名,就叫“中台”。中台到底是什么?它对于企业的意义到底是什么?当我们谈中台时我们到底在谈些什么?想要寻找到答案,仅仅沉寂在各自“中台”之中,如同管中窥豹,身入迷阵,是很难想清楚的。不如换个⾓度,从各类的“中台迷阵”中跳脱出来,尝试以更高的视角,从企业均衡可持续发展的角度来思考中台,反推它存在的价值。为了搞明白中台存在的价值,我们需要回答以下两个问题:企业为什么要平台化?企业为什么要建中台?1、企业为什么要平台化?先给答案,其实很简单:因为在当今互联网时代,⽤户才是商业战场的中心,为了快速响应用户的需求,借助平台化的力量可以事半功倍。不断快速响应、探索、挖掘、引领⽤户的需求,才是企业得以⽣存和持续发展的关键因素。那些真正尊重用户,甚⾄不惜调整⾃己颠覆⾃己来响应⽤户的企业,将在这场以⽤户为中心的商业战争中得以⽣存和发展;反之,那些在过去的成就上故步⾃封,存在侥幸⼼理希望⽤户会像之前一样继续追随⾃己的企业,则会被用户淘汰。很残酷,但这就是这个时代最基本的企业⽣存法则。平台化之所以重要,就是因为它赋予或加强了企业在以用户为中心的现代商业战争中最最最核心的能力:⽤户响应力。这种能力可以帮助企业在商战上先发制⼈,始终抢得先机。可以说,在互联网时代,商业的斗争就是对于用户响应力的比拼。又有点远大空是不是?我们来看⼏个经典的例子:例子1说起中台,最先想到的应该就属是阿⾥的“⼤中台,⼩前台”战略。阿⾥⼈通过多年不懈的努力,在业务的不断催化滋养下,将⾃己的技术和业务能力沉淀出一套综合能力平台,具备了对于前台业务变化及创新的快速响应能力。例子2海尔也早在⼗年前就已经开始推进平台化组织的转型,提出了“平台⾃营体⽀撑⼀线⾃营体”的战略规划和转型⽬标。构建了“⼈单合一”、“⽤户付薪”的创客文化,真正将平台化提⾼到了组织的⾼度。例子3华为在几年前就提出了“⼤平台炮火支撑精兵作战”的企业战略,“让听得到炮声的人能呼唤到炮火”,这句话形象的诠释了大平台⽀撑下小前台的作战策略。这种极度灵活又威力巨⼤的战法,使之可以迅速响应瞬息万变的战场,一旦锁定目标,通过大平台的炮火群,迅速精准对于战场进行强大的火⼒支援。可⻅,在互联⽹热火朝天,第四次工业革命的曙光即将到来的今日,企业能否真正做到“以用户为中心”,并不断提升自己的用户响应力来追随甚至引领用户的脚步,持续规模化创新,终将决定企业能否在这样充满挑战和机遇的市场上笑到最后,在商业上长久保持创新活力与竞争力。而平台化恰好可以助力企业更快更好的做到这些,所以这回答了第一个问题——企业需要平台化。2、企业为什么要建中台?好,到此我们想明白了为什么需要平台化。但是平台化并不是一个新概念,很多企业在这个方向上已经做了多年的努力和积淀。那为什么最近几年“中台”这个相对较新的概念又会异军突起?对于企业来讲,传统的“前台+后台”的平台化架构又为什么不能满足企业的要求呢?这就引出了我们的第二个问题:企业为什么要建中台?定义一下前台与后台因为平台这个词过于宽泛了,为了能让大家理解我在说什么,我先定义一下本文上下文我所说的前台和后台各指什么:前台:由各类前台系统组成的前端平台。每个前台系统就是一个用户触点,即企业的最终用户直接使用或交互的系统,是企业与最终用户的交点。例如用户直接使用的网站、手机App、微信公众号等都属于前台范畴。后台:由后台系统组成的后端平台。每个后台系统一般管理了企业的一类核心资源(数据+计算),例如财务系统、产品系统、客户管理系统、仓库物流管理系统等,这类系统构成了企业的后台。基础设施和计算平台作为企业的核心计算资源,也属于后台的一部分。后台并不为前台而生定义了前台和后台,对于第二个问题(企业为什么要建中台),同样先给出我的答案:因为企业后台往往并不能很好的支撑前台快速创新响应用户的需求,后台更多解决的是企业管理效率问题,而中台要解决的才是前台的创新问题。大多数企业已有的后台,要么前台根本就用不了,要么不好用,要么变更速度跟不上前台的节奏。我们看到的很多企业的后台系统,在创建之初的目标,并不是主要服务于前台系统创新,更多的是为了实现后端资源的电子化管理,解决企业管理的效率问题。这类系统要不就是当年花大价钱外购,需要每年支付大量的服务费,并且版本老旧,定制化困难;要不就是花大价钱自建,年久失修,一身的补丁,同样变更困难,也是企业所谓的“遗留系统”的重灾区。总结下来就两个字“慢”和“贵”,对业务的响应慢,动不动改个小功能就还要花一大笔钱。有人会说了,你不能拿遗留系统说事儿啊,我们可以新建后台系统啊,整个2.0问题不就解决了?但就算是新建的后台系统,因为其管理的是企业的关键核心数据,考虑到企业安全、审计、合规、法律等限制,导致其同样往往⽆法被前台系统直接使用,或是受到各类限制⽆法快速变化,以⽀持前台快速的创新需求。此时的前台和后台就像是两个不同转速的⻮轮,前台由于要快速响应前端用户的需求,讲究的是快速创新迭代,所以要求转速越快越好;⽽后台由于⾯对的是相对稳定的后端资源,⽽且往系统陈旧复杂,甚至还受到法律法规审计等相关合规约束,所以往往是稳定至上,越稳定越好,转速也自然是越慢越好。所以,随着企业务的不断发展,这种“前台+后台”的⻮轮速率“匹配失衡”的问题就逐步显现出来。随着企业业务的发展壮大,因为后台修改的成本和⻛险较⾼,也就驱使我们尽量选择保持后台系统的稳定性。但因为还要响应用户持续不断的需求,自然就会将大量的业务逻辑(业务能力)直接塞到了前台系统中,引入重复的同时还会致使前台系统不断膨胀,变得臃肿,形成了一个个⼤泥球的“烟囱式单体应用”,渐渐拖垮了前台系统的“⽤户响应⼒”。用户满意度降低,企业竞争力也随之不断下降。对于这样的问题,Gatner在2016年提出的一份《Pace-Layered Application Strategy》报告中,给出了一种解决方案,即按照“步速”将企业的应用系统划分为三个层次(正好契合前中后台的三个层次),不同的层次采用完全不同的策略。而Pace-Layered Application Strategy也为“中台”产生的必然性,提供了理论上的支撑。Pace-Layered Application Strategy在这份报告中Gatner提出,企业构建的系统从Pace-Layered的⾓度来看可以划分为三类: SOR(Systems of record ),SOD(Systems of differentiation)和SOI(Systems of innovation)。处于不同Pace-Layered的系统因为⽬的不同、关注点不同、要求不同,变化的“速率”自然也不同,匹配的也需要采⽤不同的技术架构、管理流程、治理架构甚至投资策略。⽽前面章节我们提到的后台系统,例如CRM、ERP、财务系统等,它们⼤多都处于SOR的Pace-Layered。这些系统的建设之初往往是以规范处理企业底层资源和企业的核⼼可追溯单据(例如财务单据,订单单据)为主要⽬的。它们的变更周期往往比较⻓,⽽且由于法律律审计等其他限制,导致对于它们的变更需要严谨的申报审批流程和更高级别的测试部署要求,这就导致了它们往往变化频率低、变化成本高、变化⻛险高、变化周期⻓,⽆法满⾜由⽤户驱动的快速变化的前台系统要求。我们又要尽力保持后台(SOR)系统的稳定可靠,⼜要前台系统(SOI)能够⼩而美,快速迭代。就出现了上文提到的“齿轮匹配失衡”的问题,感觉鱼与熊掌不可兼得。正当陷入僵局的时候,天空中飘来一声IT谚语:软件开发中遇到的所有问题,都可以通过增加⼀层抽象而得以解决!⾄此,⼀声惊雷滚过,“中台”脚踏七彩祥云,承载着SOD(Systems of differentiation) 的前世寄托,横空出世。我们先试着给中台下个定义:中台是真正为前台而生的平台(可以是技术平台,业务能力甚至是组织机构),它存在的唯一目的就是更好的服务前台规模化创新,进而更好的响应服务引领用户,使企业真正做到自身能力与用户需求的持续对接。中台就像是在前台与后台之间添加的⼀组“变速齿轮”,将前台与后台的速率进行匹配,是前台与后台的桥梁。它为前台而生,易于前台使用,将后台资源顺滑流向用户,响应用户。中台很像Pace-Layered中的SOD,提供了比前台(SOI)更强的稳定性,以及⽐后台(SOR)更高的灵活性,在稳定与灵活之间寻找到了⼀种美妙的平衡。中台作为变速齿轮,链接了用户与企业核心资源,并解决了配速问题:有了“中台”这⼀新的Pace-Layered断层,我们就可以将早已臃肿不堪的前台系统中的稳定通用业务能力“沉降”到中台层,为前台减肥,恢复前台的响应力;又可以将后台系统中需要频繁变化或是需要被前台直接使用的业务能力“提取”到中台层,赋予这些业务能力更强的灵活度和更低的变更成本,从而为前台提供更强大的“能力炮火”支援。所以,企业在平台化的过程中,需要建设自己的中台层(同时包括技术中台、业务中台和组织中台)。3、小结思考并回答了文初提出的两个关于中台价值的核心问题,解决了我对于中台产生的一些困惑,不知道对你有没有启发?我最后再来总结一下:以用户为中心的持续规模化创新,是中台建设的核心目标。企业的业务响应能⼒和规模化创新能力,是互联⽹时代企业综合竞争⼒的核⼼体现。平台化包括中台化,只是帮助企业达到这个目标的⼿段,并不是⽬标本身。中台(无论是技术中台、业务中台还是组织中台)的建设,根本上是为了解决企业响应力困境, 弥补创新驱动快速变化的前台,稳定可靠驱动变化周期相对较慢的后台之间的⽭盾,提供⼀个中间层来适配前台与后台的配速问题,沉淀能⼒,打通并顺滑链接前台需求与后台资源,帮助企业不断提升用户响应⼒。所以,中台到底是什么根本不重要,如何想方设法持续提高企业对于⽤户的响应力才是最重要的。而平台化或是中台化,只是恰巧走在了了这条正确的大道上。二、到底中台长啥样?列举了这么多各式各样的中台,最后都扯到了组织层面,是不是有种越听越晕的感觉?好像什么东西加个“中台”的后缀都可以靠到中台上来?那估计很快就会看到例如AI中台、VR中台、搜索中台、算法中台……对了,算法中台已经有了……让我们引用一段阿里玄难在接受采访时提到对于中台的一段我非常认同的描述:本文中我们一直提到的一个词就是“能力”,从玄难的这段采访也可以看出,在阿里“能力”也是中台的核心。甄别是不是中台,还要回到中台要解决的问题上,也就是我上面主要关注的问题。我认为一切以”以用户为中心的持续规模化创新”为目的,将后台各式各样的资源转化为前台易于使用的能力,帮助我们打赢这场以用户为中心的战争的平台,我们都可以称之为中台:业务中台提供重用服务,例如用户中心、订单中心之类的开箱即用可重用能力,为战场提供了强大的后台炮火支援能力,随叫随到,威力强大;数据中台提供了数据分析能力,帮助我们从数据中学习改进、调整方向,为战场提供了强大及时的雷达监测能力,帮助我们掌控战场;移动及算法中台提供了战场一线火力支援能力,帮助我们提供更加个性化的服务,增强用户体验,为战场提供了陆军支援能力,随机应变,所向披靡;技术中台提供了自建系统部分的技术支撑能力,帮助我们解决了基础设施,分布式数据库等底层技术问题,为前台特种兵提供了精良的武器装备;研发中台提供了自建系统部分的管理和技术实践支撑能力,帮助我们快速搭建项目、管理进度、测试、持续集成、持续交付,是前台特种兵的训练基地及快速送达战场的机动运输部队;组织中台为我们的项目提供投资管理、风险管理、资源调度等,是战场的指挥部,战争的大脑,指挥前线,调度后方。所以,评判一个平台是否称得上中台,最终评判标准不是技术,也不是长什么模样,还是得前台说了算,毕竟前台才是战争的关键,是感受得到战场的残酷、看得见用户的那部分人。前台想不想用,爱不爱用,好不好用;帮了前台多大的忙,从中台获得了多大的好处,愿意掏出多少利润来帮助建设中台,这些才是甄别中台建设对错好坏的标准。对于中台来讲,前台就是用户,以用户为中心,在中台同样适用。三、中台就是「企业级能力复用平台」如果让我给出一个定义,目前我认为最贴切的应该是: 中台就是「企业级能力复用平台」。很简单,有点失望是不是?但是为了找到一个靠谱的定义,我几乎花了快两年的时间,期间有各种各样的定义曾浮现出来,但至少到目前为止,只有这个定义我觉得最贴切、最简单、也最准确,它能解释几乎所有我碰到的关于中台的问题,例如:为什么会有那么多中台,像上文提到业务中台,数据中台,搜索中台,移动中台,哪些才是中台,哪些是蹭热点的?中台与前台的划分原则是什么?中台化与平台化的区别是什么?中台化和服务化的区别是什么?中台该怎么建设?等等…这9个字看起来简单,重要的是其背后对「中台」价值的阐释,下面就让我为大家一一拆解来看。企业级当做中台建设的时候,一定是跳出单条业务线站在企业整体视角来审视业务全景,寻找可复用的能力进行沉淀,从而希望通过能力的复用一方面消除数据孤岛业务孤岛,一方面支撑企业级规模化创新,助力企业变革,滋生生态。所以虽然中台的建设过程虽然可以自下而上,以点及面。但驱动力一定是自上而下的,全局视角的,并且需要一定的顶层设计。这也解释了为什么在企业中推动中台建设的往往都是跨业务部门,例如CIO级别领导或是企业的战略规划部门,因为只有这些横跨多条业务线的角色和组织,才会去经常反思与推动企业级的能力复用问题。这一点也引出了中台建设的一个关键难点,就是组织架构的调整和演进以及利益的重新分配,这是技术所不能解决的,也是中台建设的最强阻力。同时企业级也是区分企业中台化与应用系统服务化的关键点,简而言之中台化是企业级、是全局视角,服务化更多是系统级、是局部视角。所以从中台的兴起与爆发可以看到一种趋势,就是越来越多的企业无论是由于企业运营效率的原因还是由于创新发展的需要,对于企业全局视角跨业务线的能力沉淀都提高到了前所未有的战略高度。能力提到中台,最常听到的一个词就是「能力」。可能是因为能力这个词足够简单,又有着足够的包容度与宽度。企业的能力可能包含多个维度,常见的例如计算能力,技术能力,业务能力,数据能力,AI能力,运营能力,研发能力…其中大部分的能力还可以继续细化和二次展开,从而形成一张多维度的企业能力网。可以说,中台就是企业所有可以被「多前台产品团队」复用能力的载体。复用虽然我们一直讲「去重复用」讲了很多年,但仔细想想,大多平台化建设会将重点放在「去重」(消除重复)上,而对于「复用」则没有足够的关注。很多企业都号称已经建成了各种各样成熟的平台,但是我们问心自问一下,有多少平台是业务驱动的?有多少前台产品团队又是自愿将自己的产品接入到平台上的?有多少平台建设者是在真正关注前台产品团队的平台用户体验?「去重」讲的更多是向后看,是技术驱动的;「复用」讲的更多是向前看,是业务驱动和用户驱动的。所以「去重」与「复用」虽然经常一起出现,一起被提及,但是谈论的完全不是一件事情,目的不同,难度也不同,做到「去重」已然非常困难,关注「复用」的就更是寥寥无几,所以:「复用」是中台更加关注的目标;「可复用性」和「易复用性」是衡量中台建设好坏的重要指标;「业务响应力」和「业务满意度」也才是考核中台建设进度的重要标准。而实现更好的复用,常常改进的方向有两个方面:一方面将更高抽象(例如业务模式级别)的通用业务逻辑通过抽象后下沉到中台,这样前台就会更轻,学习成本和开发维护成本更低,越能更快的适应业务变化;缺点是,抽象级别越高,越难被复用,需要架构师对于各业务有深入的理解和非常强的抽象能力。另一方面就是通过对于中台能力的SaaS化包装,减少前台团队发现中台能力和使用中台能力的阻力,甚至通过自助式(Self-Service)的方式就快速定位和使用中台能力。目前很多企业在尝试的内部API集市或是数据商店就是在这方面的努力和尝试。平台这里的平台主要是区别于大单体的应用或是系统。传统的企业数字化规划更多的是围绕业务架构,应用架构和数据架构展开。产出也是一个个基于应用和系统的数字化建设规划,例如要采购或是自建哪些具体的系统,例如ERP、CRM等。当然这个过程并没有什么问题,可以理解此时这些独立的系统就承载了企业的各种能力,由于企业各业务线统一使用一个应用或系统,也自然实现了能力的复用。但问题常常出现在两个方面:一个是大单体系统的业务响应力有限,缺少「柔性」,当业务发展到一定阶段后,必然产生大量定制化需求,随着内部定制化模块的比例逐渐上升,响应力成指数下降,成为业务的瓶颈点。另一个则是系统间的打通通常比较困难,容易形成业务孤岛和数据孤岛。所以越来越多的企业开始像互联网学习,以平台化的方式重塑企业IT架构,从而对于业务提供足够的「柔性」,来满足对于业务的快速响应和复用的需求。小结「企业级能力复用平台」这个定义虽然看起来简单,但经过这么长时间对于中台的实践与思考,我觉得如上文所述的这个定义背后所代表的意义是目前对中台价值的最贴切的阐释:「企业级」定义了中台的范围,区分开了单系统的服务化与微服务;「能力」定义了中台的主要承载对象,能力的抽象解释了各种各样中台的存在;「复用」定义了中台的核心价值,传统的平台化对于易复用性并没有给予足够的关注,中台的提出和兴起,让人们通过可复用性将目光更多的从平台内部转换到平台对于前台业务的支撑上;「平台」定义了中台的主要形式,区别于传统的应用系统拼凑的方式,通过对于更细力度能力的识别与平台化沉淀,实现企业能力的柔性复用,对于前台业务更好的支撑。有了定义后,如何建中台的思路也就豁然开朗:如果说中台是「企业级能力复用平台」的话,那中台化就是「利用平台化手段发现、沉淀与复用企业级能力的过程」。

March 29, 2019 · 1 min · jiezi

九个有用的 Laravel Eloquent 的特性

对于使用 Laravel 的开发者来说,可能都会惊叹于 Eloquent Model 的强大,但是在强大的表面之下,其实还是有很多鲜为人知的特性的,本文即来分享十个 Laravel Eloquent 的强大特性。1.更强大的 find() 方法很多开发者在使用 find() 方法的时候,通常就只是在这里传入一个 ID 的参数,其实我们也是可以传入第二个参数的:在 find() 方法中指定需要查找的字段$user = App\User::find(1, [’name’, ‘age’]);$user = App\User::findOrFail(1, [’name’, ‘age’]);// 这里面的 name 和 age 字段就是制定只查找这两个字段2.克隆 Model直接使用 replicate() 方法即可,这样我们就很容易地创建一个 Model 的副本:$user = App\User::find(1);$newUser = $user->replicate();$newUser->save();// 这样,$newUser 和 $user 的基本数据就是一样的3.检查 Model 是否相同使用 is() 方法检查两个 Model 的 ID 是否一致,是否在同一个表中:$user = App\User::find(1);$sameUser = App\User::find(1);$diffUser = App\User::find(2);$user->is($sameUser); // true$user->is($diffUser); // false4.在关联模型中同时保存数据使用 push() 你可以在保存模型数据的同时,将所关联的数据也保存下来:class User extends Model{ public function phone() { return $this->hasOne(‘App\Phone’); }}$user = User::first();$user->name = “GeiXue”;$user->phone->number = ‘1234567890’;$user->push(); // 最后这一行 push() 会将 user 的数据和 phone 的数据同时更新到数据库中5.自定义 deleted_at 字段如果你使用过 Laravel 的软删除 Soft Delete 的话,你应该就知道其实 Laravel 在标记一个记录为已删除的状态其实是用 deleted_at 这个字段来维护的,其实你是可以自定义这个字段的:class User extends Model{ use SoftDeletes; * The name of the “deleted at” column. * * @var string */ const DELETED_AT = ‘deleted_date’;}或者你这样自定义也可以:class User extends Model{ use SoftDeletes; public function getDeletedAtColumn() { return ‘deleted_date’; }}6.获取已修改的 Model 属性使用 getChanges() 方法获取已被修改的属性:$user->getChanges()//[ “name” => “GeiXue”, ]7.检查 Model 是否被修改使用 isDirty() 方法就可以检测模型中的数据是否被修改:$user = App\User::first();$user->isDirty(); //false$user->name = “GeiXue”;$user->isDirty(); //true在使用 isDirty() 的时候,你也可以直接检测某个属性是否被修改:$user->isDirty(’name’); //true$user->isDirty(‘age’); //false8.获取 Model 的原始数据在给 Model 的属性赋予新值的时候,你可以通过 getOriginal() 来获取原来的值:$user = App\User::first();$user->name; //JellyBool$user->name = “GeiXue”; //GeiXue$user->getOriginal(’name’); //JellyBool$user->getOriginal(); //Original $user record9.刷新 Model 的数据使用 refresh() 刷新 Model 的数据,这在你使用 tinker 的时候特别有用:$user = App\User::first();$user->name; // JellyBool// 这个时候在其他地方,该用户的名字被更新为 GeiXue,你可以使用 refresh 来刷新,而不用退出 tinker$user->refresh(); $user->name; // GeiXue最后上面的九个 Eloquent 特性其实在特定的应用场景是非常有用的,希望能在你开发 Laravel 项目的时候帮到你一点点。Happy Hacking ...

March 26, 2019 · 1 min · jiezi

干货:构建复杂的 Eloquent 搜索过滤

最近,我需要在开发的事件管理系统中实现搜索功能。 一开始只是简单的几个选项 (通过名称,邮箱等搜索),到后面参数变得越来越多。今天,我会介绍整个过程以及如何构建灵活且可扩展的搜索系统。如果你想查看代码,请访问 Git 仓库 。我们将创造什么我们公司需要一种跟踪我们与世界各地客户举办的各种活动和会议的方式。我们目前的唯一方法是让每位员工在 Outlook 日程表上存储会议的详细信息。可拓展性较差!我们需要公司的每个人都可以访问,来查看我们客户的被邀请的详细信息以及他们的RSVP(国际缩用语:请回复)状态。这样,我们可以通过上次与他们互动的数据来确定哪些用户可以邀请来参加未来的活动。使用高级搜索过滤器查找的截图查找用户常用过滤用户的方法:通过姓名,电子邮件,位置通过用户工作的公司被邀请参加特定活动的用户参加过特定活动的用户邀请及已参加活动的用户邀请但尚未回复的用户答应参加但未出席的用户分配给销售经理的用户虽然这个列表不算完整,但可以让我们知道需要多少个过滤器。这将是个挑战!前端的条件过滤的截图。模型及模型关联在这个例子中我们回用到很多模型:User — 代表被邀请参加活动的用户。一个用户可以参加很多活动。Event — 代表我公司举办的活动。活动可以有多个。Rsvp — 代表用户对活动邀请的回复。一个用户对一个活动的回复是一对一的。Manager — 一个用户可以对应多个我公司的销售经理.搜索的需求在开始代码之前,我想先把搜索的需求明确一下。也就是说我要很清楚地知道我要对哪些数据做搜索功能。下面就是一个例子:{ “name”: “Billy”, “company”: “Google”, “city”: “London”, “event”: “key-note-presentation-new-york-05-2016”, “responded”: true, “response”: “I will be attending”, “managers”: [ “Tom Jones”, “Joe Bloggs” ],}总结一下上面数据想表达的搜索的条件:客人的名字是 ‘Billy’,来自 ‘Google’ 公司,目前居住在 ‘London’,已经对 ‘key-note-presentation-new-york-05–2016’ 的活动邀请做出了回复,并且回复的内容是 ‘I will be attending’,负责跟进这位客人的销售经理是 ‘Tom Jones’ 或者 ‘Joe Bloggs’。开始 — 最佳实践我是一个坚定不移的极简主义者,我坚信少即是多。下面就让我们以最简单的方式探索出解决这个需求的最佳实践。首先,在 routes.php 文件中添加如下代码:Route::post(’/search’, ‘SearchController@filter’);接下来,创建 SearchController.php artisan make:controller SearchController添加前面路由中明确的 filter() 方法:<?phpnamespace App\Http\Controllers;use App\User;use App\Http\Requests;use Illuminate\Http\Request;use App\Http\Controllers\Controller;class SearchController extends Controller{ public function filter(Request $request, User $user) { // }}由于我们需要在 filter 方法中处理请求提交的数据,所以我把 Request 类做了依赖注入。Laravel 的服务容器 会解析这个依赖,我们可以在方法中直接使用 Request 的实例,也就是 $request。User 类也是同样道理,我们需要从中检索一些数据。这个搜索需求有一点比较麻烦的是,每个参数都是可选的。所以我们要先写一系列的条件语句来判断每个参数是否存在:这是我初步写出来的代码:public function filter(Request $request, User $user){ // 根据姓名查找用户 if ($request->has(’name’)) { return $user->where(’name’, $request->input(’name’))->get(); } // 根据公司名查找用户 if ($request->has(‘company’)) { return $user->where(‘company’, $request->input(‘company’)) ->get(); } // 根据城市查找用户 if ($request->has(‘city’)) { return $user->where(‘city’, $request->input(‘city’))->get(); } // 继续根据其他条件查找 // 再无其他条件, // 返回所有符合条件的用户。 // 在实际项目中需要做分页处理。 return User::all();}很明显,上面的代码逻辑是错误的。首先,它只会根据一个条件去检索用户表,然后就返回了。所以,通过上面的代码逻辑,我们根本无法获得姓名为 ‘Billy’, 而且住在 ‘London’ 的用户。实现这种目的的一种方式是嵌套条件:// 根据用户名搜索用户if ($request->has(’name’)) { // 是否还提供了 ‘city’ 搜索参数 if ($request->has(‘city’)) { // 基于用户名及城市搜索用户 return $user->where(’name’, $request->input(’name’)) ->where(‘city’, $request->input(‘city’)) ->get(); } return $user->where(’name’, $request->input(’name’))->get();}我确信你可以看到这在两个或者三个参数的时候起作用,但是一旦我们添加更多选项,这将会难以管理。改进我们的搜索 api所以我们如何让这个生效,而同时不会因为嵌套条件而变得疯狂?我们可以使用 User 模型继续重构,来使用 builder 而不是直接返回模型。public function filter(Request $request, User $user){ $user = $user->newQuery(); // 根据用户名搜索用户 if ($request->has(’name’)) { $user->where(’name’, $request->input(’name’)); } // 根据用户公司信息搜索用户 if ($request->has(‘company’)) { $user->where(‘company’, $request->input(‘company’)); } // 根据用户城市信息搜索用户 if ($request->has(‘city’)) { $user->where(‘city’, $request->input(‘city’)); } // 继续执行其他过滤 // 获得并返回结果 return $user->get();}好多了!我们现在可以将每个搜索参数做为修饰符添加到从 $user->newQuery() 返回的查询实例中。我们现在可以根据所有的参数来做搜索了, 再多参数都不怕.一起来实践吧:$user = $user->newQuery();// 根据姓名查找用户if ($request->has(’name’)) { $user->where(’name’, $request->input(’name’));}// 根据公司名查找用户if ($request->has(‘company’)) { $user->where(‘company’, $request->input(‘company’));}// 根据城市查找用户if ($request->has(‘city’)) { $user->where(‘city’, $request->input(‘city’));}// 只查找有对接我公司销售经理的用户if ($request->has(‘managers’)) { $user->whereHas(‘managers’, function ($query) use ($request) { $query->whereIn(‘managers.name’, $request->input(‘managers’)); });}// 如果有 ’event’ 参数if ($request->has(’event’)) { // 只查找被邀请的用户 $user->whereHas(‘rsvp.event’, function ($query) use ($request) { $query->where(’event.slug’, $request->input(’event’)); }); // 只查找回复邀请的用户( 以任何形式回复都可以 ) if ($request->has(‘responded’)) { $user->whereHas(‘rsvp’, function ($query) use ($request) { $query->whereNotNull(‘responded_at’); }); } // 只查找回复邀请的用户( 限制回复的具体内容 ) if ($request->has(‘response’)) { $user->whereHas(‘rsvp’, function ($query) use ($request) { $query->where(‘response’, ‘I will be attending’); }); }}// 最终获取对象并返回return $user->get();搞定,棒极了!是否还需要重构?通过上面的代码我们实现了业务需求,可以根据搜索条件返回正确的用户信息。但是我们能说这是最佳实践吗?显然是不能。现在是通过一系列条件判断的嵌套来实现业务逻辑,而且所有的逻辑都在控制器里,这样做真的合适吗?这可能是一个见仁见智的问题,最好还是结合自己的项目,具体问题具体分析。如果你的项目比较小,逻辑相对简单,而且只是一个短期需求的项目,那么就不必纠结这个问题了,直接照上面的逻辑写就好了。 然而,如果你是在构建一个比较复杂的项目,那么我们还是需要更加优雅且扩展性好的解决方案。编写新的搜索 api当我要写一个功能接口的时候,我不会立刻去写核心代码,我通常会先想想我要怎么用这个接口。这可能就是俗称的「面向结果编程」(或者说是「结果导向思维」)。「在你写一个组件之前,建议你先写一些要用这个组件的测试代码。通过这种方式,你会更加清晰地知道你究竟要写哪些函数,以及传哪些必要的参数,这样你才能写出真正好用的接口。因为写接口的目的是简化使用组件的代码,而不是简化接口自身的代码。」 ( 摘自: c2.com)根据我的经验,这个方法能帮助我写出可读性更强,更加优雅的程序。还有一个很大的额外收获就是,通过这种阶段性的验收测试,我能更好地抓住商业需求。因此,我可以很自信地说我写的程序可以很好地满足市场的需求,具有很高商业价值。以下添加到搜索功能的代码中,我希望我的搜索 api 是这样写的:return UserSearch::apply($filters);这样有着很好的可读性。 根据经验, 如果我查阅代码能想看文章的句子一样,那会非常美妙。像刚刚的情况下:搜索用户时加上一个过滤器再返回搜索结果。这对技术人员和非技术人员都有意义。我想我需要新建一个 UserSearch 类,还需要一个静态的 apply 函数来接收过滤条件。让我开始吧:<?phpnamespace App\Search;use Illuminate\Http\Request;class UserSearch{ public static function apply(Request $filters) { // 返回搜索结果 }}最简单的方式,让我们把控制器中的代码复制到 apply 函数中:<?phpnamespace App\UserSearch;use App\User;use Illuminate\Http\Request;class UserSearch{ public static function apply(Request $filters) { $user = (new User)->newQuery(); // 基于用户名搜索 if ($filters->has(’name’)) { $user->where(’name’, $filters->input(’name’)); } // 基于用户的公司名搜索 if ($filters->has(‘company’)) { $user->where(‘company’, $filters->input(‘company’)); } // 基于用户的城市名搜索 if ($filters->has(‘city’)) { $user->where(‘city’, $filters->input(‘city’)); } // 只返回分配了销售经理的用户 if ($filters->has(‘managers’)) { $user->whereHas(‘managers’, function ($query) use ($filters) { $query->whereIn(‘managers.name’, $filters->input(‘managers’)); }); } // 搜索条件中是否包含 ’event’ ? if ($filters->has(’event’)) { // 只返回被邀请参加了活动的用户 $user->whereHas(‘rsvp.event’, function ($query) use ($filters) { $query->where(’event.slug’, $filters->input(’event’)); }); // 只返回以任何形式答复了邀请的用户 if ($filters->has(‘responded’)) { $user->whereHas(‘rsvp’, function ($query) use ($filters) { $query->whereNotNull(‘responded_at’); }); } // 只返回以某种方式答复了邀请的用户 if ($filters->has(‘response’)) { $user->whereHas(‘rsvp’, function ($query) use ($filters) { $query->where(‘response’, ‘I will be attending’); }); } } // 返回搜索结果 return $user->get(); }}我们做了一系列的改变。 首先, 我们将在控制器中的 $request 变量更名为 filters 来提高可读性。其次,由于 newQuery() 方法不是静态方法,无法通过 User 类静态调用,所以我们需要先创建一个 User 对象,再调用这个方法:$user = (new User)->newQuery();调用上面的 UserSearch 接口,对控制器的代码进行重构:<?phpnamespace App\Http\Controllers;use App\Http\Requests;use App\Search\UserSearch;use Illuminate\Http\Request;use App\Http\Controllers\Controller;class SearchController extends Controller{ public function filter(Request $request) { return UserSearch::apply($request); }}好多了,是不是?把一系列的条件判断交给专门的类处理,使控制器的代码简介清新。下面进入见证奇迹的时刻在这篇文章的例子中,一共有 7 个过滤条件,但是现实的情况是更多更多。所以在这种情况下,只用一个文件来处理所有的过滤逻辑,就显得差强人意了。扩展性不好,而且也不符合 S.O.L.I.D. principles 原则。目前,apply() 方法需要处理这些逻辑:检查参数是否存在把参数转成查询条件执行查询如果我想增加一个新的过滤条件,或者修改一下现有的某个过滤条件的逻辑,我都要不停地修改 UserSearch 类,因为所有过滤条件的处理都在这一个类里,随着业务逻辑的增加,会有点尾大不掉的感觉。所以对每个过滤条件单独建个类文件是非常有必要的。先从 Name 条件开始吧。但是,就像我们前面讲的,还是想一下我们需要怎样使用这种单一条件过滤的接口。我希望可以这样调用这个接口:$user = (new User)->newQuery();$user = static::applyFiltersToQuery($filters, $user);return $user->get();不过这里再使用 $user 这个变量名就不合适了,应该用 $query 更有意义。public static function apply(Request $filters){ $query = (new User)->newQuery(); $query = static::applyFiltersToQuery($filters, $query); return $query->get();}然后把所有条件过滤的逻辑都放到 applyFiltersToQuery() 这个新接口里。下面开始创建第一个条件过滤类:Name.<?phpnamespace App\UserSearch\Filters;class Name{ public static function apply($builder, $value) { return $builder->where(’name’, $value); }}在这个类里定义一个静态方法 apply(),这个方法接收两个参数,一个是 Builder 实例,另一个是过滤条件的值( 在这个例子中,这个值是 ‘Billy’ )。然后带着这个过滤条件返回一个新的 Builder 实例。接下来是 City 类:<?phpnamespace App\UserSearch\Filters;class City{ public static function apply($builder, $value) { return $builder->where(‘city’, $value); }}如你所见,City 类的代码逻辑跟 Name 类相同,只是过滤条件变成了 ‘city’。让每个条件过滤类都只有一个简单的 apply() 方法,而且方法接收的参数和处理的逻辑都相同,我们可以把这看成一个协议,这一点很重要,下面我会具体说明。为了确保每个条件过滤类都能遵循这个协议,我决定写一个接口,让每个类都实现这个接口。<?phpnamespace App\UserSearch\Filters;use Illuminate\Database\Eloquent\Builder;interface Filter{ /** * 把过滤条件附加到 builder 的实例上 * * @param Builder $builder * @param mixed $value * @return Builder $builder / public static function apply(Builder $builder, $value);}我为这个接口的方法写了详细的注释,这样做的好处是,对于每一个实现这个接口的类,我都可以利用我的 IDE ( PHPStorm ) 自动生成同样的注释。下面,分别在 Name 和 City 类中实现这个 Filter 接口:<?phpnamespace App\UserSearch\Filters;use Illuminate\Database\Eloquent\Builder;class Name implements Filter{ /* * 把过滤条件附加到 builder 的实例上 * * @param Builder $builder * @param mixed $value * @return Builder $builder / public static function apply(Builder $builder, $value) { return $builder->where(’name’, $value); }}以及<?phpnamespace App\UserSearch\Filters;use Illuminate\Database\Eloquent\Builder;class City implements Filter{ /* * 把过滤条件附加到 builder 的实例上 * * @param Builder $builder * @param mixed $value * @return Builder $builder */ public static function apply(Builder $builder, $value) { return $builder->where(‘city’, $value); }}完美。现在已经有两个条件过滤类完美地遵循了这个协议。把我的目录结构附在下面给大家参考一下:这是到目前为止关于搜索的文件结构。我把所有的条件过滤类的文件放在一个单独的文件夹里,这让我对已有的过滤条件一目了然。使用新的过滤器现在回过头来看 UserSearch 类的 applyFiltersToQuery() 方法,发现我们可以再做一些优化了。首先,把每个条件判断里构建查询语句的工作,交给对应的过滤类去做。// 根据姓名搜索用户if ($filters->has(’name’)) { $query = Name::apply($query, $filters->input(’name’));}// 根据城市搜索用户if ($filters->has(‘city’)) { $query = City::apply($query, $filters->input(‘city’));}现在根据过滤条件构建查询语句的工作已经转给各个相应的过滤类了,但是判断每个过滤条件是否存在的工作,还是通过一系列的条件判断语句完成的。而且条件判断的参数都是写死的,一个参数对应一个过滤类。这样我每增加一个新的过滤条件,我都要重新修改 UserSearch 类的代码。这显然是一个需要解决的问题。其实,根据我们前面介绍的命名规则, 我们很容易把这段条件判断的代码改成动态的:AppUserSearchFiltersNameAppUserSearchFiltersCity就是结合命名空间和过滤条件的名称,动态地创建过滤类(当然,要对接收到的过滤条件参数做适当的处理)。大概就是这个思路,下面是具体实现:private static function applyFiltersToQuery( Request $filters, Builder $query) { foreach ($filters->all() as $filterName => $value) { $decorator = NAMESPACE . ‘\Filters\’ . str_replace(’ ‘, ‘’, ucwords( str_replace(’’, ’ ‘, $filterName))); if (class_exists($decorator)) { $query = $decorator::apply($query, $value); } } return $query;}下面逐行分析这段代码:foreach ($filters->all() as $filterName => $value) {遍历所有的过滤参数,把参数名(比如 city)赋值给变量 $filterName,参数值(比如 London)复制给变量 $value。$decorator = NAMESPACE . ‘\Filters\’ . str_replace(’ ‘, ‘’, ucwords( str_replace(’’, ’ ‘, $filterName)));这里是对参数名进行处理,将下划线改成空格,让每个单词都首字母大写,然后去掉空格,如下例子:“name” => App\UserSearch\Filters\Name,"company" => App\UserSearch\Filters\Company,"city" => App\UserSearch\Filters\City,"event" => App\UserSearch\Filters\Event,"responded" => App\UserSearch\Filters\Responded,"response" => App\UserSearch\Filters\Response,"managers" => App\UserSearch\Filters\Managers如果有参数名是带下划线的,比如 has_responded,根据上面的规则,它将被处理成 HasResponded,因此,其相应的过滤类的名字也要是这个。if (class_exists($decorator)) {这里就是要先确定这个过滤类是存在的,再执行下面的操作,否则在客户端报错就尴尬了。$query = $decorator::apply($query, $value);这里就是神器的地方了,PHP 允许把变量 $decorator 作为类,并调用其方法(在这里就是 apply() 方法了)。现在再看这个接口的代码,发现我们再次实力证明了磨刀不误砍柴工。现在我们可以确保每个过滤类对外响应一致,内部又可以分别处理各自的逻辑。最后的优化现在 UserSearch 类的代码应该已经比之前好多了,但是,我觉得还可以更好,所以我又做了些改动,这是最终版本:<?phpnamespace App\UserSearch;use App\User;use Illuminate\Http\Request;use Illuminate\Database\Eloquent\Builder;class UserSearch{ public static function apply(Request $filters) { $query = static::applyDecoratorsFromRequest( $filters, (new User)->newQuery() ); return static::getResults($query); } private static function applyDecoratorsFromRequest(Request $request, Builder $query) { foreach ($request->all() as $filterName => $value) { $decorator = static::createFilterDecorator($filterName); if (static::isValidDecorator($decorator)) { $query = $decorator::apply($query, $value); } } return $query; } private static function createFilterDecorator($name) { return return NAMESPACE . ‘\Filters\’ . str_replace(’ ‘, ‘’, ucwords(str_replace(’_’, ’ ‘, $name))); } private static function isValidDecorator($decorator) { return class_exists($decorator); } private static function getResults(Builder $query) { return $query->get(); }}我最后决定去掉 applyFiltersToQuery() 方法,是因为感觉跟接口的主要方法名 apply() 有点冲突了。而且,为了贯彻执行单一职责原则,我把原来 applyFiltersToQuery() 方法里比较复杂的逻辑又做了拆分,为动态创建过滤类名称,和确认过滤类是否存在的判断,都写了单独的方法。这样,即便要扩展搜索接口,我也不需要再去反复修改 UserSearch 类里的代码了。需要增加新的过滤条件吗?简单,只要在 App\UserSearch\Filters 目录下创建一个过滤类,并使之实现 Filter 接口就 OK 了。结论我们已经把一个拥有所有搜索逻辑的巨大控制器方法保存成一个允许打开过滤器的模块化过滤系统,而不需要添加修改核心代码。 像评论里 @rockroxx所建议的,另一个重构的方案是把所有方法提取到 trait 并将 User 设置成 const 然后由 Interface 实现。class UserSearch implements Searchable { const MODEL = App\User; use SearchableTrait;}如果你很好的理解了这个设计模式,你可以 利用多态代替多条件。代码会提交到 GitHub 你可以 fork,测试和实验。如何解决多条件高级搜索,我希望你能留下你的想法、建议和评论。文章转自:https://learnku.com/laravel/t… 更多文章:https://learnku.com/laravel/c… ...

March 26, 2019 · 5 min · jiezi

laravel 使用扩展包生成二维码

导语之前介绍过 composer 的作用,可以很方便的管理包,同时 laravel 的开发者众多,因此有很多扩展包可以使用。本篇文章记录下用扩展包生成二维码。代码可查看 GitHub。composer 安装以及配置使用 Simple Qrcode 扩展包来生成二维码,将其配置到 laravel 中共需要三步。使用 composer require simplesoftwareio/simple-qrcode 1.3.* 安装在 config/app.php 中注册服务提供者 SimpleSoftwareIO\QrCode\QrCodeServiceProvider::class, 如下继续在 config/app.php 中添加门面 ‘QrCode’ => SimpleSoftwareIO\QrCode\Facades\QrCode::class,如下经过以上三个步骤,在 laravel 中就可以使用 QrCode 来生成二维码了。实际中通过 composer 加载的包都是以上步骤,门面可以选择不添加。使用定义好路由之后,测试下。可以使用门面,也可以实例化,都是一样的。完整代码查看 GitHub直接生成二维码 QrCode::generate(date(‘Y-m-d H:i:s’));,访问后看到如下好小,可以设置下尺寸 QrCode::size(200)->generate(date(‘Y-m-d H:i:s’));可以将生成的图片保存 $qr->generate(‘hello world’, $path.‘qr1.svg’);,第二个参数就是图片保存的路径默认是保存 svg 格式,可以指定图片格式 $qr->format(‘png’)->generate(‘hello world’, $path.‘qr2.png’);最后再来看下在视图中怎么使用 {!! QrCode::size(200)->generate(‘hello world’); !!},一行代码即可。还有更多的方法,包括设置颜色、边框、编码、合并图片等,可以查看下方参考资料。参考资料:在 Laravel 5 中通过 Simple QrCode 扩展包生成二维码详解、Simple Qrcode。

March 25, 2019 · 1 min · jiezi

laravel常用路径保存

laravel框架常用目录路径app_path()app_path函数返回app目录的绝对路径:$path = app_path();你还可以使用app_path函数为相对于app目录的给定文件生成绝对路径:$path = app_path(‘Http/Controllers/Controller.php’);base_path()base_path函数返回项目根目录的绝对路径:$path = base_path();你还可以使用base_path函数为相对于应用目录的给定文件生成绝对路径:$path = base_path(‘vendor/bin’);config_path()config_path函数返回应用配置目录的绝对路径:$path = config_path();database_path()database_path函数返回应用数据库目录的绝对路径:$path = database_path();public_path()public_path函数返回public目录的绝对路径:$path = public_path();storage_path()storage_path函数返回storage目录的绝对路径:$path = storage_path();还可以使用storage_path函数生成相对于storage目录的给定文件的绝对路径:$path = storage_path(‘app/file.txt’);获取laravel项目的路径的内置帮助函数基本都在这了转载自:花儿为何那样红

March 25, 2019 · 1 min · jiezi

联科首个开源项目启动!未来可期,诚邀加入!

OpenEA开源组织是广州市联科软件有限公司旗下的一个“开放·共享·全球化”的开源组织。OpenEA全称“Open+Enterprise+Application”,意为开放的企业应用,致力让所有企业都能轻松用上流程应用开发平台。2019年联科将逐步开源基于流程应用的快速开发平台,以“专业·高效·创新·自由·开放·回馈”为理念,以“开源之林”为目标,构建开放的技术生态圈。最近【流程设计器组件FlowDesigner】作为第一个开源项目,主要用于设计和控制流程各个运转过程,欢迎广大技术好友前来码云交流探讨!流程设计器组件FlowDesigner前言FlowDesigner来源于Linkey BPM中的流程设计器,作用于流程运行过程中的图形描述。它的操作简捷轻巧,能快速绘制出流程图。组件单独也可以使用,并能嵌入到任何需要该组件的系统中。分享,是“开源”的真谛。机不可失失不再来,准备好加入我们了吗?立即前往码云Fork项目吧,地址:https://gitee.com/openEA/Flow…

March 25, 2019 · 1 min · jiezi

Linux 搭建 laravel 项目

导语LNMP 环境已经搭建完毕,下面就是用 laravel 框架搭建个项目。下载源码下载源码的方式有很多,这里使用 composer 。前文已经讲了如何安装 composer,这里不再赘述。在 /usr/local/nginx/html/ 目录中输入 composer create-project laravel/laravel myLaravel 即可创建,关于 create-project 的参数,可以查看下方参考资料中的链接设置目录权限源码下载完成,接下来是设置目录权限。创建用户以及分组 useradd -g www-data www-data。修改 nginx 用户,编辑 /usr/local/nginx/conf/nginx.conf,修改完成后记得重启服务修改 PHP 用户,编辑 /usr/local/php/etc/php-fpm.d/www.conf 文件,同样修改完成记得重启服务项目中目录权限为 755,文件为 644。在 html 目录中依次执行 chown -R www-data.www-data ./myLaravel/ 、find ./myLaravel/ -type d -exec chmod 755 {} ;、find ./myLaravel/ -type f -exec chmod 644 {} ;修改 Nginx 配置最后一步是修改 Nginx 的配置, vim /usr/local/nginx/conf/nginx.conf 修改如下几处修改完成后重启 Nginx。访问可以看出 laravel 默认的首页参考资料:create-project、安装配置、设置权限、请给你的 Laravel 一份更好的 Nginx 配置。

March 19, 2019 · 1 min · jiezi

Laravel + Laravel-echo + EasyWeChat 实现微信扫码登录

扫码登录成为一种日趋流行的登录方式,它具有较高的安全性,同时又使我们从记忆大量的账号密码并手动输入的繁琐流程中解脱出来,有些平台甚至无账号也能扫码登录,连注册的麻烦都省了。对于接入微信开放平台的公众号应用来说,实现扫码登录是相当容易的,有 EasyWeChat SDK 加持,再按着官方的文档一把梭,很快就能完成。然而本文所要讨论的是另一种情况,有时候出于某些原因,自己的公众号不能接入开放平台,但又想进行微信扫码登录,这种情况下显示就不能再换官方的套路来了。但只要我们稍作变通,就能实现这一需求。基本思路:登录页显示微信二维码(使用 EasyWeChat SDK 创建,短时效的临时二维码)用户扫码后推送消息到服务器接口,接口中根据业务情况进行判断处理,符合条件时触发 WechatScanLogin 事件WechatScanLogin 事件实现 ShouldBroadcast 接口,所以当它被触发时也会向指定的频道进行广播前端 laravel-echo 监听频道中用户扫码登录的消息并进行处理以下就来介绍一下具体实现,先放效果图。具体实现配合本文,我创建了一个简单的示例项目,有兴趣的可以克隆下来,配合源码一起服用,效果更佳。项目地址:https://github.com/tianyong90/laravel-qrcode-login首先当然是创建 Laravel 项目,同时安装前后端依赖前端最主要依赖是 laravel-echo 和 socket.io-client前端监听事件广播是关键,我们需要一个 websocket 服务端,Laravel 官方文档在介绍消息广播时提到了 Pusher 和 laravel-echo-server。因为我使用 laradock 作为开发环境,其中内置了 laravel-echo-server 容器,十分方便,所以决定直接用它。实际上也可以使用 Pusher 服务,那么则需要安装 pusher.js 替代 socket.io-client,同时在 .env 中修改相关配置配置项目主要是配置数据库和 redis 连接,然后把 BROADCAST_DRIVER 设置为 redis(这一点很重要,如果使用 pusher 则需要修改为 pusher)如果 QUEUE_CONNECTION 设置为 redis 了,则需要记得启动队列 worker.启动 laravel-echo-server因为使用 laradock,所以只需要启动时带上 laravel-echo-server 参数就可以了,进入 laradock 目录docker-compose up -d nginx php-worker nginx mysql redis laravel-echo-server创建 WechatScanLogin 事件php artisan make:event WechatScanLoginclass WechatScanLogin implements ShouldBroadcast{ use Dispatchable, InteractsWithSockets, SerializesModels; public $token; /** * Create a new event instance. * * @param $token / public function __construct($token) { $this->token = $token; } /* * Get the channels the event should broadcast on. * * @return \Illuminate\Broadcasting\Channel|array */ public function broadcastOn() { return new Channel(‘scan-login’); }}上面最关键的就是事件要实现 ShouldBroadcast 接口并在 broadcastOn 方法中指定要广播的频道。WechatScanLogin 的公开属性 token 会自动包含在广播数据中。对接微信消息服务器laravel-wechat 的相关配置和对接,请阅读 EasyWeChat SDK 官方文档。接收扫码的消息并进行相关处理。public function serve(){ $app = app(‘wechat.official_account’); $app->server->push(function ($message) { if ($message[‘Event’] === ‘SCAN’) { $openid = $message[‘FromUserName’]; $user = User::where(‘openid’, $openid)->first(); if ($user) { // TODO: 这里根据情况加入其它鉴权逻辑 // 使用 laravel-passport 的个人访问令牌 $token = $user->createToken($user->name)->accessToken; // 广播扫码登录的消息,以便前端处理 event(new WechatScanLogin($token)); \Log::info(‘haha login’); return ‘登录成功!’; } return ‘失败鸟’; } else { // TODO: 用户不存在时,可以直接回返登录失败,也可以创建新的用户并登录该用户再返回 return ‘登录失败’; } }, \EasyWeChat\Kernel\Messages\Message::EVENT); return $app->server->serve();}使用 EasyWeChat 创建临时二维码并在页面中显示。public function index(){ $wechat = app(‘wechat.official_account’); $result = $wechat->qrcode->temporary(‘foo’, 600); $qrcodeUrl = $wechat->qrcode->url($result[’ticket’]); return view(‘index’, compact(‘qrcodeUrl’));}<img src={{ qrcodeUrl }} />>前端使用 laravel-echo 订阅对应的微信扫码登录事件,接收其中的 token 并存入本地存储作为判断是否登录的凭据,同时这个 token 也将作为访问后端 api 的授权依据。注意前面的代码中,使用了 laravel-passport 生成这个个人访问令牌,如果不了解这部分原理,请查阅 Laravel 官方文档。import Echo from ’laravel-echo’import io from ‘socket.io-client’window.io = iolet EchoInstance = new Echo({ broadcaster: ‘socket.io’, host: window.location.hostname + ‘:6001’,})EchoInstance.channel(‘scan-login’).listen(‘WechatScanLogin’, e => { localStorage.setItem(‘my_token’, this.token) // 其它处理 })总结至此,简单的扫码登录就完成了。当然,本文示例代码不怎么优雅、流程可能也有不完善的地方,主要是为了提供一个大致思路。有了这个思路,我们可以实现其它诸如扫码签到、扫码投票等各种功能,具体如何就看大家的创意了。最后放上个人博客地址:https://tianyong90.com/, 欢迎各位大佬批评指正。 ...

March 19, 2019 · 2 min · jiezi

laravel 简单入门

0、最少必要知识0.1 route 配置/routes/web.php 文件 Route::get(’/’,‘SitesController@index’); 0.2 在 controller 的 index 方法体中与视图交互// url 视图路径, 默认 prefix 为 resources/views/// data 可传数组/ 二维用 compact(‘data’)return view(url, data)0.3 视频里的流程控制与循环 0.3.1 @if @foreach 0.3.2 @endif @endforeach0.4 Migration 数据库一键迁移,数据库生成通过migration进行0.5 Eloquent ORM 对象关系映射,将数据表映射为对象0.6 model 的两种常见用法 0.6.1 setAttribute, 增加和修改用 0.6.2 queryScope, 查询设置过滤条件 0.7 表单验证,可以通过 Request 类完成0.8 编辑文章,Form::model()0.9 Restful API,操作与之对应的 HTTP 传值方式 增 post 删 delete 改 patch/put 查 get1、从安装到启动(mac)1.1 安装,版本号 Laravel Framework 5.8.3 1.1.1 使用 composer 全局安装,会生成 laravel 命令 composer global require “laravel/installer” 1.1.2 Debug, 运行 laravel 命令 如果找不到命令,创建软链接 ln -s ~/.composer/vendor/bin/laravel /usr/local/bin/laravel1.2 创建项目 1.2.1 方法一:使用 laravel 安装 laravel new my-project 1.2.1 方法二:使用 composer 安装 composer create-project laravel/laravel my-project 1.3 启动 1.3.1 方法一:使用 php 内置服务器 php -S localhost:8888 -t public 1.3.2 方法二:使用 laravel 提供的命令行工具 php artisan serve2、路由及其他目录| - routes| - - web.php web 的路由,可指指向控制器方法| - app| - - Http| - - - Controllers| - - - - SitesController 控制器| - resources| - - views 视图文件| - env 配置参数,如 mysql 相关配置3、可以将路由交给控制器,生成控件器web.php 文件里的写法如下:3.1 Route::get(’/’,‘SitesController@index’);3.2 注册标准路由 Route::resource(‘articles’, ‘ArticlesController’);3.3 查看当前路由 php artisan route:list4、向视图传递变量,blade 模板的用法4.1 controller 文件的方法中 4.1.1 方法一,传键值 view(‘index’)->with(key, value); 4.1.2 方法二,传一维数组,在模块中可以直接用其中的元素 view(‘index’, $data) // $data=[’name’=>‘Pual’, ‘sex’=>‘male’] 4.1.3 方法三,传二维数组,视图中需要先遍历 view(‘index’, compact($data)) 4.2 index.blade.php 在视图文件中使用变量 4.2.1 转义,相当于 php 的单引号,类似于 Vue {{ $key }} 4.2.2 不转义,相当于 php 的双引号 {!! $key !!} 5、模板5.1 定义视图模板 app.blade.php @yield(‘content’)5.2 继承模板 @extends(‘app’) @section(‘content’) This is a test @stop6、模板判断@if($name == ‘Paul’) This is Paul@else This is not Pual@endif7、循环输出@foreach($people as $person) {{$person}}@endforeach8、配置8.1 文件位置 .env9、Migration 数据库的版本控制,针对数据的迁移9.1 同步已经有的表 php artisan migrate 9.2 创建表 php artisan make:migration create_articles_table –create=‘articles’ 9.3 为已有表添加字段 php artisan make:migration add_intro_column_to_articles –table=articles10、Eloquent 是 laravel 的 ORM(对象关系映射)10.1 创建model php artisan make:model Articles10.2 model 关联 数据库11、简单 Blog,的几个知识点11.1 视图中url链接,拼接跳转链接,并带参数 {{ url(‘url’, $para) }}11.2 接收参数 11.2.1 设置带参路由,web.php ,关键点:{id} Route::get(‘articles/{id}’, ‘ArticlesController@show’); 11.2.2 在 controller 中接收参数 function show($id){…}12、laravel Forms 使用12.1 安装 composer 依赖包,laravel 官方依赖包 composer require laravelcollective/html12.2 配置 config -> app.php 12.2.1 prividers 数组追加 Collective\Html\HtmlServiceProvider::class, 12.2.2 aliases 数组追回 ‘Form’=>Collective\Html\FormFacade::class, ‘Html’=>Collective\Html\HtmlFacade::class 12.3 使用 12.3.1 表单开发提交数据 {!! Form::open([‘url’=>‘orders’]) !!} {!! Form::close() !!} 12.3.2 表单绑定模型 {!! Form::model($order,[‘method’=>‘patch’, ‘url’=>‘orders/’.$order->id]) !!} {!! Form::close() !!} 12.4 Form 静态方法有哪些? 参考网址:https://github.com/LaravelCollective/laravel-docs/blob/5.6/html.md 13、setAttribute 与 queryScope,特定作用的函数13.1 setAttribute 对数据库相关字段做预处理,插入或者更新数据的时候用 public function setPublishedAtAttribute($data) { $this->attributes[‘published_at’] = Carbon::createFromFormat(‘Y-m-d’, $data); }12.2 queryScope 字段过滤条件,在查询时使用 public function scopePublished($query) { $query->where(‘published_at’, ‘<=’, Carbon::now()); }14、Carbon 类 laravel 中默认的时间处理类15、表单验证15.1 使用 Request 类 15.1.1 创建 Request 类 php artisan make:request CreateOrderRequest 15.1.2 rules 验证 public function rules() { return [ // | 分隔符,min:3 最少3个长度 ’thing’=>‘required|min:3’, ‘price’=>‘required’, ’note’=>‘required’, ]; } 15.2 validate 验证 $this->validate($request, [’title’=>‘required’, ‘content’=>‘required’]); 16、编辑文章16.1 显示某条记录 {!! Form::model($order,[‘method’=>‘patch’,‘url’=>‘orders/’.$order->id]) !!} {!! Form::close() !!}16.2 提交修改 method:patch 相当于 put,专门用来做局部修改 17、用户注册和登录17.1 安装 Auth 模块 php artisan make:auth17.2 访问登录 url/login参考教程:https://study.163.com/course/… ...

March 18, 2019 · 2 min · jiezi

nginx配置laravel项目

server{ listen 443; server_name 10.0.3.15 ssl on; ssl_certificate /etc/nginx/conf.d/3i.crt; ssl_certificate /etc/nginx/conf.d/3i_nopass.key; root /var/www/html/3i-mobile/public; index index.php; location /{ try_files $uri $uri/ /index.php?$query_string; } #error_page 404 /404.html error_page 500 502 503 504 /50x.html; location = /50x.html{ root /usr/share/nginx/html; } location ~ .php${ root html; fastcgi_pass 127.0.0.1:9000; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME /var/www/html/3i-mobile/public/$fastcgi_script_name; include fastcgi_params; }}对应的Php配置文件user = daemongroup = daemonlisten = 127.0.0.1:9000listen = allowed_clients = 127.0.0.1pm = dynamicpm.max_children = 80pm.start_servers = 5pm.min_spare_servers = 5pm.max_spare_servers = 35slowlog = /var/log/php-fpm/www-slow.logphp_admin_value[error_log] = /var/log/php-fpm/www-error.logphp_admin_flag[log_errors] = onphp_value[session.save_handler] = filesphp_value[session.save_path] = /var/lib/php/sessionphp_value[soap.wsdl_cache_dir] = /var/lib/php/wsdlcache ...

March 18, 2019 · 1 min · jiezi

WebGeeker-Validation: 一个强大的 PHP 参数验证器

用于对API接口的请求参数进行合法性检查。在实现服务端的API接口时,对于每一个接口的每一个参数,都应该检测其取值是否合法,以免错误的数据输入到系统中。这个工作可以说是费时费力,但又不得不做。而且PHP本身是弱类型语言,不但要验证取值,还要验证数据的类型是否符合,这就更复杂了。本工具就是针对这个工作而设计的,能够有效地减少编码量,代码可读性好。看看下面这段代码,可以对用法有个大概印象,应该不难看懂:$params = $request->query(); // 获取GET参数// 验证(如果验证不通过,会抛出异常)Validation::validate($params, [ “offset” => “IntGe:0”, “count” => “Required|IntGeLe:1,200”,]);支持多种数据类型的校验:整型、浮点型、bool型、字符串、数组、对象、文件、日期时间,能够验证嵌套的数据结构中的参数,还支持带条件判断的验证。目录1 简介1.1 为什么要写这样一个工具?1.2 特点1.3 一个简单示例2 安装3 快速上手3.1 一个完整的示例(不使用任何框架)3.2 验证不通过的错误处理3.3 在第三方框架中的用法4 详细使用方法4.1 验证整型参数4.2 验证浮点型参数4.3 验证bool型参数4.4 验证字符串型参数4.5 验证数组型、对象型、文件型、日期时间型参数4.6 验证器串联(与)4.7 Required 验证器4.8 忽略所有 Required 验证器4.9 嵌套参数的验证4.10 条件判断型验证器4.11 验证规则并联(或)4.12 关于特殊值null, “",0,false的问题4.13 关于基本数据类型与字符串的关系4.14 自定义错误信息输出文本4.15 国际化4.16 国际化(0.4版之前)A 附录 - 验证器列表A.1 整型A.2 浮点型A.3 bool型A.4 字符串型A.5 数组型A.6 对象型A.7 文件型A.8 日期和时间型A.9 条件判断型A.10 其它验证器1 简介1.1 为什么要写这样一个工具?我在使用Laravel框架的时候,Laravel提供了一个参数验证工具,不过用起来不怎么顺畅:每一个验证都写一个验证类(继承XXX),这样太麻烦,而且系统中会多出许多许多的类;如果这些类在多处被复用,或者为了“更加”复用(减少重复代码),再在这些类之间搞出很多的继承关系,那么这些类的维护本身就是一个大问题;验证器有“一词多义”的问题。比如它有一个size验证器,它同时支持验证字符串、整型、文件等多种类型的参数,针对不同数据类型size的含义不一样。这就好比你去背英语单词,有那么一些英语单词,它有很多很多意思,不同的语境下有不同的含义。比如"present"这个单词,它既有“呈现”、“出席”的意思,也有“礼物”的意思。这种一词多义的单词最让人头疼了,搞不清它到底什么意思,而且记不住啊。为了解决这些问题,所以才写了这么一个工具。1.2 特点简洁,验证逻辑一目了然(参考后面的例子)轻量,不需要定义和维护各种验证classes验证器语义明确,没有“一词多义”的问题支持正则表达式验证支持条件验证理论上能够支持验证无限嵌套的参数易学易记。比如整型验证器都是以"Int"开头,浮点型验证器都是以"Float"开头,等等。唯一不符合这一规则的是字符串型验证器,它们一部分以"Str"开头的,但也有一部分不以"Str"开头,比如Regexp, Ip, Email, Url等。不绑定任何一个框架,无任何依赖。你可以在任何一个框架中使用这个工具,就算你不使用框架,也可以使用本工具。每个功能特性都有单元测试(共有 41 tests, 369 assertions)1.3 一个简单示例下面这个示例展示了一个查询获取用户投诉列表的Request参数的验证(用到了条件验证和针对嵌套数据结构的验证)://验证规则$validations = [ “offset” => “IntGe:0”, // 参数offset应该大于等于0 “count” => “Required|IntGeLe:1,200”, // 参数count是必需的且大于等于1小于等于200 “type” => “IntIn:1,2”, // 参数type可取值为: 1, 2 “state” => [ ‘IfIntEq:type,1|IntEq:0’, // 如果type==1(批评建议),那么参数state只能是0 ‘IfIntEq:type,2|IntIn:0,1,2’, // 如果type==2(用户投诉),那么参数state可取值为: 1, 2, 3 ], “search.keyword” => “StrLenGeLe:1,100”, // search.keyword 应该是一个长度在[1, 100]之间的字符串 “search.start_time” => “Date”, // search.start_time 应该是一个包含合法日期的字符串 “search.end_time” => “BoolSmart”, // search.end_time 应该是一个包含合法日期的字符串];// 待验证参数$params = [ “offset” => 0, // 从第0条记录开始 “count” => 10, // 最多返回10条记录 “type” => 2, // 1-批评建议, 2-用户投诉 “state” => 0, // 0-待处理, 1-处理中, 2-已处理 “search” => [ // 搜索条件 “keyword” => ‘硬件故障’, // 关键字 “start_time” => “2018-01-01”, // 起始日期 “end_time” => “2018-01-31”, // 结束日期 ],];// 验证(如果验证不通过,会抛出异常)Validation::validate($params, $validations);2 安装通过Composer安装composer require webgeeker/validation:^0.43 快速上手3.1 一个完整的示例(不使用任何框架)这个例子直接验证$_POST(POST表单)中的参数,展示了最基本的用法<?phpinclude “vendor/autoload.php”;use WebGeeker\Validation\Validation;try { Validation::validate($_POST, [ “offset” => “IntGe:0”, // 参数offset应该大于等于0 “count” => “Required|IntGeLe:1,200”, // 参数count是必需的且大于等于1小于等于200 ]);} catch (\Exception $e) { echo $e->getMessage();}注意:验证不通过会抛出异常,该异常中包含有错误描述信息3.2 验证不通过的错误处理如果验证不通过,Validation::validate(…)方法会抛出异常,建议在框架层面统一捕获这些异常,提取错误描述信息并返回给客户端。3.3 在第三方框架中的用法第三方框架一般会提供Request对象,可以取到GET, POST参数(以Laravel为例)//$params = $request->query(); // 获取GET参数$params = $request->request->all(); // 获取POST参数// 验证(如果验证不通过,会抛出异常)Validation::validate($params, [ // 此处省略验证规则]);4 详细使用方法4.1 验证整型参数整型验证器全部以"Int"开头,用于验证整型数值(如123)或整型字符串(如"123”)。其它数据类型均不匹配。“size” => “IntGeLe:1,100"这条验证要求参数"size"是整数,并且大于等于1,小于等于100。完整的整型验证器的列表参考附录 A.1 。4.2 验证浮点型参数浮点型验证器全部以"Float"开头,用于验证浮点型数值(如1.0)、浮点型字符串(如"1.0”)、整型数值(如123)或整型字符串(如"123")。其它数据类型均不匹配。“height” => “FloatGeLe:0.0,100.0"这条验证要求参数"height"是浮点数,并且大于等于0,小于等于100.0。完整的浮点型验证器的列表参考附录 A.2 。4.3 验证bool型参数bool型验证器只有两个:Bool: 合法的取值为: true, false, “true”, “false”(字符串忽略大小写)。BoolSmart: 合法的取值为: true, false, “true”, “false”, 1, 0, “1”, “0”, “yes”, “no”, “y”, “n”(字符串忽略大小写)例"accept” => “BoolSmart"完整的bool型验证器的列表参考附录 A.3 。4.4 验证字符串型参数字符串型验证器不全以"Str"开头。只接收字符串型数据,其它数据类型均不匹配。例1:“name” => “StrLenGeLe:2,20"这条验证要求参数"name"是字符串,长度在2-20之间(字符串长度是用mb_strlen()来计算的)。例2:“comment” => “ByteLenLe:1048576"这条验证要求参数"comment"是字符串,字节长度不超过1048576(字节长度是用strlen()来计算的)。例3:“email” => “Email"这条验证要求参数"email"是必须是合法的电子邮件地址。例4(正则表达式验证):“phone” => “Regexp:/^1(3[0-9]|4[579]|5[0-35-9]|7[0135678]|8[0-9]|66|9[89])\d{8}$/“这条验证要求参数"phone"是合法的手机号。关于正则表达式中的哪些特殊字符需要转义的问题,只需要用 preg_match() 函数验证好,如:preg_match(’/^string$/’, $string);然后把两个’/‘号及其中间的部分拷贝出来,放在Regexp:后面即可,不需要再做额外的转义,即使正则中有’|‘这种特殊符号,也不需要再转义。完整的字符串型验证器的列表参考附录 A.4 。4.5 验证数组型、对象型、文件型、日期时间型参数参考附录A.5-A.84.6 验证器串联(与)一条规则中可以有多个验证器前后串联,它们之间是“AND”的关系,如:“file” => “FileMaxSize:10m|FileImage"这个验证要求参数"file"是一个图像文件,并且文件大小不超过10m4.7 Required 验证器Required验证器要求参数必须存在,且其值不能为null(这个是PHP的null值,而不是字符串"null”)(参数值为null等价于参数不存在)。如果多个验证器串联,Required验证器必须在其它验证器前面。如果还有条件验证器,Required必须串联在条件验证器后面。如果验证规则中没有 Required,当参数存在时才进行验证,验证不通过会抛异常;如果参数不存在,那么就不验证(相当于验证通过)例:“size” => “Required|StrIn:small,middle,large"该验证要求参数"size"必须是字符串的"small”, “middle"或者"large”。4.8 忽略所有 Required 验证器比如当创建一个用户时,要求姓名、性别、年龄全部都要提供;但是当更新用户信息时,不需要提供全部信息,提供哪个信息就更新哪个信息。$validations = [ “name” => “Required|StrLenGeLe:2,20”, “sex” => “Required|IntIn:0,1”, “age” => “Required|IntGeLe:1,200”,];$userInfo = [ “name” => “tom”, “sex” => “0”, “age” => “10”,];Validation::validate($userInfo, $validations); // 创建用户时的验证unset($userInfo[“age”]); // 删除age字段Validation::validate($userInfo, $validations, true); // 更新用户信息时的验证注意上面代码的最后一行:validate()函数的第三个参数为true表示忽略所有的 Required 验证器。这样我们就只需要写一份验证规则,就可以同时用于创建用户和更新用户信息这两个接口。4.9 嵌套参数的验证下面这个例子展示了包含数组和对象的嵌套的参数的验证:$params = [ “comments” => [ [ “title” => “title 1”, “content” => “content 1”, ], [ “title” => “title 1”, “content” => “content 1”, ], [ “title” => “title 1”, “content” => “content 1”, ], ]];$validations = [ “comments[].title” => “Required|StrLenGeLe:2,50”, “comments[].content” => “Required|StrLenGeLe:2,500”,];Validation::validate($params, $validations);4.10 条件判断型验证器条件判断型验证器都以"If"开头。比如你想招聘一批模特,男的要求180以上,女的要求170以上,验证可以这样写:$validations = [ “sex” => “StrIn:male,female”, “height” => [ “IfStrEq:sex,male|IntGe:180”, “IfStrEq:sex,female|IntGe:170”, ],];参数"sex"的值不同,参数"height"的验证规则也不一样。完整的条件判断型验证器的列表参考附录 A.9 。4.11 验证规则并联(或)多条验证规则可以并联,它们之间是“或”的关系,如"type” => [ “StrIn:small,middle,large”, “IntIn:1,2,3”,]上面这条验证要求参数"type"既可以是字符串"small”, “middle"或"large”,也可以整型的1, 2或3验证规则并联不是简单的“或”的关系,具体验证流程如下:按顺序验证这些规则,如果有一条验证规则通过, 则该参数验证通过。如果全部验证规则都被忽略(If验证器条件不满足,或者没有Required验证器并且该参数不存在,或者有0条验证规则),也算参数验证通过。上面两条都不满足, 则该参数验证失败。这些规则如果要完全理清并不是一件容易的事,所以不建议使用验证规则并联,也尽量不要设计需要这种验证方式的参数。4.12 关于特殊值null, “",0,false的问题这些特殊的值是不等价的,它们是不同的数据类型(需要用不同的验证器去验证):““是字符串。0是整型。false是bool型。null是PHP的空。在本工具中它有特殊的含义。如果某个参数的值为null,则本工具会视为该参数不存在。比如下面两个array对于本工具来说是等价的.$params = [ “name” => “hello”,];与$params = [ “name” => “hello”, “comment” => null,];是等价的。4.13 关于基本数据类型与字符串的关系对于以下url地址http://abc.com/index.php?p1=&&p2=hello&&p3=123我们将得到的参数数组:$params = [ “p1” => “”, “p2” => “hello”, “p3” => “123”,];注意:参数"p1"的值为空字符串”",而不是null。参数"p3"的值为字符串"123”,而不是整型123。GET方式的HTTP请求是传递不了null值的。本工具的所有验证器都是强类型的,“Int"验证的是整型,“Float"验证的是浮点型,“Str*“验证的是字符串型,数据类型不匹配,验证是通不过的。但是字符串类型是个例外。因为常规的HTTP请求,所有的基本数据类型最终都会转换成字符串,所以:整型123和字符串"123"均可以通过验证器"Int"的验证;浮点型123.0和字符串"123.0"均可以通过验证器"Float"的验证;bool型true和字符串"true"均可以通过验证器"Bool"的验证;但是null值和字符串"null"永远不等价,字符串"null"就只是普通的字符串。4.14 自定义错误信息输出文本如果参数验证不通过,Validation::validate()方法会抛出异常,这个异常会包含验证不通过的错误信息描述的文本。但是这个描述文本对用户来说可能不那么友好,我们可以通过两个伪验证器来自定义这些文本:Alias 用于自定义参数名称(这个名称会与内部的错误信息模版相结合,生成最终的错误信息描述文本)>>> 用于自定义错误描述文本(这个文本会完全取代模版生成的错误描述文本)。看下面的例子:$params = [ “title” => “a”,];Validation::validate($params, [ “title” => “Required|StrLenGeLe:2,50”,]); // 抛出异常的错误描述为:“title”长度必须在 2 - 50 之间Validation::validate($params, [ “title” => “Required|StrLenGeLe:2,50|Alias:标题”, // 自定义参数名称]); // 抛出异常的错误描述为:“标题”长度必须在 2 - 50 之间Validation::validate($params, [ “title” => “Required|StrLenGeLe:2,50|>>>:标题长度应在250之间”, // 自定义错误信息描述文本]); // 抛出异常的错误描述为:标题长度应在250之间参考附录A.10获取更详细的信息4.15 国际化从0.4版开始:使用新的静态成员变量 $langCode2ErrorTemplates 来进行“错误提示信息模版”的翻译,主要目的是简化格式(感谢 @gitHusband 的建议)。旧的翻译表 $langCodeToErrorTemplates 仍然有效,已有代码无需修改(参考下一节)。如果新旧翻译表同时提供,优先新的,新表中查不到再使用旧的。要支持国际化,需要自定义一个类,继承\WebGeeker\Validation\Validation,重载两个静态成员变量:$langCode2ErrorTemplates用于提供“错误提示信息模版”的翻译对照表。完整的错误提示信息模版列表可以在\WebGeeker\Validation\Validation::$errorTemplates成员变量中找到$langCodeToTranslations用于提供“自定义参数名称”(由Alias指定)和“自定义错误描述文本”(由>>>指定)的翻译对照表。下面提供一个示例类:class MyValidation extends Validation{ // “错误提示信息模版”翻译对照表 protected static $langCodeToErrorTemplates = [ “zh-tw” => [ ‘Int’ => ‘“{{param}}”必須是整數’, // ???? ‘IntGt’ => ‘“{{param}}”必須大於 {{min}}’, ‘Str’ => ‘“{{param}}”必須是字符串’, ], “en-us” => [ ‘Int’ => ‘{{param}} must be an integer’, ‘IntGt’ => ‘{{param}} must be greater than {{min}}’, ‘Str’ => ‘{{param}} must be a string’, ], ]; // 文本翻译对照表 protected static $langCodeToTranslations = [ “zh-tw” => [ “变量” => “變量”, // ???? “变量必须是整数” => “變量必須是整數”, // ⭐ ], “en-us” => [ “变量” => “variable”, “变量必须是整数” => “variable must be an integer”, ], ];}注意:语言代码是区分大小写的,建议全部用小写,如"zh-cn”, “en-us"等。语言代码的名称是自定义的,你可以随便起名,比如"abc”(建议使用标准的语言代码)。使用这个MyValidation类来进行验证,就可以实现文本的翻译了。MyValidation::setLangCode(“zh-tw”); // 设置语言代码MyValidation::validate([“var” => 1.0], [ “var” => “Int”, // 既没有Alias,也没有>>>,只会翻译错误提示信息模版(对应????那行)]); // 会抛出异常:“var”必須是整數MyValidation::validate([“var” => 1.0], [ “var” => “Int|Alias:变量”, // 有Alias,除了翻译错误提示信息模版外,还会翻译参数名称(对应????那行)]); // 会抛出异常:“變量”必須是整數MyValidation::validate([“var” => 1.0], [ “var” => “Int|>>>:变量必须是整数”, // 有>>>,会翻译自定义错误描述文本(对应⭐那行)]); // 会抛出异常:變量必須是整數如果提供了错误的语言代码,或者没有找到翻译的文本,那么就不翻译,输出原始的文本。4.16 国际化(0.4版之前)(如果你使用的是0.4及之后的版本,建议使用新的国际化方案(参考上一节),更简洁一点)。要支持国际化,需要自定义一个类,继承\WebGeeker\Validation\Validation,重载两个静态成员变量:$langCodeToErrorTemplates用于提供“错误提示信息模版”的翻译对照表。完整的错误提示信息模版列表可以在\WebGeeker\Validation\Validation::$errorTemplates成员变量中找到$langCodeToTranslations用于提供“自定义参数名称”(由Alias指定)和“自定义错误描述文本”(由>>>指定)的翻译对照表。下面提供一个示例类:class MyValidation extends Validation{ // “错误提示信息模版”翻译对照表 protected static $langCodeToErrorTemplates = [ “zh-tw” => [ ““{{param}}”必须是整数” => ““{{param}}”必須是整數”, // ???? ““{{param}}”必须是字符串” => ““{{param}}”必須是字符串”, ], “en-us” => [ ““{{param}}”必须是整数” => “{{param}} must be a integer”, ““{{param}}”必须是字符串” => “{{param}} must be a string”, ], ]; // 文本翻译对照表 protected static $langCodeToTranslations = [ “zh-tw” => [ “变量” => “變量”, // ???? “变量必须是整数” => “變量必須是整數”, // ⭐ ], “en-us” => [ “变量” => “variable”, “变量必须是整数” => “variable must be an integer”, ], ];}注意:语言代码是区分大小写的,建议全部用小写,如"zh-cn”, “en-us"等。语言代码的名称是自定义的,你可以随便起名,比如"abc”(建议使用标准的语言代码)。使用这个MyValidation类来进行验证,就可以实现文本的翻译了。MyValidation::setLangCode(“zh-tw”); // 设置语言代码MyValidation::validate([“var” => 1.0], [ “var” => “Int”, // 既没有Alias,也没有>>>,只会翻译错误提示信息模版(对应????那行)]); // 会抛出异常:“var”必須是整數MyValidation::validate([“var” => 1.0], [ “var” => “Int|Alias:变量”, // 有Alias,除了翻译错误提示信息模版外,还会翻译参数名称(对应????那行)]); // 会抛出异常:“變量”必須是整數MyValidation::validate([“var” => 1.0], [ “var” => “Int|>>>:变量必须是整数”, // 有>>>,会翻译自定义错误描述文本(对应⭐那行)]); // 会抛出异常:變量必須是整數如果提供了错误的语言代码,或者没有找到翻译的文本,那么就不翻译,输出原始的文本。A 附录 - 验证器列表A.1 整型整型验证器全部以"Int"开头。整型验证器示例说明IntInt“{{param}}”必须是整数IntEqIntEq:100“{{param}}”必须等于 {{value}}IntGtIntGt:100“{{param}}”必须大于 {{min}}IntGeIntGe:100“{{param}}”必须大于等于 {{min}}IntLtIntLt:100“{{param}}”必须小于 {{max}}IntLeIntLe:100“{{param}}”必须小于等于 {{max}}IntGtLtIntGtLt:1,100“{{param}}”必须大于 {{min}} 小于 {{max}}IntGeLeIntGeLe:1,100“{{param}}”必须大于等于 {{min}} 小于等于 {{max}}IntGtLeIntGtLe:1,100“{{param}}”必须大于 {{min}} 小于等于 {{max}}IntGeLtIntGeLt:1,100“{{param}}”必须大于等于 {{min}} 小于 {{max}}IntInIntIn:2,3,5,7,11“{{param}}”只能取这些值: {{valueList}}IntNotInIntNotIn:2,3,5,7,11“{{param}}”不能取这些值: {{valueList}}A.2 浮点型内部一律使用double来处理浮点型验证器示例说明FloatFloat“{{param}}”必须是浮点数FloatGtFloatGt:1.0“{{param}}”必须大于 {{min}}FloatGeFloatGe:1.0“{{param}}”必须大于等于 {{min}}FloatLtFloatLt:1.0“{{param}}”必须小于 {{max}}FloatLeFloatLe:1.0“{{param}}”必须小于等于 {{max}}FloatGtLtFloatGtLt:0,1.0“{{param}}”必须大于 {{min}} 小于 {{max}}FloatGeLeFloatGeLe:0,1.0“{{param}}”必须大于等于 {{min}} 小于等于 {{max}}FloatGtLeFloatGtLe:0,1.0“{{param}}”必须大于 {{min}} 小于等于 {{max}}FloatGeLtFloatGeLt:0,1.0“{{param}}”必须大于等于 {{min}} 小于 {{max}}A.3 bool型bool型验证器示例说明BoolBool合法的取值为: true, false, “true”, “false”(忽略大小写)BoolSmartBoolSmart合法的取值为: true, false, “true”, “false”, 1, 0, “1”, “0”, “yes”, “no”, “y”, “n”(忽略大小写)A.4 字符串型字符串型验证器示例说明StrStr“{{param}}”必须是字符串StrEqStrEq:abc“{{param}}”必须等于”{{value}}“StrEqIStrEqI:abc“{{param}}”必须等于”{{value}}"(忽略大小写)StrNeStrNe:abc“{{param}}”不能等于”{{value}}“StrNeIStrNeI:abc“{{param}}”不能等于”{{value}}"(忽略大小写)StrInStrIn:abc,def,g“{{param}}”只能取这些值: {{valueList}}StrInIStrInI:abc,def,g“{{param}}”只能取这些值: {{valueList}}(忽略大小写)StrNotInStrNotIn:abc,def,g“{{param}}”不能取这些值: {{valueList}}StrNotInIStrNotInI:abc,def,g“{{param}}”不能取这些值: {{valueList}}(忽略大小写)StrLenStrLen:8“{{param}}”长度必须等于 {{length}}StrLenGeStrLenGe:8“{{param}}”长度必须大于等于 {{min}}StrLenLeStrLenLe:8“{{param}}”长度必须小于等于 {{max}}StrLenGeLeStrLenGeLe:6,8“{{param}}”长度必须在 {{min}} - {{max}} 之间ByteLenByteLen:8“{{param}}”长度(字节)必须等于 {{length}}ByteLenGeByteLenGe:8“{{param}}”长度(字节)必须大于等于 {{min}}ByteLenLeByteLenLe:8“{{param}}”长度(字节)必须小于等于 {{max}}ByteLenGeLeByteLenGeLe:6,8“{{param}}”长度(字节)必须在 {{min}} - {{max}} 之间LettersLetters“{{param}}”只能包含字母AlphabetAlphabet同LettersNumbersNumbers“{{param}}”只能是纯数字DigitsDigits同NumbersLettersNumbersLettersNumbers“{{param}}”只能包含字母和数字NumericNumeric“{{param}}”必须是数值。一般用于大数处理(超过double表示范围的数,一般会用字符串来表示)(尚未实现大数处理), 如果是正常范围内的数, 可以使用’Int’或’Float’来检测VarNameVarName“{{param}}”只能包含字母、数字和下划线,并且以字母或下划线开头EmailEmail“{{param}}”必须是合法的emailUrlUrl“{{param}}”必须是合法的Url地址IpIp“{{param}}”必须是合法的IP地址MacMac“{{param}}”必须是合法的MAC地址RegexpRegexp:/^abc$/Perl正则表达式匹配A.5 数组型数组型验证器示例说明ArrArr“{{param}}”必须是数组ArrLenArrLen:5“{{param}}”数组长度必须等于 {{length}}ArrLenGeArrLenGe:1“{{param}}”数组长度必须大于等于 {{min}}ArrLenLeArrLenLe:9“{{param}}”数组长度必须小于等于 {{max}}ArrLenGeLeArrLenGeLe:1,9“{{param}}”长数组度必须在 {{min}} ~ {{max}} 之间A.6 对象型对象型验证器示例说明ObjObj“{{param}}”必须是对象A.7 文件型文件型验证器示例说明FileFile“{{param}}”必须是文件FileMaxSizeFileMaxSize:10mb“{{param}}”必须是文件, 且文件大小不超过{{size}}FileMinSizeFileMinSize:100kb“{{param}}”必须是文件, 且文件大小不小于{{size}}FileImageFileImage“{{param}}”必须是图片FileVideoFileVideo“{{param}}”必须是视频文件FileAudioFileAudio“{{param}}”必须是音频文件FileMimesFileMimes:mpeg,jpeg,png“{{param}}”必须是这些MIME类型的文件:{{mimes}}A.8 日期和时间型日期和时间型验证器示例说明DateDate“{{param}}”必须符合日期格式YYYY-MM-DDDateFromDateFrom:2017-04-13“{{param}}”不得早于 {{from}}DateToDateTo:2017-04-13“{{param}}”不得晚于 {{to}}DateFromToDateFromTo:2017-04-13,2017-04-13“{{param}}”必须在 {{from}} ~ {{to}} 之间DateTimeDateTime“{{param}}”必须符合日期时间格式YYYY-MM-DD HH:mm:ssDateTimeFromDateTimeFrom:2017-04-13 12:00:00“{{param}}”不得早于 {{from}}DateTimeToDateTimeTo:2017-04-13 12:00:00“{{param}}”必须早于 {{to}}DateTimeFromToDateTimeFromTo:2017-04-13 12:00:00,2017-04-13 12:00:00“{{param}}”必须在 {{from}} ~ {{to}} 之间A.9 条件判断型在一条验证规则中,条件验证器必须在其它验证器前面,多个条件验证器可以串联。注意,条件判断中的“条件”一般是检测另外一个参数的值,而当前参数的值是由串联在条件判断验证器后面的其它验证器来验证。条件判断型验证器示例说明IfIf:selected如果参数"selected"值等于 1, true, ‘1’, ’true’, ‘yes’或 ‘y’(字符串忽略大小写)IfNotIfNot:selected如果参数"selected"值等于 0, false, ‘0’, ‘false’, ’no’或’n’(字符串忽略大小写)IfTrueIfTrue:selected如果参数"selected"值等于 true 或 ’true’(忽略大小写)IfFalseIfFalse:selected如果参数"selected"值等于 false 或 ‘false’(忽略大小写)IfExistIfExist:var如果参数"var"存在IfNotExistIfNotExist:var如果参数"var"不存在IfIntEqIfIntEq:var,1if (var === 1)IfIntNeIfIntNe:var,2if (var !== 2). 特别要注意的是如果条件参数var的数据类型不匹配, 那么If条件是成立的; 而其它几个IfIntXx当条件参数var的数据类型不匹配时, If条件不成立IfIntGtIfIntGt:var,0if (var > 0)IfIntLtIfIntLt:var,1if (var < 0)IfIntGeIfIntGe:var,6if (var >= 6)IfIntLeIfIntLe:var,8if (var <= 8)IfIntInIfIntIn:var,2,3,5,7if (in_array(var, [2,3,5,7]))IfIntNotInIfIntNotIn:var,2,3,5,7if (!in_array(var, [2,3,5,7]))IfStrEqIfStrEq:var,waitingif (var === ‘waiting’)IfStrNeIfStrNe:var,editingif (var !== ’editing’). 特别要注意的是如果条件参数var的数据类型不匹配, 那么If条件是成立的; 而其它几个IfStrXx当条件参数var的数据类型不匹配时, If条件不成立IfStrGtIfStrGt:var,aif (var > ‘a’)IfStrLtIfStrLt:var,zif (var < ‘z’)IfStrGeIfStrGe:var,Aif (var >= ‘0’)IfStrLeIfStrLe:var,Zif (var <= ‘9’)IfStrInIfStrIn:var,normal,warning,errorif (in_array(var, [’normal’, ‘warning’, ’error’], true))IfStrNotInIfStrNotIn:var,warning,errorif (!in_array(var, [‘warning’, ’error’], true))A.10 其它验证器其它验证器示例说明RequiredRequired待验证的参数是必需的。如果验证器串联,除了条件型验证器外,必须为第一个验证器AliasAlias:参数名称自定义错误提示文本中的参数名称(必须是最后一个验证器)>>>>>>:这是自定义错误提示文本自定义错误提示文本(与Alias验证器二选一,必须是最后一个验证器)自定义PHP函数function() {}暂不提供该机制,因为如果遇到本工具不支持的复杂参数验证,你可以直接写PHP代码来验证,不需要再经由本工具来验证(否则就是脱裤子放屁,多此一举) ...

March 15, 2019 · 5 min · jiezi

PHP 多维数组中的 array_find

过渡最近在开始使用 ThinkPHP 5.1 进行一系列开发工作,因为之前是使用 Laravel 进行开发,像是标题中的这种小问题都在 Laravel 中很容易实现。直接使用 array_first 方法进行查找即可。快速实现但是在 ThinkPHP 中 并没有提供类似方法进行快速处理,所以有需要来重复造轮子了?至此想到的第一个方法就是使用 array_search 不过这个方法中官方提供的方案仅用于简单的一维数组搜索,而且返回的也只是 index 并不是找到的结果,淡然通过 index 我们也可以取出项目来,在 PHP 5.5 带来的新方法 array_column,可以方便的实现二维搜索 在这里的用户笔记 为我们提供了一个小的示例。$userdb=Array( (0) => Array ( (uid) => ‘100’, (name) => ‘Sandra Shush’, (url) => ‘urlof100’ ), (1) => Array ( (uid) => ‘5465’, (name) => ‘Stefanie Mcmohn’, (pic_square) => ‘urlof100’ ), (2) => Array ( (uid) => ‘40489’, (name) => ‘Michael’, (pic_square) => ‘urlof40489’ ));$key = array_search(40489, array_column($userdb, ‘uid’));并且赢得了 800+ 的赞赏,到这里可能你会觉得 通过这个方式取到 index 然后用 index 取出来就行了。一些????但是,如果你再往下翻一下,你会看到另一条用户笔记 ,这条用户笔记告诉我们 当我们使用这种方式来实现二维搜索时你 PHP 版本 必须要在 5.5 + ,作者同时告诉我们Since array_column() will produce a resulting array; it won’t preserve your multi-dimentional array’s keys. So if you check against your keys, it will fail.机翻一下 :由于array_column()将产生一个新的数组; 它不会保留多维数组的原来的键。 因此,如果您检查您的键,它将失败。然后作者也为我们提供了一个????$people = array( 2 => array( ’name’ => ‘John’, ‘fav_color’ => ‘green’ ), 5 => array( ’name’ => ‘Samuel’, ‘fav_color’ => ‘blue’ ));$found_key = array_search(‘blue’, array_column($people, ‘fav_color’)); // 1// Here, you could expect that the $found_key would be “5” but it’s NOT. It will be 1. Since it’s the second element of the produced array by the array_column() function.// 机翻一下:在这里,你预期 $found_key 的将是“5”,但它不是,它将是1.因为它是array_column()函数生成的数组的第二个元素。// 另外 作者还提到了// Secondly, if your array is big, I would recommend you to first assign a new variable so that it wouldn’t call array_column() for each element it searches. For a better performance, you could do;// 机翻一下:其次,如果您的数组很大,我建议您先分配一个新变量,这样它就不会为它搜索的每个元素调用array_column()。 为了获得更好的性能,你可以做到;$colors = array_column($people, ‘fav_color’);$found_key = array_search(‘blue’, $colors);看完了这些提示,你已经发现了这其中的坑,然而,这并没有结束,因为如果数据不够纯净的话,你用 array_search 实现的功能可能就只局限于 in_array 。而且,尽管到了这里,还会遇到另外一个坑,先看????<?php $userdb = array( 0 => array( ‘uid’ => 100, ’name’ => ‘Sandra Shush’, ‘url’ => ‘urlof100’ ), ‘8’ => array( ‘uid’ => 5465, ’name’ => ‘Stefanie Mcmohn’, ‘pic_square’ => ‘urlof100’ ), ‘3’ => Array( // ‘uid’ => 5555, ’name’ => ‘Michael’, ‘pic_square’ => ‘urlof40489’ ), ‘6’ => Array( ‘uid’ => 40489, ’name’ => ‘Michael’, ‘pic_square’ => ‘urlof40489’ )); $found_key = array_search(40489, array_column($userdb, ‘uid’));在这里 猜想一下 $found_key 会是什么?答案是: 2。??? ,因为当在执行 array_column() 时,第三个元素,也就是键为3的数据中 uid 被注释了,这时候 PHP 就会忽略它,并不会被保留索引位置,所以这时候结果只有 3 个元素,第四个向上替补了,因为数组索引是 0 开始,所以 2 就相当于第 3 个元素了。大家都是怎么做的?在 array_search 的用户笔记区中,看到了很多都是数组循环并比对后返回,比如这个,但是这样又遇到一些局限性问题,这时候我们又想到了 Laravel 中把自主权交给调用者,由调用者在匿名方法内进行处理并返回 bool 值 进行处理。至此,看回 Laravel 的 array_fisrt 方法,通过 Laravel 源码可以看到 array_first 是 Arr::fisrt 方法的一个包装,在这里我们可以看到 Laravel 的实现方式,在这一小段代码中 还看到了另一个方法 value(),可以看到,这个方法就是判断是否是一个匿名方法,如果是就执行方法并返回,不是就直接返回。public static function first($array, callable $callback = null, $default = null) { if (is_null($callback)) { if (empty($array)) { return value($default); } foreach ($array as $item) { return $item; } } foreach ($array as $key => $value) { if (call_user_func($callback, $value, $key)) { return $value; } } return value($default); }if (! function_exists(‘value’)) { /** * Return the default value of the given value. * * @param mixed $value * @return mixed */ function value($value) { return $value instanceof Closure ? $value() : $value; }}看到这里,几乎是比较完整的实现,在这里还想到了另外一个和助手函数有关的项目:Underscore.php这是一个 PHP 的方法库,也可以算是是 JS中的 underscore.js和 loadsh、ramda 的 PHP 实现。在这个项目中我们看到了实现方式public static function find($array, Closure $closure) { foreach ($array as $key => $value) { if ($closure($value, $key)) { return $value; } } return; }这里的实现更为简单,最终也实现了我们想要的结果。 ...

March 13, 2019 · 3 min · jiezi

使用Envoy实现一键部署项目

Envoy是一个composer扩展包,它的本质作用是代替你登录远程的目标服务器(下称目标机)并执行一系列命令,它的执行环境要有事先装有php与composer,但它不仅仅能在php项目里起作用,原因是前面提到的它的本质是帮你执行命令,而这命令不只针对php的命令。因此你不仅可以把它当作部署项目的工具,甚至可以是对目标机的简单管理工具。下面从本地机对目标机的登录到envoy的安装使用来分步介绍它。实现本地机与目标机的ssh密钥登录假定目标服务器是sorgo@192.168.8.8#如果本地机的用户还没rsa密钥的那先生成ssh-keygen -t rsa -C “your_email@example.com”#发送密钥到目标机,并进行密码验证ssh-copy-id sorgo@192.168.8.8#测试是否能直接ssh登录而不再要求输入密码ssh sorgo@192.168.8.8安装和使用#全局安装composer global require laravel/envoy#一键生成envoy执行文件模板:Envoy.blade.phpenvoy init sorgo@192.168.8.8修改Envoy.blade.php文件{{– 这是blade文件里的注释 –}}{{– web是标识这台服务器的名字 –}}@servers([‘web’ => ‘jeffio@116.85.48.221’]){{– deploy是给这个任务起的名字 –}}@task(‘deploy’) cd /www/wwwroot/sifou.com git pull origin master composer install@endtask执行任务,命令格式是envoy run 任务名envoy run deploy以上即可一键完成:进入指定目录git拉取更新安装composer包这样一个简单的部署就完成了,极大降低了维护的操作成本。参考更多写操作请参考收下文档Envoy详细文档

March 11, 2019 · 1 min · jiezi

分享一些简单的 Laravel 编码实践

将任何 PHP 框架称为最好的框架都是错误的,因为不同的框架都有各自的优点。 通常来说,一个PHP开发者会根据项目需求来选择合适的框架。 但相信我, 我现在已经完全爱上了 Laravel。关于 Laravel,它使用起来简单且舒适,适用于编写产品代码,并能极大的推动开发过程。 Laravel 中我最喜欢的一点是它是使用当下编程中的最佳实践所构建的。我个人更喜欢保持 Laravel 推荐的基本代码结构。当然你也可以选择其他可用的方法,但这可能会在之后的使用中出现一些问题。这里有一些在 Laravel 开发中值得记住的简单建议:最大限度的使用你的 .env 文件;不要破坏框架核心,不要编辑 vendor 文件夹中的文件,你可以选择继承相关函数来实现。扩展优于修改。不要直接通过 PHPMyAdmin 或者其他数据库控制台创建表和索引。 请使用数据库迁移表来创建表、增加修改字段,然后提交到 Git 仓库。测试的时候不要直接向数据库插入假值。 创建填充文件(Seeder 文件)来填充数据库。更倾向于使用 Artisan 脚手架而不是手动创建东西,这会极大的提升你的生产力。确保使用一些 artisan 命令来提升性能: php artisan route:cache // 路由缓存 php artisan config:cache // 配置信息缓存 php artisan optimize — force // 类映射加载优化尽量不要将闭包写在 routes.php 文件中,而是将它们移到你的控制器中。创建自定义的类和函数时要特别注意命名规范,尤其是对于模型。 Laravel 的工作原理是这样的,对于一个命名为 users 的表, Laravel 希望该表的模型被命名为 User 。尽量为每一个请求创建 Validation Requests 。尽管 PHP 有一个能够帮助你读取、写入、比较或者计算日期的 DateTime 类,但还是建议你使用 Carbon 扩展来处理日期。始终保持使用最新的版本, Laravel 更新得很快,所以跟上节奏。为了更好的性能,始终使用 gulp、 Elixir 来将你的脚本和 sass 文件编译为压缩版, Laravel 已经为你做好了底层的工作。欢迎在评论里推荐更多内容…文章转自:https://learnku.com/laravel/t… 更多文章:https://learnku.com/laravel/c… ...

March 7, 2019 · 1 min · jiezi

实现一个简单的di容器

之前看了好多框架,laravel,thinkphp,yii等等。基本上都使用了容器。对于我而言,虽然看懂了laravel是怎么写的,但是如果自己不去尝试一下,始终觉得不会这个东西。下面的代码是我实现的一个简单的容器,很多地方处理并不是很好,但是应该已经足够了。<?phpclass Container{ //$binds 这个变量保存,是名字=>实例的映射 private $binds = []; public static $instance = null; /** * 单例 / public static function getInstance(){ if(static::$instance == null){ static::$instance = new static(); return static::$instance ; } return static::$instance; } /* * 一开始是受到laravel的影响,所以写了一个bind函数, * 看完laravel的容器实现,印象之中,$concrete和$abstract来回变换。 * 下面的代码有点像thinkphp的里面的实现,好理解一点 / public static function bind($name,$class = null){ if($class instanceof Closure){ static::getInstance()->binds[$name] = $class; }else if(is_object($class)){ static::getInstance()->binds[$name] = $class; }else{ //在这里开始make一个数组,laravel好像是make和build分开的。 static::getInstance()->make($name); } } /* * 核心是make方法了 / public static function get($name){ return static::getInstance()->make($name); } /* * 核心make方法 */ public function make($name){ try{ //根据类名去查找$this->binds实例是否已经存在,如果存在就直接返回 if(array_key_exists($name,$this->binds)){ return $this->binds[$name]; } //根据类名得到它的反射类 $reflectClass = new ReflectionClass($name); //利用反射类 $constructor = $reflectClass->getConstructor(); //如果没有构造器的话,就直接去实例化它 $params = []; if(!is_null($constructor)){ //获取构造器中的方法 $constructorParams = $constructor->getParameters(); // var_dump($constructorParams); //保存构造器的参数 foreach($constructorParams as $constructorParam){ //这个地方主要是判断参数是否是类,如果是就递归的构造它,不是就简单的添加到$this->params中 if(!is_null($constructorParam->getType())){ $params[] =$this->make($constructorParam->name,$constructorParam->name); }else{ $params[] = $constructorParam->name; } } } //在这个地方构造实例 $class = $reflectClass->newInstanceArgs($params); //绑定 $this->binds[$name] = $class; return $class; }catch(ReflectionException $e){ echo $e->getMessage(); } } private function __construct(){} private function __clone(){}}?>下面是我的测试文件了,<?phprequire “./Container.php”;class TestFather{ private $name = “TestFather”; public function __construct(){ }}class Test extends TestFather{ private $name = “Test”; // public function __construct(DI $di, DI2 $di2,$name){ // } public function __construct(DI $di,$name,$param_2 =[]){ } public function sayName(){ echo $this->name; } public function sayDI2Name(DI2 $di2){ //如果这么写的,di2方法会先于前面的字符串打印出来 // echo “form Test say di2 name: “.$di2->sayName(); echo “form Test say di2 name: “; echo $di2->sayName(); }}class DI{ private $name = “DI”; public function __construct(DI2 $di2){} public function sayName(){ echo $this->name; } }class DI2{ private $name = “DI2”; public function __construct(){} public function sayName(){ echo $this->name; } }class DI3{ private $name = “DI3”; public function __construct(){} public function sayName(){ echo $this->name; } }class DI4{ private $name = “DI4”; public function __construct(){} public function sayName(){ echo $this->name; } }//要不要无所谓了// Container::bind(’test’,‘Test’); $test = Container::get(’test’);$test->sayName();echo “\n”;$test->sayDI2Name(new DI2());echo “\n”;$di = Container::get(‘di’);$di->sayName();echo “\n”;$di2 = Container::get(‘di2’);$di2->sayName();echo “\n”;$di3 = new DI3();Container::get(‘di3’,$di3)->sayName();echo “\n”;$di4 = function(){ return new DI4();};Container::get(‘di4’,$di4)->sayName();echo “\n”;?>最后的结果如下 ...

March 6, 2019 · 2 min · jiezi

使用 TDD 测试驱动开发来构建 Laravel REST API

TDD 以及敏捷开发的先驱者之一的 James Grenning有句名言:如果你没有进行测试驱动开发,那么你应该正在做开发后堵漏的事 - James Grenning今天我们将进行一场基于 Laravel 的测试驱动开发之旅。 我们将创建一个完整的 Laravel REST API,其中包含身份验证和 CRUD 功能,而无需打开 Postman 或浏览器。?注意:本旅程假定你已经理解了 Laravel 和 PHPUnit 的基本概念。你是否已经明晰了这个问题?那就开始吧。项目设置首先创建一个新的 Laravel 项目 composer create-project –prefer-dist laravel/laravel tdd-journey。然后,我们需要创建 用户认证 脚手架,执行 php artisan make:auth ,设置好 .env 文件中的数据库配置后,执行数据库迁移 php artisan migrate。本测试项目并不会使用到刚生成的用户认证相关的路由和视图。我们将使用 jwt-auth。所以需要继续 安装 jwt 到项目。注意:如果您在执行 jwt:generate 指令时遇到错误, 您可以参考 这里解决这个问题,直到 jwt 被正确安装到项目中。最后,您需要在 tests/Unit 和 tests/Feature 目录中删除 ExampleTest.php 文件,使我们的测试结果不被影响。编码首先将 JWT 驱动配置为 auth 配置项的默认值:<?php // config/auth.php file’defaults’ => [ ‘guard’ => ‘api’, ‘passwords’ => ‘users’,],‘guards’ => [ … ‘api’ => [ ‘driver’ => ‘jwt’, ‘provider’ => ‘users’, ],],然后将如下内容放到你的 routes/api.php 文件里:<?phpRoute::group([‘middleware’ => ‘api’, ‘prefix’ => ‘auth’], function () { Route::post(‘authenticate’, ‘AuthController@authenticate’)->name(‘api.authenticate’); Route::post(‘register’, ‘AuthController@register’)->name(‘api.register’);});现在我们已经将驱动设置完成了,如法炮制,去设置你的用户模型:<?php…class User extends Authenticatable implements JWTSubject{ … //获取将被存储在 JWT 主体 claim 中的标识 public function getJWTIdentifier() { return $this->getKey(); } // 返回一个键值对数组,包含要添加到 JWT 的任何自定义 claim public function getJWTCustomClaims() { return []; }}我们所需要做的就是实现 JWTSubject 接口然后添加相应的方法即可。接下来,我们需要增加权限认证方法到控制器中.运行 php artisan make:controller AuthController 并且添加以下方法:<?php…class AuthController extends Controller{ public function authenticate(Request $request){ // 验证字段 $this->validate($request,[’email’ => ‘required|email’,‘password’=> ‘required’]); // 测试验证 $credentials = $request->only([’email’,‘password’]); if (! $token = auth()->attempt($credentials)) { return response()->json([’error’ => ‘Incorrect credentials’], 401); } return response()->json(compact(’token’)); } public function register(Request $request){ // 表达验证 $this->validate($request,[ ’email’ => ‘required|email|max:255|unique:users’, ’name’ => ‘required|max:255’, ‘password’ => ‘required|min:8|confirmed’, ]); // 创建用户并生成 Token $user = User::create([ ’name’ => $request->input(’name’), ’email’ => $request->input(’email’), ‘password’ => Hash::make($request->input(‘password’)), ]); $token = JWTAuth::fromUser($user); return response()->json(compact(’token’)); }}这一步非常直接,我们要做的就是添加 authenticate 和 register 方法到我们的控制器中。在 authenticate 方法,我们验证了输入,尝试去登录,如果成功就返回令牌。在 register 方法,我们验证输入,然后基于此创建一个用户并且生成令牌。4. 接下来,我们进入相对简单的部分。 测试我们刚写入的内容。 使用 php artisan make:test AuthTest 生成测试类。 在新的 tests / Feature / AuthTest 中添加以下方法:<?php /** * @test * Test registration /public function testRegister(){ //创建测试用户数据 $data = [ ’email’ => ’test@gmail.com’, ’name’ => ‘Test’, ‘password’ => ‘secret1234’, ‘password_confirmation’ => ‘secret1234’, ]; //发送 post 请求 $response = $this->json(‘POST’,route(‘api.register’),$data); //判断是否发送成功 $response->assertStatus(200); //接收我们得到的 token $this->assertArrayHasKey(’token’,$response->json()); //删除数据 User::where(’email’,’test@gmail.com’)->delete();}/* * @test * Test login /public function testLogin(){ //创建用户 User::create([ ’name’ => ’test’, ’email’=>’test@gmail.com’, ‘password’ => bcrypt(‘secret1234’) ]); //模拟登陆 $response = $this->json(‘POST’,route(‘api.authenticate’),[ ’email’ => ’test@gmail.com’, ‘password’ => ‘secret1234’, ]); //判断是否登录成功并且收到了 token $response->assertStatus(200); $this->assertArrayHasKey(’token’,$response->json()); //删除用户 User::where(’email’,’test@gmail.com’)->delete();}上面代码中的几行注释概括了代码的大概作用。 您应该注意的一件事是我们如何在每个测试中创建和删除用户。 测试的全部要点是它们应该彼此独立并且应该在你的理想情况下存在数据库中的状态。如果你想全局安装它,可以运行 $ vendor / bin / phpunit 或 $ phpunit 命令。 运行后它应该会给你返回是否安装成功的数据。 如果不是这种情况,您可以浏览日志,修复并重新测试。 这就是 TDD 的美丽之处。5. 对于本教程,我们将使用『菜谱 Recipes』作为我们的 CRUD 数据模型。首先创建我们的迁移数据表 php artisan make:migration create_recipes_table 并添加以下内容:<?php …public function up(){ Schema::create(‘recipes’, function (Blueprint $table) { $table->increments(‘id’); $table->string(’title’); $table->text(‘procedure’)->nullable(); $table->tinyInteger(‘publisher_id’)->nullable(); $table->timestamps(); });}public function down(){ Schema::dropIfExists(‘recipes’);}然后运行数据迁移。 现在使用命令 php artisan make:model Recipe 来生成模型并将其添加到我们的模型中。<?php …protected $fillable = [’title’,‘procedure’];/* * 发布者 * @return \Illuminate\Database\Eloquent\Relations\BelongsTo /public function publisher(){ return $this->belongsTo(User::class);}然后将此方法添加到 user 模型。<?php… /* * 获取所有菜谱 * @return \Illuminate\Database\Eloquent\Relations\HasMany */public function recipes(){ return $this->hasMany(Recipe::class);}6. 现在我们需要最后一部分设置来完成我们的食谱管理。 首先,我们将创建控制器 php artisan make:controller RecipeController 。 接下来,编辑 routes / api.php 文件并添加 create 路由端点。<?php … Route::group([‘middleware’ => [‘api’,‘auth’],‘prefix’ => ‘recipe’],function (){ Route::post(‘create’,‘RecipeController@create’)->name(‘recipe.create’);});在控制器中,还要添加 create 方法<?php … public function create(Request $request){ //验证数据 $this->validate($request,[’title’ => ‘required’,‘procedure’ => ‘required|min:8’]); //创建配方并附加到用户 $user = Auth::user(); $recipe = Recipe::create($request->only([’title’,‘procedure’])); $user->recipes()->save($recipe); //返回 json 格式的食谱数据 return $recipe->toJson();}使用 php artisan make:test RecipeTest 生成特征测试并编辑内容,如下所示:<?php …class RecipeTest extends TestCase{ use RefreshDatabase; … //创建用户并验证用户身份 protected function authenticate(){ $user = User::create([ ’name’ => ’test’, ’email’ => ’test@gmail.com’, ‘password’ => Hash::make(‘secret1234’), ]); $token = JWTAuth::fromUser($user); return $token; } public function testCreate() { //获取 token $token = $this->authenticate(); $response = $this->withHeaders([ ‘Authorization’ => ‘Bearer ‘. $token, ])->json(‘POST’,route(‘recipe.create’),[ ’title’ => ‘Jollof Rice’, ‘procedure’ => ‘Parboil rice, get pepper and mix, and some spice and serve!’ ]); $response->assertStatus(200); }}上面的代码你可能还是不太理解。我们所做的就是创建一个用于处理用户注册和 token 生成的方法,然后在 testCreate() 方法中使用该 token 。注意使用 RefreshDatabase trait ,这个 trait 是 Laravel 在每次测试后重置数据库的便捷方式,非常适合我们漂亮的小项目。好的,所以现在,我们只要判断当前请求是否是响应状态,然后继续运行 $ vendor/bin/phpunit 。如果一切运行顺利,您应该收到错误。 ?There was 1 failure:1) TestsFeatureRecipeTest::testCreateExpected status code 200 but received 500.Failed asserting that false is true./home/user/sites/tdd-journey/vendor/laravel/framework/src/Illuminate/Foundation/Testing/TestResponse.php:133/home/user/sites/tdd-journey/tests/Feature/RecipeTest.php:49FAILURES!Tests: 3, Assertions: 5, Failures: 1.查看日志文件,我们可以看到罪魁祸首是 Recipe 和 User 类中的 publisher 和 recipes 的关系。 Laravel 尝试在表中找到一个字段为 user_id 的列并将其用作于外键,但在我们的迁移中,我们将publisher_id 设置为外键。 现在,将行调整为://食谱文件public function publisher(){ return $this->belongsTo(User::class,‘publisher_id’);}//用户文件public function recipes(){ return $this->hasMany(Recipe::class,‘publisher_id’);}然后重新运行测试。 如果一切顺利,我们将获得所有绿色测试!?… 3 / 3 (100%)…OK (3 tests, 5 assertions)现在我们仍然需要测试创建配方的方法。为此,我们可以判断用户的『菜谱 Recipes』计数。更新你的 testCreate 方法,如下所示:<?php…//获取 token$token = $this->authenticate();$response = $this->withHeaders([ ‘Authorization’ => ‘Bearer ‘. $token,])->json(‘POST’,route(‘recipe.create’),[ ’title’ => ‘Jollof Rice’, ‘procedure’ => ‘Parboil rice, get pepper and mix, and some spice and serve!’]);$response->assertStatus(200);//得到计数做出判断$count = User::where(’email’,’test@gmail.com’)->first()->recipes()->count();$this->assertEquals(1,$count);我们现在可以继续编写其余的方法。首先,编写我们的 routes/api.php<?php…Route::group([‘middleware’ => [‘api’,‘auth’],‘prefix’ => ‘recipe’],function (){ Route::post(‘create’,‘RecipeController@create’)->name(‘recipe.create’); Route::get(‘all’,‘RecipeController@all’)->name(‘recipe.all’); Route::post(‘update/{recipe}’,‘RecipeController@update’)->name(‘recipe.update’); Route::get(‘show/{recipe}’,‘RecipeController@show’)->name(‘recipe.show’); Route::post(‘delete/{recipe}’,‘RecipeController@delete’)->name(‘recipe.delete’);});接下来,我们将方法添加到控制器。 以下面这种方式更新 RecipeController 类。<?php ….//创建配方public function create(Request $request){ //验证 $this->validate($request,[’title’ => ‘required’,‘procedure’ => ‘required|min:8’]); //创建配方并附加到用户 $user = Auth::user(); $recipe = Recipe::create($request->only([’title’,‘procedure’])); $user->recipes()->save($recipe); //返回配方的 json 格式数据 return $recipe->toJson();}//获取所有的配方public function all(){ return Auth::user()->recipes;}//更新配方public function update(Request $request, Recipe $recipe){ //检查用户是否是配方的所有者 if($recipe->publisher_id != Auth::id()){ abort(404); return; } //更新并返回 $recipe->update($request->only(’title’,‘procedure’)); return $recipe->toJson();}//显示单个食谱的详细信息public function show(Recipe $recipe){ if($recipe->publisher_id != Auth::id()){ abort(404); return; } return $recipe->toJson();}//删除一个配方public function delete(Recipe $recipe){ if($recipe->publisher_id != Auth::id()){ abort(404); return; } $recipe->delete();}代码和注释已经很好地解释了这个逻辑。最后我们的 test/Feature/RecipeTest:<?php… use RefreshDatabase;protected $user;// 创建用户并验证他protected function authenticate(){ $user = User::create([ ’name’ => ’test’, ’email’ => ’test@gmail.com’, ‘password’ => Hash::make(‘secret1234’), ]); $this->user = $user; $token = JWTAuth::fromUser($user); return $token;}// 测试创建路由public function testCreate(){ // 获取令牌 $token = $this->authenticate(); $response = $this->withHeaders([ ‘Authorization’ => ‘Bearer ‘. $token, ])->json(‘POST’,route(‘recipe.create’),[ ’title’ => ‘Jollof Rice’, ‘procedure’ => ‘Parboil rice, get pepper and mix, and some spice and serve!’ ]); $response->assertStatus(200); // 获取计数并断言 $count = $this->user->recipes()->count(); $this->assertEquals(1,$count);}// 测试显示所有路由public function testAll(){ // 验证并将配方附加到用户 $token = $this->authenticate(); $recipe = Recipe::create([ ’title’ => ‘Jollof Rice’, ‘procedure’ => ‘Parboil rice, get pepper and mix, and some spice and serve!’ ]); $this->user->recipes()->save($recipe); // 调用路由并断言响应 $response = $this->withHeaders([ ‘Authorization’ => ‘Bearer ‘. $token, ])->json(‘GET’,route(‘recipe.all’)); $response->assertStatus(200); // 断言计数为1,第一项的标题相关 $this->assertEquals(1,count($response->json())); $this->assertEquals(‘Jollof Rice’,$response->json()[0][’title’]);}// 测试更新路由public function testUpdate(){ $token = $this->authenticate(); $recipe = Recipe::create([ ’title’ => ‘Jollof Rice’, ‘procedure’ => ‘Parboil rice, get pepper and mix, and some spice and serve!’ ]); $this->user->recipes()->save($recipe); // 调用路由并断言响应 $response = $this->withHeaders([ ‘Authorization’ => ‘Bearer ‘. $token, ])->json(‘POST’,route(‘recipe.update’,[‘recipe’ => $recipe->id]),[ ’title’ => ‘Rice’, ]); $response->assertStatus(200); // 断言标题是新标题 $this->assertEquals(‘Rice’,$this->user->recipes()->first()->title);}// 测试单一的展示路由public function testShow(){ $token = $this->authenticate(); $recipe = Recipe::create([ ’title’ => ‘Jollof Rice’, ‘procedure’ => ‘Parboil rice, get pepper and mix, and some spice and serve!’ ]); $this->user->recipes()->save($recipe); $response = $this->withHeaders([ ‘Authorization’ => ‘Bearer ‘. $token, ])->json(‘GET’,route(‘recipe.show’,[‘recipe’ => $recipe->id])); $response->assertStatus(200); // 断言标题是正确的 $this->assertEquals(‘Jollof Rice’,$response->json()[’title’]);}// 测试删除路由public function testDelete(){ $token = $this->authenticate(); $recipe = Recipe::create([ ’title’ => ‘Jollof Rice’, ‘procedure’ => ‘Parboil rice, get pepper and mix, and some spice and serve!’ ]); $this->user->recipes()->save($recipe); $response = $this->withHeaders([ ‘Authorization’ => ‘Bearer ‘. $token, ])->json(‘POST’,route(‘recipe.delete’,[‘recipe’ => $recipe->id])); $response->assertStatus(200); // 断言没有食谱 $this->assertEquals(0,$this->user->recipes()->count());}除了附加测试之外,我们还添加了一个类范围的 $user 属性。 这样,我们不止可以利用 $user 来使用 authenticate 方法不仅生成令牌,而且还为后续其他对 $user 的操作做好了准备。现在运行 $ vendor/bin/phpunit 如果操作正确,你应该进行所有绿色测试。结论希望这能让你深度了解在 TDD 在 Laravel 项目中的运行方式。 他绝对是一个比这更宽泛的概念,一个不受特地方法约束的概念。虽然这种开发方法看起来比常见的调试后期程序要耗时, 但他很适合在代码中尽早捕获错误。虽然有些情况下非 TDD 方式会更有用,但习惯于 TDD 模式开发是一种可靠的技能和习惯。本演练的全部代码可参见 Github here 仓库。请随意使用。干杯!文章转自:https://learnku.com/laravel/t… 更多文章:https://learnku.com/laravel/c… ...

March 6, 2019 · 5 min · jiezi

设计模式超级简单的解释

推荐阅读design-patterns-for-humans 中文版MongoDB 资源、库、工具、应用程序精选列表中文版有哪些鲜为人知,但是很有意思的网站?一份攻城狮笔记每天搜集 Github 上优秀的项目一些有趣的民间故事超好用的谷歌浏览器、Sublime Text、Phpstorm、油猴插件合集*设计模式超简单的解释!(本项目从 design-patterns-for-humans fork)介绍设计模式是反复出现问题的解决方案; 如何解决某些问题的指导方针。它们不是可以插入应用程序并等待神奇发生的类,包或库。相反,这些是如何在某些情况下解决某些问题的指导原则。设计模式是反复出现问题的解决方案; 如何解决某些问题的指导方针维基百科将它们描述为在软件工程中,软件设计模式是软件设计中给定上下文中常见问题的通用可重用解决方案。它不是可以直接转换为源代码或机器代码的完成设计。它是如何解决可在许多不同情况下使用的问题的描述或模板。⚠️注意设计模式不是解决所有问题的灵丹妙药。不要试图强迫他们; 如果这样做的话,应该发生坏事。请记住,设计模式是问题的解决方案,而不是解决问题的解决方案;所以不要过分思考。如果以正确的方式在正确的地方使用,他们可以证明是救世主; 否则他们可能会导致代码混乱。另请注意,下面的代码示例是PHP-7,但是这不应该阻止你因为概念是相同的。设计模式的类型创建型结构型行为型创建型设计模式简单来说创建模式专注于如何实例化对象或相关对象组。维基百科说在软件工程中,创建设计模式是处理对象创建机制的设计模式,试图以适合于该情况的方式创建对象。对象创建的基本形式可能导致设计问题或增加设计的复杂性。创建设计模式通过某种方式控制此对象创建来解决此问题。简单工厂模式(Simple Factory)工厂方法模式(Factory Method)抽象工厂模式(Abstract Factory)构建器模式原型模式(Prototype)单例模式(Singleton)????简单工厂模式(Simple Factory)现实世界的例子考虑一下,你正在建房子,你需要门。你可以穿上你的木匠衣服,带上一些木头,胶水,钉子和建造门所需的所有工具,然后开始在你的房子里建造它,或者你可以简单地打电话给工厂并把内置的门送到你这里不需要了解关于制门的任何信息或处理制作它所带来的混乱。简单来说简单工厂只是为客户端生成一个实例,而不会向客户端公开任何实例化逻辑维基百科说在面向对象编程(OOP)中,工厂是用于创建其他对象的对象 - 正式工厂是一种函数或方法,它从一些方法调用返回变化的原型或类的对象,这被假定为“新”。程序化示例首先,我们有一个门界面和实现interface Door{ public function getWidth(): float; public function getHeight(): float;}class WoodenDoor implements Door{ protected $width; protected $height; public function __construct(float $width, float $height) { $this->width = $width; $this->height = $height; } public function getWidth(): float { return $this->width; } public function getHeight(): float { return $this->height; }}然后,我们有我们的门工厂,门,并返回它class DoorFactory{ public static function makeDoor($width, $height): Door { return new WoodenDoor($width, $height); }}然后它可以用作// Make me a door of 100x200$door = DoorFactory::makeDoor(100, 200);echo ‘Width: ’ . $door->getWidth();echo ‘Height: ’ . $door->getHeight();// Make me a door of 50x100$door2 = DoorFactory::makeDoor(50, 100);什么时候用?当创建一个对象不仅仅是一些分配而且涉及一些逻辑时,将它放在专用工厂中而不是在任何地方重复相同的代码是有意义的。????工厂方法模式(Factory Method)现实世界的例子考虑招聘经理的情况。一个人不可能对每个职位进行面试。根据职位空缺,她必须决定并将面试步骤委托给不同的人。简单来说它提供了一种将实例化逻辑委托给子类的方法。维基百科说在基于类的编程中,工厂方法模式是一种创建模式,它使用工厂方法来处理创建对象的问题,而无需指定将要创建的对象的确切类。这是通过调用工厂方法来创建对象来完成的 - 在接口中指定并由子类实现,或者在基类中实现并可选地由派生类覆盖 - 而不是通过调用构造函数。程序化示例以上面的招聘经理为例。首先,我们有一个访谈者界面和一些实现interface Interviewer{ public function askQuestions();}class Developer implements Interviewer{ public function askQuestions() { echo ‘Asking about design patterns!’; }}class CommunityExecutive implements Interviewer{ public function askQuestions() { echo ‘Asking about community building’; }}现在让我们创造我们的 HiringManagerabstract class HiringManager{ // Factory method abstract protected function makeInterviewer(): Interviewer; public function takeInterview() { $interviewer = $this->makeInterviewer(); $interviewer->askQuestions(); }}现在任何孩子都可以延长并提供所需的面试官class DevelopmentManager extends HiringManager{ protected function makeInterviewer(): Interviewer { return new Developer(); }}class MarketingManager extends HiringManager{ protected function makeInterviewer(): Interviewer { return new CommunityExecutive(); }}然后它可以用作$devManager = new DevelopmentManager();$devManager->takeInterview(); // Output: Asking about design patterns$marketingManager = new MarketingManager();$marketingManager->takeInterview(); // Output: Asking about community building.什么时候用?在类中有一些通用处理但在运行时动态决定所需的子类时很有用。换句话说,当客户端不知道它可能需要什么样的子类时。????抽象工厂模式(Abstract Factory)现实世界的例子从Simple Factory扩展我们的门例子。根据您的需求,您可以从木门店,铁门的铁门或相关商店的PVC门获得木门。另外,你可能需要一个有不同种类特色的家伙来安装门,例如木门木匠,铁门焊机等。你可以看到门之间存在依赖关系,木门需要木匠,铁门需要焊工等简单来说工厂工厂; 将个人但相关/依赖工厂分组在一起而不指定其具体类别的工厂。维基百科说抽象工厂模式提供了一种封装一组具有共同主题但没有指定其具体类的单个工厂的方法程序化示例翻译上面的门例子。首先,我们有我们的Door界面和一些实现interface Door{ public function getDescription();}class WoodenDoor implements Door{ public function getDescription() { echo ‘I am a wooden door’; }}class IronDoor implements Door{ public function getDescription() { echo ‘I am an iron door’; }}然后我们为每种门类型都配备了一些装配专家interface DoorFittingExpert{ public function getDescription();}class Welder implements DoorFittingExpert{ public function getDescription() { echo ‘I can only fit iron doors’; }}class Carpenter implements DoorFittingExpert{ public function getDescription() { echo ‘I can only fit wooden doors’; }}现在我们有抽象工厂,让我们制作相关对象的家庭,即木门工厂将创建一个木门和木门配件专家和铁门工厂将创建一个铁门和铁门配件专家interface DoorFactory{ public function makeDoor(): Door; public function makeFittingExpert(): DoorFittingExpert;}// Wooden factory to return carpenter and wooden doorclass WoodenDoorFactory implements DoorFactory{ public function makeDoor(): Door { return new WoodenDoor(); } public function makeFittingExpert(): DoorFittingExpert { return new Carpenter(); }}// Iron door factory to get iron door and the relevant fitting expertclass IronDoorFactory implements DoorFactory{ public function makeDoor(): Door { return new IronDoor(); } public function makeFittingExpert(): DoorFittingExpert { return new Welder(); }}然后它可以用作$woodenFactory = new WoodenDoorFactory();$door = $woodenFactory->makeDoor();$expert = $woodenFactory->makeFittingExpert();$door->getDescription(); // Output: I am a wooden door$expert->getDescription(); // Output: I can only fit wooden doors// Same for Iron Factory$ironFactory = new IronDoorFactory();$door = $ironFactory->makeDoor();$expert = $ironFactory->makeFittingExpert();$door->getDescription(); // Output: I am an iron door$expert->getDescription(); // Output: I can only fit iron doors正如你所看到的木门工厂的封装carpenter和wooden door还铁门厂已封装的iron door和welder。因此,它帮助我们确保对于每个创建的门,我们没有得到错误的拟合专家。什么时候用?当存在相互关联的依赖关系时,涉及非简单的创建逻辑????构建器模式现实世界的例子想象一下,你在Hardee’s,你订购了一个特定的交易,让我们说,“Big Hardee”,他们毫无_疑问地_把它交给你了; 这是简单工厂的例子。但有些情况下,创建逻辑可能涉及更多步骤。例如,你想要一个定制的地铁交易,你有多种选择如何制作你的汉堡,例如你想要什么面包?你想要什么类型的酱汁?你想要什么奶酪?在这种情况下,建筑商模式得以拯救。简单来说允许您创建不同风格的对象,同时避免构造函数污染。当有几种风格的物体时很有用。或者在创建对象时涉及很多步骤。维基百科说构建器模式是对象创建软件设计模式,其目的是找到伸缩构造器反模式的解决方案。话虽如此,让我补充说一下伸缩构造函数反模式是什么。在某一点或另一点,我们都看到了如下构造函数:public function __construct($size, $cheese = true, $pepperoni = true, $tomato = false, $lettuce = true){}如你看到的; 构造函数参数的数量很快就会失控,并且可能难以理解参数的排列。此外,如果您希望将来添加更多选项,此参数列表可能会继续增长。这被称为伸缩构造器反模式。程序化示例理智的替代方案是使用构建器模式。首先,我们要制作汉堡class Burger{ protected $size; protected $cheese = false; protected $pepperoni = false; protected $lettuce = false; protected $tomato = false; public function __construct(BurgerBuilder $builder) { $this->size = $builder->size; $this->cheese = $builder->cheese; $this->pepperoni = $builder->pepperoni; $this->lettuce = $builder->lettuce; $this->tomato = $builder->tomato; }}然后我们有了建设者class BurgerBuilder{ public $size; public $cheese = false; public $pepperoni = false; public $lettuce = false; public $tomato = false; public function __construct(int $size) { $this->size = $size; } public function addPepperoni() { $this->pepperoni = true; return $this; } public function addLettuce() { $this->lettuce = true; return $this; } public function addCheese() { $this->cheese = true; return $this; } public function addTomato() { $this->tomato = true; return $this; } public function build(): Burger { return new Burger($this); }}然后它可以用作:$burger = (new BurgerBuilder(14)) ->addPepperoni() ->addLettuce() ->addTomato() ->build();什么时候用?当可能存在几种类型的对象并避免构造函数伸缩时。与工厂模式的主要区别在于:当创建是一步过程时,将使用工厂模式,而当创建是多步骤过程时,将使用构建器模式。????原型模式(Prototype)现实世界的例子记得多莉?被克隆的羊!让我们不详细介绍,但关键点在于它完全是关于克隆的简单来说通过克隆基于现有对象创建对象。维基百科说原型模式是软件开发中的创新设计模式。当要创建的对象类型由原型实例确定时使用它,该实例被克隆以生成新对象。简而言之,它允许您创建现有对象的副本并根据需要进行修改,而不是从头开始创建对象并进行设置。程序化示例在PHP中,它可以很容易地使用 cloneclass Sheep{ protected $name; protected $category; public function __construct(string $name, string $category = ‘Mountain Sheep’) { $this->name = $name; $this->category = $category; } public function setName(string $name) { $this->name = $name; } public function getName() { return $this->name; } public function setCategory(string $category) { $this->category = $category; } public function getCategory() { return $this->category; }}然后它可以像下面一样克隆$original = new Sheep(‘Jolly’);echo $original->getName(); // Jollyecho $original->getCategory(); // Mountain Sheep// Clone and modify what is required$cloned = clone $original;$cloned->setName(‘Dolly’);echo $cloned->getName(); // Dollyecho $cloned->getCategory(); // Mountain sheep您也可以使用魔术方法__clone来修改克隆行为。什么时候用?当需要一个与现有对象类似的对象时,或者与克隆相比,创建的成本会很高。????单例模式(Singleton)现实世界的例子一次只能有一个国家的总统。无论何时打电话,都必须将同一位总统付诸行动。这里的总统是单身人士。简单来说确保只创建特定类的一个对象。维基百科说在软件工程中,单例模式是一种软件设计模式,它将类的实例化限制为一个对象。当需要一个对象来协调整个系统的操作时,这非常有用。单例模式实际上被认为是反模式,应该避免过度使用它。它不一定是坏的,可能有一些有效的用例,但应谨慎使用,因为它在您的应用程序中引入了一个全局状态,并且在一个地方更改它可能会影响其他区域,并且它可能变得非常难以调试。关于它们的另一个坏处是它使你的代码紧密耦合加上嘲弄单例可能很困难。程序化示例要创建单例,请将构造函数设为私有,禁用克隆,禁用扩展并创建静态变量以容纳实例final class President{ private static $instance; private function __construct() { // Hide the constructor } public static function getInstance(): President { if (!self::$instance) { self::$instance = new self(); } return self::$instance; } private function __clone() { // Disable cloning } private function __wakeup() { // Disable unserialize }}然后才能使用$president1 = President::getInstance();$president2 = President::getInstance();var_dump($president1 === $president2); // true结构型设计模式简单来说结构模式主要涉及对象组成,或者换句话说,实体如何相互使用。或者另一种解释是,它们有助于回答“如何构建软件组件?”维基百科说在软件工程中,结构设计模式是通过识别实现实体之间关系的简单方法来简化设计的设计模式。适配器模式(Adapter)桥梁模式(Bridge)组合模式(Composite)装饰模式(Decorator)门面模式(Facade)享元模式(Flyweight)代理模式(Proxy)????适配器模式(Adapter)现实世界的例子请注意,您的存储卡中有一些照片,需要将它们传输到计算机上。为了传输它们,您需要某种与您的计算机端口兼容的适配器,以便您可以将存储卡连接到您的计算机。在这种情况下,读卡器是适配器。另一个例子是着名的电源适配器; 三脚插头不能连接到双管插座,需要使用电源适配器,使其与双叉插座兼容。另一个例子是翻译人员将一个人所说的话翻译成另一个人简单来说适配器模式允许您在适配器中包装其他不兼容的对象,以使其与另一个类兼容。维基百科说在软件工程中,适配器模式是一种软件设计模式,它允许将现有类的接口用作另一个接口。它通常用于使现有类与其他类一起工作而无需修改其源代码。程序化示例考虑一个有猎人的游戏,他猎杀狮子。首先,我们有一个Lion所有类型的狮子必须实现的接口interface Lion{ public function roar();}class AfricanLion implements Lion{ public function roar() { }}class AsianLion implements Lion{ public function roar() { }}猎人期望任何Lion接口的实现都可以进行搜索。class Hunter{ public function hunt(Lion $lion) { $lion->roar(); }}现在让我们说我们必须WildDog在我们的游戏中添加一个,以便猎人也可以追捕它。但我们不能直接这样做,因为狗有不同的界面。为了使它与我们的猎人兼容,我们将不得不创建一个兼容的适配器// This needs to be added to the gameclass WildDog{ public function bark() { }}// Adapter around wild dog to make it compatible with our gameclass WildDogAdapter implements Lion{ protected $dog; public function __construct(WildDog $dog) { $this->dog = $dog; } public function roar() { $this->dog->bark(); }}而现在WildDog可以在我们的游戏中使用WildDogAdapter。$wildDog = new WildDog();$wildDogAdapter = new WildDogAdapter($wildDog);$hunter = new Hunter();$hunter->hunt($wildDogAdapter);????桥梁模式(Bridge)现实世界的例子考虑您有一个包含不同页面的网站,您应该允许用户更改主题。你会怎么做?为每个主题创建每个页面的多个副本,或者您只是创建单独的主题并根据用户的首选项加载它们?桥模式允许你做第二个ie用简单的话说桥模式是关于优先于继承的组合。实现细节从层次结构推送到具有单独层次结构的另一个对象。维基百科说桥接模式是软件工程中使用的设计模式,旨在“将抽象与其实现分离,以便两者可以独立变化”程序化示例从上面翻译我们的WebPage示例。这里我们有WebPage层次结构interface WebPage{ public function __construct(Theme $theme); public function getContent();}class About implements WebPage{ protected $theme; public function __construct(Theme $theme) { $this->theme = $theme; } public function getContent() { return “About page in " . $this->theme->getColor(); }}class Careers implements WebPage{ protected $theme; public function __construct(Theme $theme) { $this->theme = $theme; } public function getContent() { return “Careers page in " . $this->theme->getColor(); }}和单独的主题层次结构interface Theme{ public function getColor();}class DarkTheme implements Theme{ public function getColor() { return ‘Dark Black’; }}class LightTheme implements Theme{ public function getColor() { return ‘Off white’; }}class AquaTheme implements Theme{ public function getColor() { return ‘Light blue’; }}而且这两个层次结构$darkTheme = new DarkTheme();$about = new About($darkTheme);$careers = new Careers($darkTheme);echo $about->getContent(); // “About page in Dark Black”;echo $careers->getContent(); // “Careers page in Dark Black”;????组合模式(Composite)现实世界的例子每个组织都由员工组成。每个员工都有相同的功能,即有工资,有一些责任,可能会或可能不会向某人报告,可能会或可能不会有一些下属等。简单来说复合模式允许客户以统一的方式处理单个对象。维基百科说在软件工程中,复合模式是分区设计模式。复合模式描述了一组对象的处理方式与对象的单个实例相同。复合的意图是将对象“组合”成树结构以表示部分整体层次结构。通过实现复合模式,客户可以统一处理单个对象和组合。程序化示例以上面的员工为例。这里我们有不同的员工类型interface Employee{ public function __construct(string $name, float $salary); public function getName(): string; public function setSalary(float $salary); public function getSalary(): float; public function getRoles(): array;}class Developer implements Employee{ protected $salary; protected $name; protected $roles; public function __construct(string $name, float $salary) { $this->name = $name; $this->salary = $salary; } public function getName(): string { return $this->name; } public function setSalary(float $salary) { $this->salary = $salary; } public function getSalary(): float { return $this->salary; } public function getRoles(): array { return $this->roles; }}class Designer implements Employee{ protected $salary; protected $name; protected $roles; public function __construct(string $name, float $salary) { $this->name = $name; $this->salary = $salary; } public function getName(): string { return $this->name; } public function setSalary(float $salary) { $this->salary = $salary; } public function getSalary(): float { return $this->salary; } public function getRoles(): array { return $this->roles; }}然后我们有一个由几种不同类型的员工组成的组织class Organization{ protected $employees; public function addEmployee(Employee $employee) { $this->employees[] = $employee; } public function getNetSalaries(): float { $netSalary = 0; foreach ($this->employees as $employee) { $netSalary += $employee->getSalary(); } return $netSalary; }}然后它可以用作// Prepare the employees$john = new Developer(‘John Doe’, 12000);$jane = new Designer(‘Jane Doe’, 15000);// Add them to organization$organization = new Organization();$organization->addEmployee($john);$organization->addEmployee($jane);echo “Net salaries: " . $organization->getNetSalaries(); // Net Salaries: 27000☕装饰模式(Decorator)现实世界的例子想象一下,您经营一家提供多种服务的汽车服务店。现在你如何计算收费账单?您选择一项服务并动态地向其添加所提供服务的价格,直到您获得最终成本。这里的每种服务都是装饰者。简单来说Decorator模式允许您通过将对象包装在装饰器类的对象中来动态更改对象在运行时的行为。维基百科说在面向对象的编程中,装饰器模式是一种设计模式,它允许将行为静态或动态地添加到单个对象,而不会影响同一类中其他对象的行为。装饰器模式通常用于遵守单一责任原则,因为它允许在具有独特关注区域的类之间划分功能。程序化示例让我们以咖啡为例。首先,我们有一个简单的咖啡实现咖啡界面interface Coffee{ public function getCost(); public function getDescription();}class SimpleCoffee implements Coffee{ public function getCost() { return 10; } public function getDescription() { return ‘Simple coffee’; }}我们希望使代码可扩展,以允许选项在需要时修改它。让我们做一些附加组件(装饰器)class MilkCoffee implements Coffee{ protected $coffee; public function __construct(Coffee $coffee) { $this->coffee = $coffee; } public function getCost() { return $this->coffee->getCost() + 2; } public function getDescription() { return $this->coffee->getDescription() . ‘, milk’; }}class WhipCoffee implements Coffee{ protected $coffee; public function __construct(Coffee $coffee) { $this->coffee = $coffee; } public function getCost() { return $this->coffee->getCost() + 5; } public function getDescription() { return $this->coffee->getDescription() . ‘, whip’; }}class VanillaCoffee implements Coffee{ protected $coffee; public function __construct(Coffee $coffee) { $this->coffee = $coffee; } public function getCost() { return $this->coffee->getCost() + 3; } public function getDescription() { return $this->coffee->getDescription() . ‘, vanilla’; }}让我们现在喝杯咖啡$someCoffee = new SimpleCoffee();echo $someCoffee->getCost(); // 10echo $someCoffee->getDescription(); // Simple Coffee$someCoffee = new MilkCoffee($someCoffee);echo $someCoffee->getCost(); // 12echo $someCoffee->getDescription(); // Simple Coffee, milk$someCoffee = new WhipCoffee($someCoffee);echo $someCoffee->getCost(); // 17echo $someCoffee->getDescription(); // Simple Coffee, milk, whip$someCoffee = new VanillaCoffee($someCoffee);echo $someCoffee->getCost(); // 20echo $someCoffee->getDescription(); // Simple Coffee, milk, whip, vanilla????门面模式(Facade)现实世界的例子你怎么打开电脑?“按下电源按钮”你说!这就是你所相信的,因为你正在使用计算机在外部提供的简单界面,在内部它必须做很多事情来实现它。这个复杂子系统的简单接口是一个外观。简单来说Facade模式为复杂的子系统提供了简化的界面。维基百科说外观是一个对象,它为更大的代码体提供了简化的接口,例如类库。程序化示例从上面看我们的计算机示例。这里我们有电脑课class Computer{ public function getElectricShock() { echo “Ouch!”; } public function makeSound() { echo “Beep beep!”; } public function showLoadingScreen() { echo “Loading..”; } public function bam() { echo “Ready to be used!”; } public function closeEverything() { echo “Bup bup bup buzzzz!”; } public function sooth() { echo “Zzzzz”; } public function pullCurrent() { echo “Haaah!”; }}在这里,我们有门面class ComputerFacade{ protected $computer; public function __construct(Computer $computer) { $this->computer = $computer; } public function turnOn() { $this->computer->getElectricShock(); $this->computer->makeSound(); $this->computer->showLoadingScreen(); $this->computer->bam(); } public function turnOff() { $this->computer->closeEverything(); $this->computer->pullCurrent(); $this->computer->sooth(); }}现在使用立面$computer = new ComputerFacade(new Computer());$computer->turnOn(); // Ouch! Beep beep! Loading.. Ready to be used!$computer->turnOff(); // Bup bup buzzz! Haah! Zzzzz????享元模式(Flyweight)现实世界的例子你有没有从一些摊位买到新鲜的茶?他们经常制作你需要的不止一个杯子,并为其他任何客户保存其余的,以节省资源,例如天然气等.Flyweight模式就是那个即共享。简单来说它用于通过尽可能多地与类似对象共享来最小化内存使用或计算开销。维基百科说在计算机编程中,flyweight是一种软件设计模式。flyweight是一个通过与其他类似对象共享尽可能多的数据来最小化内存使用的对象; 当简单的重复表示将使用不可接受的内存量时,它是一种大量使用对象的方法。程序化的例子从上面翻译我们的茶例子。首先,我们有茶类和茶具// Anything that will be cached is flyweight.// Types of tea here will be flyweights.class KarakTea{}// Acts as a factory and saves the teaclass TeaMaker{ protected $availableTea = []; public function make($preference) { if (empty($this->availableTea[$preference])) { $this->availableTea[$preference] = new KarakTea(); } return $this->availableTea[$preference]; }}然后我们有TeaShop接受订单并为他们服务class TeaShop{ protected $orders; protected $teaMaker; public function __construct(TeaMaker $teaMaker) { $this->teaMaker = $teaMaker; } public function takeOrder(string $teaType, int $table) { $this->orders[$table] = $this->teaMaker->make($teaType); } public function serve() { foreach ($this->orders as $table => $tea) { echo “Serving tea to table# " . $table; } }}它可以如下使用$teaMaker = new TeaMaker();$shop = new TeaShop($teaMaker);$shop->takeOrder(’less sugar’, 1);$shop->takeOrder(‘more milk’, 2);$shop->takeOrder(‘without sugar’, 5);$shop->serve();// Serving tea to table# 1// Serving tea to table# 2// Serving tea to table# 5????代理模式(Proxy)现实世界的例子你有没有用过门禁卡进门?打开该门有多种选择,即可以使用门禁卡或按下绕过安检的按钮打开。门的主要功能是打开,但在它上面添加了一个代理来添加一些功能。让我用下面的代码示例更好地解释它。简单来说使用代理模式,类表示另一个类的功能。维基百科说代理以其最一般的形式,是一个充当其他东西的接口的类。代理是一个包装器或代理对象,客户端正在调用它来访问幕后的真实服务对象。使用代理可以简单地转发到真实对象,或者可以提供额外的逻辑。在代理中,可以提供额外的功能,例如当对真实对象的操作是资源密集时的高速缓存,或者在调用对象的操作之前检查先决条件。程序化示例从上面看我们的安全门示例。首先我们有门界面和门的实现interface Door{ public function open(); public function close();}class LabDoor implements Door{ public function open() { echo “Opening lab door”; } public function close() { echo “Closing the lab door”; }}然后我们有一个代理来保护我们想要的任何门class SecuredDoor{ protected $door; public function __construct(Door $door) { $this->door = $door; } public function open($password) { if ($this->authenticate($password)) { $this->door->open(); } else { echo “Big no! It ain’t possible.”; } } public function authenticate($password) { return $password === ‘$ecr@t’; } public function close() { $this->door->close(); }}以下是它的使用方法$door = new SecuredDoor(new LabDoor());$door->open(‘invalid’); // Big no! It ain’t possible.$door->open(’$ecr@t’); // Opening lab door$door->close(); // Closing lab door另一个例子是某种数据映射器实现。例如,我最近使用这种模式为MongoDB制作了一个ODM(对象数据映射器),我在使用魔术方法的同时围绕mongo类编写了一个代理__call()。所有方法调用被代理到原始蒙戈类和结果检索到的返回,因为它是但在的情况下,find或findOne数据被映射到所需的类对象和对象返回代替Cursor。行为型设计模式简单来说它关注对象之间的职责分配。它们与结构模式的不同之处在于它们不仅指定了结构,还概述了它们之间的消息传递/通信模式。或者换句话说,他们协助回答“如何在软件组件中运行行为?”维基百科说在软件工程中,行为设计模式是识别对象之间的共同通信模式并实现这些模式的设计模式。通过这样做,这些模式增加了执行该通信的灵活性。责任链模式(Chain Of Responsibilities)命令行模式(Command)迭代器模式(Iterator)中介者模式(Mediator)备忘录模式(Memento)观察者模式(Observer)访问者模式(Visitor)策略模式(Strategy)状态模式(State)模板方法模式(Template Method)????责任链模式(Chain Of Responsibilities)现实世界的例子例如,你有三种付款方式(A,B和C)安装在您的帐户; 每个都有不同的数量。A有100美元,B具有300美元和C具有1000美元,以及支付偏好被选择作为A再B然后C。你试着购买价值210美元的东西。使用责任链,首先A会检查帐户是否可以进行购买,如果是,则进行购买并且链条将被破坏。如果没有,请求将继续进行帐户B检查金额,如果是链将被破坏,否则请求将继续转发,直到找到合适的处理程序。在这里A,B和C 是链条的链接,整个现象是责任链。简单来说它有助于构建一系列对象。请求从一端进入并继续从一个对象到另一个对象,直到找到合适的处理程序。维基百科说在面向对象的设计中,责任链模式是一种由命令对象源和一系列处理对象组成的设计模式。每个处理对象都包含定义它可以处理的命令对象类型的逻辑; 其余的传递给链中的下一个处理对象。程序化示例翻译上面的帐户示例。首先,我们有一个基本帐户,其中包含将帐户链接在一起的逻辑和一些帐户abstract class Account{ protected $successor; protected $balance; public function setNext(Account $account) { $this->successor = $account; } public function pay(float $amountToPay) { if ($this->canPay($amountToPay)) { echo sprintf(‘Paid %s using %s’ . PHP_EOL, $amountToPay, get_called_class()); } elseif ($this->successor) { echo sprintf(‘Cannot pay using %s. Proceeding ..’ . PHP_EOL, get_called_class()); $this->successor->pay($amountToPay); } else { throw new Exception(‘None of the accounts have enough balance’); } } public function canPay($amount): bool { return $this->balance >= $amount; }}class Bank extends Account{ protected $balance; public function __construct(float $balance) { $this->balance = $balance; }}class Paypal extends Account{ protected $balance; public function __construct(float $balance) { $this->balance = $balance; }}class Bitcoin extends Account{ protected $balance; public function __construct(float $balance) { $this->balance = $balance; }}现在让我们使用上面定义的链接准备链(即Bank,Paypal,Bitcoin)// Let’s prepare a chain like below// $bank->$paypal->$bitcoin//// First priority bank// If bank can’t pay then paypal// If paypal can’t pay then bit coin$bank = new Bank(100); // Bank with balance 100$paypal = new Paypal(200); // Paypal with balance 200$bitcoin = new Bitcoin(300); // Bitcoin with balance 300$bank->setNext($paypal);$paypal->setNext($bitcoin);// Let’s try to pay using the first priority i.e. bank$bank->pay(259);// Output will be// ==============// Cannot pay using bank. Proceeding ..// Cannot pay using paypal. Proceeding ..:// Paid 259 using Bitcoin!????命令行模式(Command)现实世界的例子一个通用的例子是你在餐厅点餐。您(即Client)要求服务员(即Invoker)携带一些食物(即Command),服务员只是将请求转发给主厨(即Receiver),该主厨知道什么以及如何烹饪。另一个例子是你(即)使用遥控器()Client打开(即Command)电视(即)。ReceiverInvoker简单来说允许您将操作封装在对象中。这种模式背后的关键思想是提供将客户端与接收器分离的方法。维基百科说在面向对象的编程中,命令模式是行为设计模式,其中对象用于封装执行动作或稍后触发事件所需的所有信息。此信息包括方法名称,拥有该方法的对象以及方法参数的值。程序化示例首先,我们有接收器,它可以执行每个可以执行的操作// Receiverclass Bulb{ public function turnOn() { echo “Bulb has been lit”; } public function turnOff() { echo “Darkness!”; }}然后我们有一个接口,每个命令将实现,然后我们有一组命令interface Command{ public function execute(); public function undo(); public function redo();}// Commandclass TurnOn implements Command{ protected $bulb; public function __construct(Bulb $bulb) { $this->bulb = $bulb; } public function execute() { $this->bulb->turnOn(); } public function undo() { $this->bulb->turnOff(); } public function redo() { $this->execute(); }}class TurnOff implements Command{ protected $bulb; public function __construct(Bulb $bulb) { $this->bulb = $bulb; } public function execute() { $this->bulb->turnOff(); } public function undo() { $this->bulb->turnOn(); } public function redo() { $this->execute(); }}然后我们Invoker与客户端进行交互以处理任何命令// Invokerclass RemoteControl{ public function submit(Command $command) { $command->execute(); }}最后,让我们看看我们如何在客户端使用它$bulb = new Bulb();$turnOn = new TurnOn($bulb);$turnOff = new TurnOff($bulb);$remote = new RemoteControl();$remote->submit($turnOn); // Bulb has been lit!$remote->submit($turnOff); // Darkness!命令模式还可用于实现基于事务的系统。在执行命令时,一直保持命令历史记录的位置。如果成功执行了最后一个命令,那么所有好处都只是遍历历史记录并继续执行undo所有已执行的命令。➿迭代器模式(Iterator)现实世界的例子旧的无线电设备将是迭代器的一个很好的例子,用户可以从某个频道开始,然后使用下一个或上一个按钮来浏览相应的频道。或者以MP3播放器或电视机为例,您可以按下下一个和上一个按钮来浏览连续的频道,换句话说,它们都提供了一个界面来迭代各自的频道,歌曲或电台。简单来说它提供了一种访问对象元素而不暴露底层表示的方法。维基百科说在面向对象的编程中,迭代器模式是一种设计模式,其中迭代器用于遍历容器并访问容器的元素。迭代器模式将算法与容器分离; 在某些情况下,算法必然是特定于容器的,因此不能解耦。程序化的例子在PHP中,使用SPL(标准PHP库)很容易实现。从上面翻译我们的广播电台示例。首先,我们有RadioStationclass RadioStation{ protected $frequency; public function __construct(float $frequency) { $this->frequency = $frequency; } public function getFrequency(): float { return $this->frequency; }}然后我们有了迭代器use Countable;use Iterator;class StationList implements Countable, Iterator{ /* @var RadioStation[] $stations / protected $stations = []; /* @var int $counter */ protected $counter; public function addStation(RadioStation $station) { $this->stations[] = $station; } public function removeStation(RadioStation $toRemove) { $toRemoveFrequency = $toRemove->getFrequency(); $this->stations = array_filter($this->stations, function (RadioStation $station) use ($toRemoveFrequency) { return $station->getFrequency() !== $toRemoveFrequency; }); } public function count(): int { return count($this->stations); } public function current(): RadioStation { return $this->stations[$this->counter]; } public function key() { return $this->counter; } public function next() { $this->counter++; } public function rewind() { $this->counter = 0; } public function valid(): bool { return isset($this->stations[$this->counter]); }}然后它可以用作$stationList = new StationList();$stationList->addStation(new RadioStation(89));$stationList->addStation(new RadioStation(101));$stationList->addStation(new RadioStation(102));$stationList->addStation(new RadioStation(103.2));foreach($stationList as $station) { echo $station->getFrequency() . PHP_EOL;}$stationList->removeStation(new RadioStation(89)); // Will remove station 89????中介者模式(Mediator)现实世界的例子一个典型的例子就是当你在手机上与某人交谈时,有一个网络提供商坐在你和他们之间,你的对话通过它而不是直接发送。在这种情况下,网络提供商是中介。简单来说Mediator模式添加第三方对象(称为mediator)来控制两个对象(称为同事)之间的交互。它有助于减少彼此通信的类之间的耦合。因为现在他们不需要了解彼此的实施。维基百科说在软件工程中,中介模式定义了一个对象,该对象封装了一组对象的交互方式。由于它可以改变程序的运行行为,因此这种模式被认为是一种行为模式。程序化示例这是聊天室(即中介)与用户(即同事)相互发送消息的最简单示例。首先,我们有调解员即聊天室interface ChatRoomMediator { public function showMessage(User $user, string $message);}// Mediatorclass ChatRoom implements ChatRoomMediator{ public function showMessage(User $user, string $message) { $time = date(‘M d, y H:i’); $sender = $user->getName(); echo $time . ‘[’ . $sender . ‘]:’ . $message; }}然后我们有我们的用户,即同事class User { protected $name; protected $chatMediator; public function __construct(string $name, ChatRoomMediator $chatMediator) { $this->name = $name; $this->chatMediator = $chatMediator; } public function getName() { return $this->name; } public function send($message) { $this->chatMediator->showMessage($this, $message); }}和用法$mediator = new ChatRoom();$john = new User(‘John Doe’, $mediator);$jane = new User(‘Jane Doe’, $mediator);$john->send(‘Hi there!’);$jane->send(‘Hey!’);// Output will be// Feb 14, 10:58 [John]: Hi there!// Feb 14, 10:58 [Jane]: Hey!????备忘录模式(Memento)现实世界的例子以计算器(即发起者)为例,无论何时执行某些计算,最后的计算都会保存在内存中(即纪念品),以便您可以回到它并使用某些操作按钮(即看管人)恢复它。简单来说Memento模式是关于以稍后可以以平滑方式恢复的方式捕获和存储对象的当前状态。维基百科说memento模式是一种软件设计模式,它提供将对象恢复到其先前状态的能力(通过回滚撤消)。当您需要提供某种撤消功能时通常很有用。程序化示例让我们举一个文本编辑器的例子,它不时地保存状态,你可以根据需要恢复。首先,我们有memento对象,可以保存编辑器状态class EditorMemento{ protected $content; public function __construct(string $content) { $this->content = $content; } public function getContent() { return $this->content; }}然后我们有我们的编辑器即即将使用memento对象的创作者class Editor{ protected $content = ‘’; public function type(string $words) { $this->content = $this->content . ’ ’ . $words; } public function getContent() { return $this->content; } public function save() { return new EditorMemento($this->content); } public function restore(EditorMemento $memento) { $this->content = $memento->getContent(); }}然后它可以用作$editor = new Editor();// Type some stuff$editor->type(‘This is the first sentence.’);$editor->type(‘This is second.’);// Save the state to restore to : This is the first sentence. This is second.$saved = $editor->save();// Type some more$editor->type(‘And this is third.’);// Output: Content before Savingecho $editor->getContent(); // This is the first sentence. This is second. And this is third.// Restoring to last saved state$editor->restore($saved);$editor->getContent(); // This is the first sentence. This is second.????观察者模式(Observer)现实世界的例子一个很好的例子是求职者,他们订阅了一些职位发布网站,只要有匹配的工作机会,他们就会得到通知。简单来说定义对象之间的依赖关系,以便每当对象更改其状态时,都会通知其所有依赖项。维基百科说观察者模式是一种软件设计模式,其中一个称为主体的对象维护其依赖者列表,称为观察者,并通常通过调用其中一种方法自动通知它们任何状态变化。程序化的例子从上面翻译我们的例子。首先,我们有求职者需要通知职位发布class JobPost{ protected $title; public function __construct(string $title) { $this->title = $title; } public function getTitle() { return $this->title; }}class JobSeeker implements Observer{ protected $name; public function __construct(string $name) { $this->name = $name; } public function onJobPosted(JobPost $job) { // Do something with the job posting echo ‘Hi ’ . $this->name . ‘! New job posted: ‘. $job->getTitle(); }}然后我们会找到求职者会订阅的招聘信息class EmploymentAgency implements Observable{ protected $observers = []; protected function notify(JobPost $jobPosting) { foreach ($this->observers as $observer) { $observer->onJobPosted($jobPosting); } } public function attach(Observer $observer) { $this->observers[] = $observer; } public function addJob(JobPost $jobPosting) { $this->notify($jobPosting); }}然后它可以用作// Create subscribers$johnDoe = new JobSeeker(‘John Doe’);$janeDoe = new JobSeeker(‘Jane Doe’);// Create publisher and attach subscribers$jobPostings = new EmploymentAgency();$jobPostings->attach($johnDoe);$jobPostings->attach($janeDoe);// Add a new job and see if subscribers get notified$jobPostings->addJob(new JobPost(‘Software Engineer’));// Output// Hi John Doe! New job posted: Software Engineer// Hi Jane Doe! New job posted: Software Engineer????访问者模式(Visitor)现实世界的例子考虑去迪拜的人。他们只需要一种方式(即签证)进入迪拜。抵达后,他们可以自己来迪拜的任何地方,而无需征求许可或做一些腿部工作,以便访问这里的任何地方; 让他们知道一个地方,他们可以访问它。访客模式可以让您做到这一点,它可以帮助您添加访问的地方,以便他们可以尽可能多地访问,而无需做任何腿部工作。简单来说访客模式允许您向对象添加更多操作,而无需修改它们。维基百科说在面向对象的编程和软件工程中,访问者设计模式是一种将算法与其运行的对象结构分离的方法。这种分离的实际结果是能够在不修改这些结构的情况下向现有对象结构添加新操作。这是遵循开放/封闭原则的一种方式。程序化的例子让我们举一个动物园模拟的例子,我们有几种不同的动物,我们必须让它们成为声音。让我们用访客模式翻译这个// Visiteeinterface Animal{ public function accept(AnimalOperation $operation);}// Visitorinterface AnimalOperation{ public function visitMonkey(Monkey $monkey); public function visitLion(Lion $lion); public function visitDolphin(Dolphin $dolphin);}然后我们有动物实施class Monkey implements Animal{ public function shout() { echo ‘Ooh oo aa aa!’; } public function accept(AnimalOperation $operation) { $operation->visitMonkey($this); }}class Lion implements Animal{ public function roar() { echo ‘Roaaar!’; } public function accept(AnimalOperation $operation) { $operation->visitLion($this); }}class Dolphin implements Animal{ public function speak() { echo ‘Tuut tuttu tuutt!’; } public function accept(AnimalOperation $operation) { $operation->visitDolphin($this); }}让我们实现我们的访客class Speak implements AnimalOperation{ public function visitMonkey(Monkey $monkey) { $monkey->shout(); } public function visitLion(Lion $lion) { $lion->roar(); } public function visitDolphin(Dolphin $dolphin) { $dolphin->speak(); }}然后它可以用作$monkey = new Monkey();$lion = new Lion();$dolphin = new Dolphin();$speak = new Speak();$monkey->accept($speak); // Ooh oo aa aa! $lion->accept($speak); // Roaaar!$dolphin->accept($speak); // Tuut tutt tuutt!我们可以通过为动物建立一个继承层次结构来做到这一点,但是每当我们不得不为动物添加新动作时我们就必须修改动物。但现在我们不必改变它们。例如,假设我们被要求向动物添加跳跃行为,我们可以通过创建新的访问者来添加它,即class Jump implements AnimalOperation{ public function visitMonkey(Monkey $monkey) { echo ‘Jumped 20 feet high! on to the tree!’; } public function visitLion(Lion $lion) { echo ‘Jumped 7 feet! Back on the ground!’; } public function visitDolphin(Dolphin $dolphin) { echo ‘Walked on water a little and disappeared’; }}并用于使用$jump = new Jump();$monkey->accept($speak); // Ooh oo aa aa!$monkey->accept($jump); // Jumped 20 feet high! on to the tree!$lion->accept($speak); // Roaaar!$lion->accept($jump); // Jumped 7 feet! Back on the ground!$dolphin->accept($speak); // Tuut tutt tuutt!$dolphin->accept($jump); // Walked on water a little and disappeared????策略模式(Strategy)现实世界的例子考虑排序的例子,我们实现了冒泡排序,但数据开始增长,冒泡排序开始变得非常缓慢。为了解决这个问题,我们实现了快速排序。但是现在虽然快速排序算法对大型数据集的效果更好,但对于较小的数据集来说速度非常慢。为了解决这个问题,我们实施了一个策略,对于小型数据集,将使用冒泡排序并进行更大规模的快速排序。简单来说策略模式允许您根据情况切换算法或策略。维基百科说在计算机编程中,策略模式(也称为策略模式)是一种行为软件设计模式,可以在运行时选择算法的行为。程序化的例子从上面翻译我们的例子。首先,我们有战略界面和不同的战略实施interface SortStrategy{ public function sort(array $dataset): array;}class BubbleSortStrategy implements SortStrategy{ public function sort(array $dataset): array { echo “Sorting using bubble sort”; // Do sorting return $dataset; }}class QuickSortStrategy implements SortStrategy{ public function sort(array $dataset): array { echo “Sorting using quick sort”; // Do sorting return $dataset; }}然后我们的客户将使用任何策略class Sorter{ protected $sorter; public function __construct(SortStrategy $sorter) { $this->sorter = $sorter; } public function sort(array $dataset): array { return $this->sorter->sort($dataset); }}它可以用作And it can be used as$dataset = [1, 5, 4, 3, 2, 8];$sorter = new Sorter(new BubbleSortStrategy());$sorter->sort($dataset); // Output : Sorting using bubble sort$sorter = new Sorter(new QuickSortStrategy());$sorter->sort($dataset); // Output : Sorting using quick sort????状态模式(State)现实世界的例子想象一下,你正在使用一些绘图应用程序,你选择绘制画笔。现在画笔根据所选颜色改变其行为,即如果你选择了红色,它会画成红色,如果是蓝色则会是蓝色等。简单来说它允许您在状态更改时更改类的行为。维基百科说状态模式是一种行为软件设计模式,它以面向对象的方式实现状态机。使用状态模式,通过将每个单独的状态实现为状态模式接口的派生类来实现状态机,并通过调用由模式的超类定义的方法来实现状态转换。状态模式可以解释为一种策略模式,它能够通过调用模式接口中定义的方法来切换当前策略。程序化的例子让我们以文本编辑器为例,它允许您更改键入的文本的状态,即如果您选择了粗体,则开始以粗体显示,如果是斜体,则以斜体显示等。首先,我们有状态接口和一些状态实现interface WritingState{ public function write(string $words);}class UpperCase implements WritingState{ public function write(string $words) { echo strtoupper($words); }}class LowerCase implements WritingState{ public function write(string $words) { echo strtolower($words); }}class DefaultText implements WritingState{ public function write(string $words) { echo $words; }}然后我们有编辑class TextEditor{ protected $state; public function __construct(WritingState $state) { $this->state = $state; } public function setState(WritingState $state) { $this->state = $state; } public function type(string $words) { $this->state->write($words); }}然后它可以用作$editor = new TextEditor(new DefaultText());$editor->type(‘First line’);$editor->setState(new UpperCase());$editor->type(‘Second line’);$editor->type(‘Third line’);$editor->setState(new LowerCase());$editor->type(‘Fourth line’);$editor->type(‘Fifth line’);// Output:// First line// SECOND LINE// THIRD LINE// fourth line// fifth line????<模板方法模式(Template Method)现实世界的例子假设我们正在建造一些房屋。构建的步骤可能看起来像准备房子的基地建造墙壁添加屋顶添加其他楼层这些步骤的顺序永远不会改变,即在建造墙壁等之前不能建造屋顶,但是每个步骤都可以修改,例如墙壁可以由木头或聚酯或石头制成。简单来说模板方法定义了如何执行某个算法的框架,但是将这些步骤的实现推迟到子类。维基百科说在软件工程中,模板方法模式是一种行为设计模式,它定义了操作中算法的程序框架,将一些步骤推迟到子类。它允许重新定义算法的某些步骤而不改变算法的结构。程序化示例想象一下,我们有一个构建工具,可以帮助我们测试,lint,构建,生成构建报告(即代码覆盖率报告,linting报告等),并在测试服务器上部署我们的应用程序。首先,我们有基类,它指定构建算法的骨架abstract class Builder{ // Template method final public function build() { $this->test(); $this->lint(); $this->assemble(); $this->deploy(); } abstract public function test(); abstract public function lint(); abstract public function assemble(); abstract public function deploy();}然后我们可以实现我们的实现class AndroidBuilder extends Builder{ public function test() { echo ‘Running android tests’; } public function lint() { echo ‘Linting the android code’; } public function assemble() { echo ‘Assembling the android build’; } public function deploy() { echo ‘Deploying android build to server’; }}class IosBuilder extends Builder{ public function test() { echo ‘Running ios tests’; } public function lint() { echo ‘Linting the ios code’; } public function assemble() { echo ‘Assembling the ios build’; } public function deploy() { echo ‘Deploying ios build to server’; }}然后它可以用作$androidBuilder = new AndroidBuilder();$androidBuilder->build();// Output:// Running android tests// Linting the android code// Assembling the android build// Deploying android build to server$iosBuilder = new IosBuilder();$iosBuilder->build();// Output:// Running ios tests// Linting the ios code// Assembling the ios build// Deploying ios build to server????总结一下那就是把它包起来。我将继续改进这一点,因此您可能希望观看/加注此存储库以重新访问。此外,我计划对架构模式进行相同的编写,请继续关注它。 ...

March 6, 2019 · 14 min · jiezi

Laravel 和 Spring Boot 两个框架比较创业篇(一:开发效率)

我个人是比较不喜欢去正儿八经的比较两个框架的,这样没有意义,不过欲善其事先利其器!技术是相通的,但是在某个特定的领域的某个阶段肯定有相对最适合的一个工具!这里比较不是从技术角度比较,而是从公司技术选型考虑的,特别是初创的互联网创业公司。没办法,谁让互联网公司离不开软件呢!哈哈哈。首先是双方选手出场介绍:LaravelLaravel框架号称是Web艺术家的框架,富有生产力,代表了最优雅最流行的PHP框架,经过一段时间的使用,也上了一个项目,感觉特点如下:比较规范(PHP的框架中),适合团队分工协作开发速度快(社区生态和脚手架加持)部署方便(PHP的部署就那样吧,Git一套推拉下来就搞定了)功能模块比较全面架构较复杂(在PHP框架中,O(∩∩)O哈哈~)全栈,前后端一个IDE搞定其他文中再说Spring BootSpring Boot准确来说并不是一个完整的框架,而是为了使 Spring 全家桶更方便使用、更亲民而产生的一个整合框架。所以Spring Boot 的背后是 Spring 近乎无敌的生态和解决方案。先简单说一下特点吧:背靠 Java 这个老家伙,还有 Spring 这个J2EE 的标准背书,生态非常强大开发速度快(在Java系列中。。。),约定大于配置基于JVM,执行效率有保障需要掌握Spring的那一套,对于本身不是 J2EE 的童鞋学习成本有点高有Cloud 加持,微服务在召唤智能到令人发指的Spring Data JPA其他稍后文中再说好啦,介绍完选手,就开始来分析一下该用哪个啦,这里我们设定一个情境:假设 小红 是一位有一个自认为价值 20亿 的Idea,并且打算付诸实践的小BOSS(即将成为),稍懂软件架构和开发技术,没错,是很菜的那种(如果很厉害那随便怎么用框架了,没所谓),且启动资金只有 30万。我也不想假设的这么惨的,现实中这种情况很多,那我们就以这种情景展开分析。小红要以最低成本、最快速度推出 1.0 版本,投放市场,收集反馈,持续迭代。这是一个系统工程,讲其他因素剔除,只考虑技术问题,可以总结成以下几点:成本(开发效率和人工成本)响应(迭代和部署效率)安全(稳定性和 BUG解决速度)协作(团队协作和扩展性)1.开发效率开发这个过程,我们将它定义为需求和原型都已经确定,并且已经简单建模完毕,嗯,就是猿们到岗后拿着需求文档打开电脑(Windows)的时候开始,到 1.0 版本发布这段时间,是谁跑得快!O(∩∩)O哈哈~首先是 Laravel 框架,步骤是这样的:配置本地环境:包括PHP-CLI、Vagrant 、VirtualBox、HomeStead Box、Composer、nodejs(Mix要用到)、Python、Virtual Studio、Node-gyp(Node-Sass要用到)、PHPStorm、Git,一切就绪后composer create-project laravel/laravel xxx开发:定义migration、model,然后transformer和repository,再写service和passport啥的,再写controller,view视图,然后完善 Event、Notification、推送啥的,期间伴随着单元测试部署:Git push、Git Clone 、Pull,env整一个,上线对 Laravel 的开发流程熟悉的人呢,开发速度是很快的。我们再来看看Spring Boot:业务不复杂就不要折腾微服务啦,不要像某人一样明明只有一台机器,硬是要开几十个端口,然后跑几十个Spring Boot的小服务,还用Cloud全家桶串起来了。我竟无言以对单体应用撸起来,步骤如下:配置开发环境:IntelliJ IDEA下一个、JDK装一个、其他要用到的Redis啥的装上,分分钟就搞定可以开撸了。开发:定义JAP Entity,Repository、Service,配置Spring Security(包括Oauth2),定义Validation,开撸Controller、异常处理,视图层啥的,单元测试也少不了部署:打出Jar包,扔到服务器上执行吧,nginx映射一下,搞定我个人觉得Spring Boot的开发效率要比 Laravel 框架高些!为什么呢? 因为如果对 Spring 的机制熟悉,也了解 Security、JPA、Thymeleaf模板、RabbitMQ 等等功能模块的使用,Spring Boot 的封装是比 Laravel 要好的,但前提是对Spring 那一套熟悉,不然从何入手都弄不清楚。Spring 有些组件是非常复杂的,例如 Spring SecurityLaravel 框架借鉴了很多 Java Spring 的思想,比如容器,依赖注入、切面,这方面明显 Spring Boot 是正宗,注解啥的6得飞起!Java 语言非常严谨,在开发过程中的体验比较好,至少像我这样天马行空的猿,还非得要 Java 这个老头来管着,不然分分钟要跑偏。回到开发效率这个问题上,如果对两个框架都比较熟悉的情况下,Spring Boot 是开发比较快的,但 Laravel在某些方面是完胜Spring Boot,如下:Laravel 框架的 ORM 构建需要经历两个步骤,migration 和 model ,而且改动 migration 需要调整 model,无法向 JPA 一样Entity 即数据库结构;Laravel 框架需要手动实现一些注入绑定,通常是$app->bind,尽管这不消耗多少时间,但是比起Spring强大的注解还是慢不少,而且主流IDE对 Spring 的 Bean 提供了导航查看功能,牛逼哄哄啊;如果要做网页渲染,Laravel的动态脚本语言特性加上Blade模板基本是秒杀Spring Boot 的;要让层次更分明一些的话,Laravel 需要手动实现Repository 模式,反正我是受不了Model 直接定义业务逻辑的,放在Controller里也受不了,不但难看,还不好扩展;在授权这方面,Laravel 自带的和Spring Security 都很强大,可以说是开箱即用,打平;Laravel框架开发反馈调试方面是完胜Spring Boot的,这方面可以说所有非编译型的语言都很爽!尽管Spring Boot 也有DevTool,但是架不住 PHP 根本就不需要重新启动呀。Laravel框架的代码提示远远比不上Spring Boot,而且还需要第三方包Ide-Helper的加持,不然代码追踪都不行,可是就算用了第三方包还是看不了 容器内长啥样啊;像 Laravel 这样靠面向对象体现优雅的框架,却遇到了PHP 这门面向对象不太完全的语言,以致于在 Java 体系内很容易实现的一个功能,到了PHP体系却无能为力;Route 路由这方面 Laravel 非常强大,而且直观,比Spring Boot 灵活,所以定义路由的时候效率完爆Spring Boot;异常处理两者都非常方便,提供了统一处理的方式,难分伯仲;Api Json数据定制这方面,Laravel 比 Spring Boot 要强大,这是因为PHP的数组操作非常灵活,对于 Java 来说需要定义工具类和实体类来专门处理;i18n国际化,Laravel 比Spring Boot 方便;前端资源处理,就这个功能本身来说,Laravel的Mix配合Blade模板完爆Spring Boot,但是话说回来,只要不是全栈,这不算什么优势。设想一下如果是前端做好页面,拿到后端套模板,那Thymeleaf 完爆 Blade,因为Thymeleaf 可以保留预览数据,渲染实际数据,Blade 做不到这一点。总结:在技能掌握充足的情况下,个人感觉 Spring Boot 开发效率要略高于Laravel。个人掌握情况不一样,请勿喷,可以参考文中的几个维度,自己思考一下。最后想提一下,顺便求证:Laravel 不念 “拉瓦” Laravel 不念 “拉瓦” Laravel 不念 “拉瓦”时候不早了,有点困。今天就写到这,明天再写人工成本的考量。大家晚安!谢谢 ...

March 6, 2019 · 1 min · jiezi

laravel-admin 文件上传阿里OSS

前言因为项目需求,需要把图片上传至阿里云 OSS,我的 Api 接口和后台项目是分开的,都使用的 laravel 框架开发,Api 接入 OSS 这里就不做讨论了,这里主要说一下 laravel-admin 上传阿里 OSS 的问题。网上的一些教程也有非常好的,但只说了使用流程,很少有说碰到的问题之类的情况,这里主要就是讲述我在 laravel-admin 接入阿里 OSS 时所遇到的一些问题,以后还有问题时,也会在这里更新。开发环境下面是我的 composer.json 内容(只列出本文需要):“require”: { “php”: “>=7.0.0”, “encore/laravel-admin”: “^1.6”, “jacobcyl/ali-oss-storage”: “^2.1”, “laravel/framework”: “5.5.*”, …}具体流程1、下载合适的第三方包在 composer.json 文件中的 require 添加 “jacobcyl/ali-oss-storage”: “^2.1”;或者直接运行 composer require jacobcyl/ali-oss-storage:^2.1 亦可。2、添加服务提供者在 config/app.php 文件下增加 Jacobcyl\AliOSS\AliOssServiceProvider::class,,如下图所示:3、在 config/filesystems.php 增加 OSS 配置信息如下:‘disks’ => [ ’local’ => [ ‘driver’ => ’local’, ‘root’ => storage_path(‘app’), ], ‘public’ => [ ‘driver’ => ’local’, ‘root’ => storage_path(‘app/public’), ‘url’ => env(‘APP_URL’).’/storage’, ‘visibility’ => ‘public’, ], ‘s3’ => [ ‘driver’ => ‘s3’, ‘key’ => env(‘AWS_ACCESS_KEY_ID’), ‘secret’ => env(‘AWS_SECRET_ACCESS_KEY’), ‘region’ => env(‘AWS_DEFAULT_REGION’), ‘bucket’ => env(‘AWS_BUCKET’), ], // 这里是新增 ‘oss’ => [ ‘driver’ => ‘oss’, ‘access_id’ => // 这里是你的 OSS 的 accessId, ‘access_key’ => // 这里是你的 OSS 的 accessKey, ‘bucket’ => // 这里是你的 OSS 自定义的存储空间名称, ’endpoint’ => ‘oss-cn-hangzhou.aliyuncs.com’, // 这里以杭州为例 ‘cdnDomain’ => ‘’, // 使用 cdn 时才需要写, https://加上 Bucket 域名 ‘ssl’ => true, // true 使用 ‘https://’ false 使用 ‘http://’. 默认 false, ‘isCName’ => false, // 是否使用自定义域名,true: Storage.url() 会使用自定义的 cdn 或域名生成文件 url,false: 使用外部节点生成url ‘debug’ => false, ], ],4、在 config/filesystems.php 更改 ‘default’ 配置信息如下:‘default’ => env(‘FILESYSTEM_DRIVER’, ‘oss’),也可以在 env 文件中定义 FILESYSTEM_DRIVER = oss 也可。5、在 config/admin.php 修改 upload 配置如下:‘upload’ => [ // Disk in config/filesystem.php. ‘disk’ => ‘oss’, // 这里就是指向 disks 下面的 oss 配置 // Image and file upload path under the disk above. ‘directory’ => [ ‘image’ => ‘images’, ‘file’ => ‘files’, ],],网上的步骤一般就是到这里了,上面的流程参考:laravel-admin 文件上传 oss;问题出现但是这时候问题就出现了, laravel-admin 本身为了开发者快速开发,本身就完成了一部分功能,当我们使用默认账号 admin 登录进去后,在后台的页面右上角和左上角都有默认的头像显示,这个默认头像是存放在本地 local 下的,在 vendor/encore/laravel-admin/resources/views/partials 下 header.blade.php 和 sidebar.blade.php 两个视图文件中显示,请看下图:header.blade.phpsidebar.blade.php而我们在 具体流程 的 5个步骤中已经把上传的配置改成了 oss 了,这时访问后台时,就会抛出一个异常:一开始我以为是 config/filesystems.php 的 default 还写成 local 会解决,但结果并没有。由于时间的原因,我还没有深入去研究,对于 laravel 框架文件上传的原理,我还是个新手,不过这里放上我的解决方法,如果有更好的解决方案,欢迎下方指正,谢谢!解决把 header.blade.php 和 sidebar.blade.php 两个视图文件中的图片的 src 改成阿里云 OSS 存放图片的路径,比如: https://xxx.oss-cn-hangzhou.aliyuncs.com/xxx/xxx/5c77a20012963.jpg ,这张图片就是你想要上传的头像图片地址。这里只是举个例子,当然这样写还是不方便,万一以后更改,还是需要找到这两个文件手动改,很麻烦,可根据自身需求进行解耦优化,这里就不做讨论了。道路阻且长,仍需不断前行!文章参考: [https://blog.csdn.net/zxdf123/article/details/82752145][6][https://blog.csdn.net/guyaofei/article/details/79918697][7] ...

March 4, 2019 · 2 min · jiezi

laravel中Dingo api如何Custom ExceptionHandler

背景在近期使用Dingo api处理接口时,发现laravel本身appExceptionsHandler中无法捕获异常。后来查阅资料发现,Dingo api接管了api请求的异常处理。导致无法自定义错误返回,很是头疼。最后在dingo的issues找到了处理方法。方法创建一个自定义异常处理 继承自Dingo\Api\Exception\Handler,重写handle方法 app/Exceptions/ApiHandler.php<?phpnamespace App\Exceptions;use Exception;use Dingo\Api\Exception\Handler as DingoHandler;class ApiHandler extends DingoHandler{ public function handle(Exception $exception) { if ($exception instanceof \Illuminate\Auth\AuthenticationException) { return response()->json([‘message’ => ‘Unauthorized’, ‘status_code’ => 401], 401); } return parent::handle($exception); }}创建一个服务容器 app/Providers/DingoServiceProvider.php<?phpnamespace App\Providers;use Dingo\Api\Provider\DingoServiceProvider as DingoServiceProviders;use App\Exceptions\ApiHandler as ExceptionHandler;class DingoServiceProvider extends DingoServiceProviders{ protected function registerExceptionHandler() { $this->app->singleton(‘api.exception’, function ($app) { return new ExceptionHandler($app[‘Illuminate\Contracts\Debug\ExceptionHandler’], $this->config(’errorFormat’), $this->config(‘debug’)); }); }}将服务容器添加到config/app.php中…‘providers’ => [… App\Providers\DingoServiceProvider::class,…];结语参考issues链接:https://github.com/dingo/api/… @shanginn 提供的方法会存在接口返回500,且没有任何数据返回。

March 4, 2019 · 1 min · jiezi

Laravel 全局异常错误处理源码解析及使用场景

如果没有全局的异常错误拦截器,那我们在每个可能发生错误异常的业务逻辑分支中,都要使用 try … catch,然后将执行结果返回 Controller层,再由其根据结果来构造相应的 Response,那代码冗余的会相当可以。全局异常错误处理,是每个框架都应该具备的,这次我们就通过简析 Laravel 的源码和执行流程,来看一下此模式是如何被运用的。源码解析laravel/laravel 脚手架中有一个预定义好的异常处理器:app/Exceptions/Handler.phpnamespace App\Exceptions;use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;class Handler extends ExceptionHandler{ // 不被处理的异常错误 protected $dontReport = []; // 认证异常时不被flashed的数据 protected $dontFlash = [ ‘password’, ‘password_confirmation’, ]; // 上报异常至错误driver,如日志文件(storage/logs/laravel.log),第三方日志存储分析平台 public function report(Exception $exception) { parent::report($exception); } // 将异常信息响应给客户端 public function render($request, Exception $exception) { return parent::render($request, $exception); }}当 Laravel 处理一次请求时,在启动文件中注册了以下服务:bootstrap/app.php// 绑定 http 服务提供者$app->singleton( Illuminate\Contracts\Http\Kernel::class, App\Http\Kernel::class);// 绑定 cli 服务提供者$app->singleton( Illuminate\Contracts\Console\Kernel::class, App\Console\Kernel::class);// 这里将异常处理器的服务提供者绑定到了 App\Exceptions\Handler::class$app->singleton( Illuminate\Contracts\Debug\ExceptionHandler::class, App\Exceptions\Handler::class);而后进入请求捕获,处理阶段:public/index.php// 使用 http 服务处理请求$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);// http 服务处理捕获的请求 $requeset$response = $kernel->handle( $request = Illuminate\Http\Request::capture());因Illuminate\Contracts\Http\Kernel::class 具体提供者是 App\Http\Kernel::class 继承至 Illuminate\Foundation\Http\Kernel::class,我们去其中看 http 服务 的 handle 方法是如何处理请求的。请求处理阶段:Illuminate\Foundation\Http\Kernel::class 的 handle 方法对请求做一次处理,如果没有异常则分发路由,如果有异常则调用 reportException 和 renderException 方法记录&渲染异常。具体处理者则是我们在 bootstrap/app.php 中注册绑定的异常处理服务 Illuminate\Contracts\Debug\ExceptionHandler::class 的 report & render,具体的服务即绑定的 App\Exceptions\Handler::class。public function handle($request){ try { // 没有异常 则进入路由分发 $request->enableHttpMethodParameterOverride(); $response = $this->sendRequestThroughRouter($request); } catch (Exception $e) { // 捕获异常 则 report & render $this->reportException($e); $response = $this->renderException($request, $e); } catch (Throwable $e) { $this->reportException($e = new FatalThrowableError($e)); $response = $this->renderException($request, $e); } $this->app[’events’]->dispatch( new Events\RequestHandled($request, $response) ); return $response;}//Report the exception to the exception handler.protected function reportException(Exception $e){ // 服务Illuminate\Contracts\Debug\ExceptionHandler::class 的 report 方法 $this->app[ExceptionHandler::class]->report($e);}//Render the exception to a response.protected function renderException($request, Exception $e){ // 服务Illuminate\Contracts\Debug\ExceptionHandler::class 的 render 方法 return $this->app[ExceptionHandler::class]->render($request, $e);}handler 方法作为请求处理的入口,后续的路由分发,用户业务调用(controller, model)等执行的上下文依然在此方法中,故异常也能在这一层被捕获。然后我们就可以在业务中通过 throw new CustomException($code, “错误异常描述”); 的方式将控制流程转交给全局异常处理器,由其解析异常并构建响应实体给客户端,这一模式在 Api服务 的开发中是效率极高的。laravel 的依赖中有 symfony 这个超级棒的组件库,symfony 为我们提供了详细的 Http 异常库,我们可以直接借用这些异常类(当然也可以自定义)laravel 有提供 abort 助手函数来实现创建一个异常错误,但主要面向 web 网站(因为laravel主要就是用来开发后台的嘛)的,对 Api 不太友好,而且看源码发现只顾及了 404 这货。/** * abort(401, “你需要登录”) * abort(403, “你登录了也白搭”) * abort(404, “页面找不到了”) * Throw an HttpException with the given data. * * @param int $code * @param string $message * @param array $headers * @return void * * @throws \Symfony\Component\HttpKernel\Exception\HttpException */public function abort($code, $message = ‘’, array $headers = []){ if ($code == 404) { throw new NotFoundHttpException($message); } throw new HttpException($code, $message, null, $headers);}即只有 404 用了具体的异常类去抛出,其他的状态码都一股脑的归为 HttpException,这样就不太方便我们在全局异常处理器的 render 中根据 Exception 的具体类型来分而治之了,但 abort 也的确是为了方便你调用具体的错误页面的 resources/views/errors/{statusCode.blade.php} 的,需要对 Api 友好自己改写吧。使用场景// 业务代码 不满足直接抛出异常即可if ("" = trim($username)) { throw new BadRequestHttpException(“用户名必须”);}// 全局处理器public function render($request, Exception $exception){ if ($exception instanceof BadRequestHttpException) { return response()->json([ “err” => 400, “msg” => $exception->getMessage() ]); } if ($exception instanceof AccessDeniedHttpException) { return response()->json([ “err” => 403, “msg” => “unauthorized” ]); } if ($exception instanceof NotFoundHttpException) { return response()->json([ “err” => 403, “msg” => “forbidden” ]); } if ($exception instanceof NotFoundHttpException) { return response()->json([ “err” => 404, “msg” => “not found” ]); } if ($exception instanceof MethodNotAllowedHttpException) { return response()->json([ “err” => 405, “msg” => “method not allowed” ]); } if ($exception instanceof MethodNotAllowedHttpException) { return response()->json([ “err” => 406, “msg” => “你想要的数据类型我特么给不了啊” ]); } if ($exception instanceof TooManyRequestsHttpException) { return response()->json([ “err” => 429, “msg” => “to many request” ]); } return parent::render($request, $exception);} ...

March 4, 2019 · 2 min · jiezi

Laravel Excel 的五个隐藏功能

Laravel Excel package 最近发布了 3.0 版本,它所具有的新功能,可以帮助简化高级需求,并且可用性极高。大家一起来探讨一下可能不知道的一些隐藏功能,这些功能使 Laravel Excel 成为 Excel 拓展的最佳首选。1. 从 HTML 或者是 Blade 导入数据假设已经有一个 HTML 表格模版代码 – resources/views/customers/table.blade.php:<table class=“table”> <thead> <tr> <th></th> <th>First name</th> <th>Last name</th> <th>Email</th> <th>Created at</th> <th>Updated at</th> </tr> </thead> <tbody> @foreach ($customers as $customer) <tr> <td>{{ $customer->id }}</td> <td>{{ $customer->first_name }}</td> <td>{{ $customer->last_name }}</td> <td>{{ $customer->email }}</td> <td>{{ $customer->created_at }}</td> <td>{{ $customer->updated_at }}</td> </tr> @endforeach </tbody></table>你可以使用它去重复导入这个表格到 Excel步骤1. 生成一个 Export 类php artisan make:export CustomersFromView –model=Customer步骤2. 使用 FromView 进行操作namespace App\Exports;use App\Customer;use Illuminate\Contracts\View\View;use Maatwebsite\Excel\Concerns\FromView;class CustomersExportView implements FromView{ public function view(): View { return view(‘customers.table’, [ ‘customers’ => Customer::orderBy(‘id’, ‘desc’)->take(100)->get() ]); }}这里是导入的 Excel 文件:注意:这里只能导出 HTML 表格,不能具有任何标签,比如 html,body,div 等。2. 导出到 PDF,HTML,或是其他格式的文件虽然包的名称是 Laravel Excel,但是提供了多种导出格式,并且使用起来十分简单,只要在类里再添加一个参数即可:return Excel::download(new CustomersExport(), ‘customers.xlsx’, ‘Html’);比如这么做,就导出到了HTML,如下图所示:没有太多的样式,下面是源代码:不仅如此,它还可以导出到 PDF,甚至你可以从中选择三种库,使用方法是一样的,你只要在最后一个参数指定格式就好了,下面是一些例子。 文档示例:注意:你必须通过 composer 安装指定的 PDF 包,比如:composer require dompdf/dompdf导出的 PDF 如下所示:3. 按需格式化单元格Laravel Excel 有一个强有力的「爸爸」 – PhpSpreadsheet。因此它就拥有其各种底层功能,包括各种方式的单元格格式化。此处是一个如何在 Laravel Export 类中使用它的例子,例如 app/Exports/CustomersExportStyling.php:步骤 1. 在头部引入适当的类。use Maatwebsite\Excel\Concerns\WithEvents;use Maatwebsite\Excel\Events\AfterSheet;步骤 2. 在 implements 部分使用 WithEvents 接口。class CustomersExportStyling implements FromCollection, WithEvents{ // …步骤 3. 用 AfterSheet 事件来创建 registerEvents() 方法。/ * @return array /public function registerEvents(): array{ return [ AfterSheet::class => function(AfterSheet $event) { // … 此处你可以任意格式化 }, ];}这里有个例子:/* * @return array /public function registerEvents(): array{ return [ AfterSheet::class => function(AfterSheet $event) { // 所有表头-设置字体为14 $cellRange = ‘A1:W1’; $event->sheet->getDelegate()->getStyle($cellRange)->getFont()->setSize(14); // 将样式数组应用于B2:G8范围单元格 $styleArray = [ ‘borders’ => [ ‘outline’ => [ ‘borderStyle’ => \PhpOffice\PhpSpreadsheet\Style\Border::BORDER_THICK, ‘color’ => [‘argb’ => ‘FFFF0000’], ] ] ]; $event->sheet->getDelegate()->getStyle(‘B2:G8’)->applyFromArray($styleArray); // 将第一行行高设置为20 $event->sheet->getDelegate()->getRowDimension(1)->setRowHeight(20); // 设置 A1:D4 范围内文本自动换行 $event->sheet->getDelegate()->getStyle(‘A1:D4’) ->getAlignment()->setWrapText(true); }, ];}这些「随机」样例展示的结果如下所示:你可以在 Recipes page of PhpSpreadsheet docs中找到所有的以上以及更多示例。4. 隐藏模型属性假设我们已经创建了Laravel 5.7默认的users表:现在我们尝试用简单的FromCollection来导出用户表数据:class UsersExport implements FromCollection{ public function collection() { return User::all(); }}在导出的Excel 里,你只能看到如下字段,但是没有password和remember_token:这是因为在User模型里定义了隐藏字段的属性:class User extends Authenticatable{ // … / * 这个数组用来定义需要隐藏的字段。 * * @var array / protected $hidden = [ ‘password’, ‘remember_token’, ];}所以,默认情况下这些字段是隐藏的,如果你想在导出数据的时候某些字段不被导出的话,可以直接在模型中定义隐藏属性$hidden。5. 公式出于某种原因,Laravel Excel 包的官方文档中并没有提及公式,但是这是Excel 重要的功能!幸运的是,我们可以直接将公式写在导出数据的类中,我们需要设置cell 的值,就像这样:=A2+1 or SUM(A1:A10)。其中一种方式就是实现WithMapping 接口:use App\Customer;use Maatwebsite\Excel\Concerns\FromCollection;use Maatwebsite\Excel\Concerns\WithMapping;class CustomersExportFormulas implements FromCollection, WithMapping{ public function collection() { return Customer::all(); } / * @var Customer $customer * @return array */ public function map($customer): array { return [ $customer->id, ‘=A2+1’, $customer->first_name, $customer->last_name, $customer->email, ]; }}以上就是Laravel Excel的五个鲜为人知的功能。文章转自: https://learnku.com/laravel/t… 更多文章:https://learnku.com/laravel/c… ...

March 4, 2019 · 2 min · jiezi

用Docker搭建Laravel和Vue项目的开发环境

在这篇文章中我们将通过Docker在个人本地电脑上构建一个快速、轻量级、不依赖本地电脑所安装的任何开发套件的可复制的Laravel和Vue项目的开发环境(开发环境的所有依赖都安装在Docker构建容器里),加入Vue只是因为有的项目里会在Laravel项目中使用Vue做前后端分离开发,开发环境中需要安装前端开发需要的工具集,当然前后端也可以分成两个项目开发,这个话题不在本篇文章的讨论范围内。所以我们的目标是:不在本地安装Mamp/Wamp这样的软件不使用类似Vagrant这样的虚拟机不在本地电脑全局安装PHP开发所需要的工具集不在本地电脑全局安装前端开发所需要的工具集不在本地电脑全局安装Mysql和Nginx开始前你需要先去安装一个Docker客户端,Docker的官网中有详细的安装方法。第一步:获取Laravel的源码包因为我们电脑上不安装Composer,所以就不能使用Composer来创建Laravel项目了, 这里我使用cURL直接从github上下载了最新的Laravel源码包,你也可以使用wget或者git clone 来获取源码包。curl -L -O https://github.com/laravel/laravel/archive/v5.5.0.tar.gz /&& tar -zxvf v5.5.0.tar.gz /&& rm v5.5.0.tar.gz上面的命令在curl下载完源码包后会解压源码压缩包,解压完成后在把源码压缩包v5.5.0.tar.gz删掉,执行完后你会看到一个laravel-5.5.0的项目目录。第二步:添加docker-compose.yml在项目中创建docker-compose.yml文件。Compose 项目是 Docker 官方的开源项目,负责实现对 Docker 容器集群的快速编排。我们知道使用一个 Dockerfile 模板文件,可以让用户很方便的定义一个单独的应用容器。在这里我们会用到四个容器分别将PHP、Mysql、Nginx放在四个不同的容器中,通过compose`将四个应用容器关联到一起组成项目。编排文件的开头如下:version: ‘2’services: # our services will go here在编排文件中,把每个容器叫做一个服务,services下定义整个应用中用到的所有服务(即容器)。App服务APP服务的容器将执行我们项目中的代码。app: build: context: ./ dockerfile: app.dockerfile working_dir: /var/www volumes: - ./:/var/www environment: - “DB_PORT=3306” - “DB_HOST=database"Notes:我们使用app.dockerfile这个镜像文件来构建我们的App容器,在镜像文件中我们会对项目中用到的PHP模块镜像配置,也会额外安装NPM用来构建前端代码。working_dir: /var/www把工作目录设置成了/var/www,在容器中项目代码将会被放在/var/www目录下面,包括使用docker exec app执行的命令也都是以/var/www为当前工作目录的。volumes是容器内数据卷所挂载路径设置,在这里我们只定义一个数据卷,把宿主机项目目录挂到在容器中的/var/www上,这样我们在本地电脑对项目代码进行的更改就会马上同步到容器中去,反过来也是一样,容器中对代码做的更改也会及时反馈到本地电脑的项目中。environment设置环境变量名,这里我们设置了DB_PORT和DB_HOST 这样就不用修改项目中的.env文件里关于这两项的值了,当然任何你需要在开发环境单独设置的环境变量都可以写到这里,Laravel读取配置使用的DotEnv会检测是否系统有指定环境变量的设置,有的话就不会在去读取.env文件了。现在我们需要创建上面build环节中提到的app.dockerfile这个文件了,具体内容如下:FROM php:7.1.22-fpm# Update packagesRUN apt-get update# Install PHP and composer dependenciesRUN apt-get install -qq git curl libmcrypt-dev libjpeg-dev libpng-dev libfreetype6-dev libbz2-dev# Clear out the local repository of retrieved package files# RUN apt-get clean# Install needed extensions# Here you can install any other extension that you need during the test and deployment processRUN apt-get clean; docker-php-ext-install pdo pdo_mysql mcrypt zip gd pcntl opcache bcmath# Installs Composer to easily manage your PHP dependencies.RUN curl –silent –show-error https://getcomposer.org/installer | php – –install-dir=/usr/local/bin –filename=composer# Install NodeRUN apt-get update &&\ apt-get install -y –no-install-recommends gnupg &&\ curl -sL https://deb.nodesource.com/setup_10.x | bash - &&\ apt-get update &&\ apt-get install -y –no-install-recommends nodejs &&\ npm config set registry https://registry.npm.taobao.org –global &&\ npm install –global gulp-cliCMD php-fpmNotes:我在这里先将NPM和Composer装到了app容器中,因为在开发时经常需要执行他们,如果发布到生产环境,一般是使用单独的composer对项目代码进行构建而不是放在运行应用的容器里,容器的核心思想之一就是保持单一,这样才能做到快速增加相同角色的容器。Web服务接下来,我们需要配置一个Web服务器用,我们把这个容器在编排文件中命名成webweb: build: context: ./ dockerfile: web.dockerfile working_dir: /var/www volumes_from: - app ports: - 8080:80Notes:volumes_from用来复用在app服务中定义的数据卷路径通过ports将本地电脑的8080端口映射到web容器的80端口,这样在开发环境中我们就不用设置hosts文件,直接通过IP加端口就能访问服务了。Web服务器选用nginx,所以我们需要用一个nginx镜像文件来构建这个容器,在这之前我们需要在nginx镜像的基础上再设置一下项目中用到的vhost,所以我们需要一个web.dockerfile文件,它的定义如下:FROM nginx:1.10ADD vhost.conf /etc/nginx/conf.d/default.conf根据镜像文件的定义,我们把项目中的vhost.conf复制到了容器的/etc/nginx/conf.d/default.conf中,这样基本的nginx配置就配置好了,vhost.conf中的定义如下:server { listen 80; index index.php index.html; root /var/www/public; location / { try_files $uri /index.php?$args; } location ~ .php$ { fastcgi_split_path_info ^(.+.php)(/.+)$; fastcgi_pass app:9000; fastcgi_index index.php; include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_param PATH_INFO $fastcgi_path_info; }}Notes:因为是开发环境我们就只进行最简单的配置,不做调优考虑了。fastcgi_pass app:9000; nginx将对PHP的请求通过fastcgi传递给了app服务的9000端口,docker-compose会自动把services中定义的容器服务连接起来,各个服务相互之间使用服务名称引用。Mysql服务接下来我们将配置Mysql服务,与上面两个服务有点不一样的是,在PHP-FPM和Nginx的容器中,我们配置本地电脑的文件可以同步到容器中供容器访问,这让我们开发时对文件作的更改能够快速的在容器中得到反馈加快我们的开发过程。但是在数据库容器中我们希望容器中创建的文件能够持久化(默认容器销毁时,容器内创建的文件也会被销毁),我们可以通过Docker的数据卷来实现上述功能,只不过这次不用再把本地电脑的文件挂在到数据卷上了,Docker客户端会管理创建的数据卷的在本地电脑上具体存储的位置。下面是编排文件中对database服务的设置version: ‘2’services: database: image: mysql:5.7 volumes: - dbdata:/var/lib/mysql environment: - “MYSQL_DATABASE=homestead” - “MYSQL_USER=homestead” - “MYSQL_PASSWORD=secret” - “MYSQL_ROOT_PASSWORD=secret” ports: - “33061:3306"volumes: dbdata:Notes:在文件的最下面我们通过volumes命令创建了一个名为dbdata的数据卷(dbdata后面的冒号是有意写上去的,这是YML文件的一个语法限制,不用太关心)定义完数据卷后,在上面我们使用<name>:<dir>的格式,通知Docker,将dbdata数据卷挂在到容器中的/var/lib/mysql目录上environments中设置的是Mysql的docker镜像需要的四个必要参数。ports端口映射中,我们将本地电脑的33061端口映射到容器的3306端口,这样我们就能通过电脑上的数据库工具连接到docker内的Mysql了。将所有服务编排到一起下面是完整的docker-compose.yml文件,通过编排文件我们将三个应用容器关联在一起组成了项目的服务端version: ‘2’services: # The Application app: build: context: ./ dockerfile: app.dockerfile working_dir: /var/www volumes: - ./:/var/www environment: - “DB_PORT=3306” - “DB_HOST=database” # The Web Server web: build: context: ./ dockerfile: web.dockerfile working_dir: /var/www volumes_from: - app ports: - 8080:80 # The Database database: image: mysql:5.6 volumes: - dbdata:/var/lib/mysql environment: - “MYSQL_DATABASE=homestead” - “MYSQL_USER=homestead” - “MYSQL_PASSWORD=secret” - “MYSQL_ROOT_PASSWORD=secret” ports: - “33061:3306"volumes: dbdata:启动服务按照上面的步骤配置好编排文件还有指定的docker镜像文件后,我们就可以通过下面的命令启动服务了,执行完后会启动上面文件里定义的三个服务。docker-compose up -d 第一次启动时,由于docker客户端要下载上面提到的三个镜像并且构建服务所以启动速度会慢一些,等到下载完镜像并构建完成后,以后的启动都会非常快。初始化Laravel项目启动完服务后我们可以初始化Laravel项目了,步骤跟官方文档里介绍的一样,但是需要在启动的app服务的容器里执行:docker-compose exec app composer installdocker-compose exec app npm install // 如果包含前端项目的话再执行相关命令docker-compose exec app cp .env.example .envdocker-compose exec app php artisan key:generatedocker-compose exec app php artisan optimizedocker-compose exec app php artisan migrate –seeddocker-compose exec app php artisan make:controller MyControllerNotes:docker-compose exec 将命令发送到指定的容器中去执行app是定义在docker-compose.yml中的一个服务,它是一个运行着php-fpm的容器php artisan migrate 是要在容器里执行的命令查看nginx日志的方法:docker ps 找到nginx服务的container iddocker exec -it <contianer id> /bin/bash 进入nginx容器nginx日志的具体路径请查看项目中的vhost.conf执行完上面的命令后你就能通过http://127.0.0.1:8080/访问到项目啦。在我的Github gist有一组参考文件方便同学们参考https://gist.github.com/kevin…gist里的文件稍微旧一些,后来在使用的过程中又加入些新的PHP模块和Node,之前composer也单独放到了一个容器中,不过相信聪明的你看到这里应该已经会根据需求更改这些文件啦。 ...

March 4, 2019 · 2 min · jiezi

Laravel 配置双模板

在开发过程中, 不时会遇到某些项目需要使用两套模板如PC端和Mobile端使用不同的模板文件, 以达到最佳的用户体验遇到这种情况我们应该如何配置Laravel的模板文件呢?1. 安装whichbrowser/parser 传送门: WhichBrowser/Parser-PHP用于判断PC或Mobile设备, 按需加载不同的模板composer require whichbrowser/parser2. 使用artisan命令新建一个Middleware(中间件)执行后会在app/Http/Middleware目录生成中间件文件php artisan make:middleware Template3. 编辑Template.php文件class Template{ protected $except = []; public function handle($request, Closure $next) { $result = new WhichBrowser\Parser(getallheaders()); // 如果是桌面类型, 返回true $isDesktop = $result->isType(‘desktop’); if ($isDesktop) { // 加载pc端的模板文件 $path = resource_path(‘views/pc/’); } else { // 加载mobile端的模板文件 $path = resource_path(‘views/mobile/’); } // 获取视图查找器实例 $view = app(‘view’)->getFinder(); // 重新定义视图目录 $view->prependLocation($path); // 返回请求 return $next($request); }}4. 最后注册中间件在app/Http/Kernel.php类中 按需注册中间件如注册全局中间件:protected $middleware = [ \App\Http\Middleware\Template::class,];搞定, 就可以根据不同的设备加载不同的模板文件了在控制中只需这样, 就可以根据不同的设备来加载不同的模板了return view(‘registration.index’, $data);如从PC设备打开网页: 加载 /resources/views/pc/registration/index.blade.php 模板如从移动设备打开网页: 加载 /resources/views/mobile/registration/index.blade.php 模板原文: Laravel 配置双模板 ...

March 1, 2019 · 1 min · jiezi

Laravel 测试: PHPUnit 入门教程

介绍 PHPUnit 测试的基础知识,使用基本的 PHPUnit 断言和 Laravel 测试助手。介绍PHPUnit 是最古老和最著名的 PHP 单元测试包之一。它主要用于单元测试,这意味着可以用尽可能小的组件测试代码,但是它也非常灵活,可以用于很多不仅仅是单元测试。PHPUnit 包含许多简单和灵活的断言允许您轻松地测试代码,当您测试特定的组件时,这些断言非常有效。但是,它确实意味着测试更高级的代码(如控制器和表单提交验证)可能会复杂得多。为了帮助开发人员更容易地进行开发, Laravel 框架 包含了一系列 应用程序测试帮助程序 ,允许您编写非常简单的 PHPUnit 测试来测试应用程序的复杂部分。本教程的目的是向您介绍 PHPUnit 测试的基础知识,使用默认 PHPUnit 断言和 Laravel 测试助手。这样做的目的是在本教程结束时,您可以自信地为应用程序编写基本测试。前提本教程假设您已经熟悉 Laravel 并知道如何在应用程序目录中运行命令(例如 php artisan 命令)。我们将创建几个基本的示例类来学习不同的测试工具如何工作,因此建议您为本教程创建一个新的应用程序。如果已经安装了 Laravel ,则可以通过运行以下命令创建新的测试应用程序:laravel new phpunit-tests或者,您可以直接使用 Composer 创建新应用程序:composer create-project laravel/laravel –prefer-dist其他安装方法也可以在 Laravel 文档中找到。创建一个新的测试使用 PHPUnit 的第一步是创建一个新的测试类。测试类的约定是它们存储在应用程序目录的 ./tests/ 下。在这个文件夹中,每个测试类都被命名为 <name>Test.php 。这种格式允许 PHPUnit 查找每个测试类—它将忽略任何不以 Test.php 结尾的文件。在新的 Laravel 应用程序中,你会注意到 ./tests/ 目录中有两个文件: ExampleTest.php 和 TestCase.php. TestCase.php 文件是一个引导文件用于在我们的测试中设置 Laravel 环境。这允许我们在测试中使用 Laravel Facades 并为测试助手提供框架,我们将在稍后介绍。 ExampleTest.php 是一个示例测试类,其中包含使用应用程序测试助手的基本测试用例-暂时忽略它。要创建一个新的测试类,我们可以手动创建一个新文件,或者运行由 Laravel 提供的 Artisan 命令 make:test 为了创建一个名为 BasicTest 的测试类,我们只需要运行这个 artisan 命令:php artisan make:test BasicTestLaravel 将创建一个如下所示的基本测试类:<?phpclass BasicTest extends TestCase{ /** * 一个基本的测试示例。 * * @return void / public function testExample() { $this->assertTrue(true); }}这里要注意的最重要的事情是 test 方法名称上的前缀,与 Test 类名后缀一样,这样 test 前缀告诉 PHPUnit 在测试时运行哪些方法。如果您忘记了 test 前缀,那么 PHPUnit 将忽略该方法。在我们第一次运行测试套件之前,有必要指出 Laravel 提供的默认 phpunit.xml 文件。 PHPUnit 在运行时会自动在当前目录中查找名为 phpunit.xml 或者 phpunit.xml.dist 的文件。您可以在此处配置测试的特定选项。这个文件中有很多信息,但是现在最重要的部分是在 testsuite 目录定义:<?xml version=“1.0” encoding=“UTF-8”?><phpunit … > <testsuites> <testsuite name=“Application Test Suite”> <directory>./tests/</directory> </testsuite> </testsuites> …</phpunit>这将告诉 PHPUnit 运行时在 ./tests/ 目录中找到的测试,正如我们之前所知,这是存储测试的约定。现在我们已经创建了一个基本测试,并且知道了 PHPUnit 配置,现在是第一次运行测试的时候了。您可以通过运行以下 phpunit 命令来运行测试:./vendor/bin/phpunit您应该看到与此类似的输出:PHPUnit 4.8.19 by Sebastian Bergmann and contributors…Time: 103 ms, Memory: 12.75MbOK (2 tests, 3 assertions)现在我们已经有了一个有效的 PHPUnit 设置,现在是时候开始编写一个基本测试了。注意,它会统计2个测试和3个断言,因为 ExampleTest.php 文件包含了一个带有两个断言的测试。我们的新基本测试包括一个单独的断言,该断言已通过。写一个基础测试为了帮助 PHPUnit 提供的基本断言,我们将首先创建一个提供一些简单功能的基本类在 ./app/ 目录中创建一个名为 Box.php 的新文件,并复制此示例类:<?phpnamespace App;class Box{ /* * @var array / protected $items = []; /* * 使用给定项构造框 * * @param array $items / public function __construct($items = []) { $this->items = $items; } /* * 检查指定的项目是否在框中。 * * @param string $item * @return bool / public function has($item) { return in_array($item, $this->items); } /* * 从框中移除项,如果框为空,则为 null 。 * * @return string / public function takeOne() { return array_shift($this->items); } /* * 从包含指定字母开头的框中检索所有项目。 * * @param string $letter * @return array / public function startsWith($letter) { return array_filter($this->items, function ($item) use ($letter) { return stripos($item, $letter) === 0; }); }}接下来, 打开你的 ./tests/BasicTest.php 类(我们之前创建的类),并删除默认创建的 testExample 方法, 你应该留一个空类。我们现在将使用七个基本的 PHPUnit 断言来为我们的 Box 类编写测试。这些断言是:assertTrue()assertFalse()assertEquals()assertNull()assertContains()assertCount()assertEmpty()assertTrue() 和 assertFalse()assertTrue() 和 assertFalse() 允许你声明一个值等于 true 或 false 。这意味着它们非常适合测试返回布尔值的方法。在我们的 Box 类中,我们有一个名为 has($item) 的方法,当指定的项在 box 中或不在 box 中时,该方法返回对应返回 true 或 false .要在 PHPUnit 中为此编写测试,我们可以执行以下操作:<?phpuse App\Box;class BasicTest extends TestCase{ public function testHasItemInBox() { $box = new Box([‘cat’, ’toy’, ’torch’]); $this->assertTrue($box->has(’toy’)); $this->assertFalse($box->has(‘ball’)); }}注意我们如何只将一个参数传递给 assertTrue() 和 assertFalse() 方法,并且它是 has($item) 方法的输入.如果您现在运行 ./vendor/bin/phpunit 命令,您会注意到输出包括:OK (2 tests, 4 assertions)这意味着我们的测试已经通过。如果您将 assertFalse() 替换成 assertTrue() 并运行 phpunit 命令,输出将如下所示:PHPUnit 4.8.19 by Sebastian Bergmann and contributors.F.Time: 93 ms, Memory: 13.00MbThere was 1 failure:1) BasicTest::testHasItemInBoxFailed asserting that false is true../tests/BasicTest.php:12FAILURES!Tests: 2, Assertions: 4, Failures: 1.这告诉我们第12行的断言未能断言 false 值是 true - 因为我们将 assertFalse() 替换为 assertTrue() 。将其交换回来,然后重新运行 PHPUnit 。测试应该再次通过,因为我们已经修复了破损的测试。assertEquals() 与 assertNull()接下来,让我们看看 assertEquals(), 以及 assertNull()。assertEquals() 用于比较变量实际值与预期值是否相等。我们用它来检查 takeOne() 方法的返回值是否为 Box 内的当前值。当 Box 为空时,takeOne() 将返回 null,我们亦可使用 assertNull() 来进行检查。与 assertTrue()、assertFalse() 以及 assertNull() 不同,assertEquals() 需要两个参数。第一个参数为 预期 值,第二个参数则为 实际 值。可参照如下代码实现以上断言(assertions):<?phpuse App\Box;class BasicTest extends TestCase{ public function testHasItemInBox() { $box = new Box([‘cat’, ’toy’, ’torch’]); $this->assertTrue($box->has(’toy’)); $this->assertFalse($box->has(‘ball’)); } public function testTakeOneFromTheBox() { $box = new Box([’torch’]); $this->assertEquals(’torch’, $box->takeOne()); // 当前 Box 为空,应当为 Null $this->assertNull($box->takeOne()); }}运行 phpunit 命令,你应当看到如下输出:OK (3 tests, 6 assertions)assertContains() 和 assertCount() 以及 assertEmpty()终于,我们有三个作用于数组有关的断言,我们能够使用它们去检查 Box 类中的 startsWith($item) 方法。 assertContains() 断言传递进来的数组中包含指定值, assertCount() 断言数组的项数为指定数量,assertEmpty() 断言传递进来的数组为空。让我们来执行以下测试:<?phpuse App\Box;class BasicTest extends TestCase{ public function testHasItemInBox() { $box = new Box([‘cat’, ’toy’, ’torch’]); $this->assertTrue($box->has(’toy’)); $this->assertFalse($box->has(‘ball’)); } public function testTakeOneFromTheBox() { $box = new Box([’torch’]); $this->assertEquals(’torch’, $box->takeOne()); // Null,现在这个 box 是空的。 $this->assertNull($box->takeOne()); } public function testStartsWithALetter() { $box = new Box([’toy’, ’torch’, ‘ball’, ‘cat’, ’tissue’]); $results = $box->startsWith(’t’); $this->assertCount(3, $results); $this->assertContains(’toy’, $results); $this->assertContains(’torch’, $results); $this->assertContains(’tissue’, $results); // 如果传递复数断言数组为空 $this->assertEmpty($box->startsWith(’s’)); }}保存并再一次运行你的测试:OK (4 tests, 9 assertions)恭喜你,你刚刚使用七个基础的 PHPUnit 断言完成了对 Box 类的全部测试。通过这些简单的断言你能够做许多事,对于其他断言,大多数要更复杂,不过它们仍遵循以上使用规则。测试你的程序在你的程序里,对每个组件进行单元测试在很多情况下都是有必要的,而且也应该成为你开发过程中必不可少的一部分,但这并不是你需要做的全部的测试。当你构建一个包含复杂视图、导航和表单的程序时,你同样想测试这些组件。这时,Laravel的测试助手可以使这些测试像单元测试简单组件一样容易。我们之前查看在 ./tests/ 目录下的默认文件时跳过了 ./tests/ExampleTest.php 文件。 现在打开它,内容如下所示:<?phpclass ExampleTest extends TestCase{ /** 一个基本功能测试示例。** @return void*/ public function testBasicExample() { $this->visit(’/’) ->see(‘Laravel 5’); }}我们可以看到这个测试示例非常简单。在不知道测试助手如何运作的情况下,我们可以猜测它的意思如下:当我访问/ (根目录)我应该看到 ‘Laravel 5’如果你打开你的web浏览器,访问我们的程序(如果你没有启动你的web服务器,你可以运行 php artisan serve ),你应该可以在web根目录上看到屏幕上有“Laravel 5”的文本。 鉴于这个测试已经通过了PHPUnit,我们可以很确定地说我们对这个测试示例改造是正确的。这个测试确保了访问/路径,网页可以返回“‘Laravel 5”的文本。一个如此简单的检查也许不代表什么,但如果你的网站上要显示关键信息,它就可以在一个别处的改动导致这个页面无法正常显示正确的信息时,防止你部署一个被损坏的程序。visit()、see() 以及 dontSee()现在尝试编写自己的测试,更进一步理解它吧。首先,编辑 ./app/Http/routes.php ,增加一个新的路由。为了教程目的,我们创建希腊字母定义的路由:<?phpRoute::get(’/’,function () { return view(‘welcome’);});Route::get(’/alpha’,function () { return view(‘alpha’);});然后,创建视图文件 ./resources/views/alpha.blade.php,使用 Alpha 作为关键字,保存基本的HTML文件:<!DOCTYPE html><html> <head> <title>Alpha</title> </head> <body> <p>This is the Alpha page.</p> </body></html>打开浏览器,输入网址: http://localhost:8000/beta,页面会显示出 “This is the Alpha page.” 的内容。现在我们有了测试用到的模版文件,下一步,我们通过运行命令 make:test 来创建一个新的测试文件:php artisan make:test AlphaTest然后变成刚创建好的测试文件,按照框架提供的例子,测试 “alpha” 页面上没有包含 “beta” 。 我们可以使用方法 dontSee() ,它是 see() 的对应的反向方法。下面代码是上面实现的简单例子:<?phpclass AlphaTest extends TestCase{ public function testDisplaysAlpha() { $this->visit(’/alpha’) ->see(‘Alpha’) ->dontSee(‘Beta’); }}保存并运行 PHPUnit (./vendor/bin/phpunit),测试代码应该会全部通过,你会看到像这样的测试状态内容显示:OK (5 tests,12 assertions)开发前先写测试对于测试来说,测试驱动开发 (TDD) 是非常酷的方法,首先我们先写测试。写完测试并执行它们,你会发现测试没通过,接下来 我们编写满足测试的代码,再次执行测试,使测试通过。 接下来让我们开始。首先,建立一个 BetaTest 类使用 make:test artisan 命令:php artisan make:test BetaTest接下来,更新测试用例以便检查 /beta 的路由 route 为「Beta」:<?phpclass BetaTest extends TestCase{ public function testDisplaysBeta() { $this->visit(’/beta’) ->see(‘Beta’) ->dontSee(‘Alpha’); }}现在使用 ./vendor/bin/phpunit 命令来执行测试。结果是一个看起来简洁但不好的错误信息,如下:> ./vendor/bin/phpunitPHPUnit 4.8.19 by Sebastian Bergmann and contributors…..F.Time: 144 ms, Memory: 14.25MbThere was 1 failure:1) BetaTest::testDisplaysBeta一个对 [http://localhost/beta] 的请求失败了。收到状态码 [404]。…FAILURES!Tests: 6, Assertions: 13, Failures: 1.我们现在需要创建这个不存在的路由。让我们开始。首先,编辑 ./app/Http/routes.php 文件来创建新的 /beta 路由:<?phpRoute::get(’/’, function () { return view(‘welcome’);});Route::get(’/alpha’, function () { return view(‘alpha’);});Route::get(’/beta’, function () { return view(‘beta’);});接下来,在 ./resources/views/beta.blade.php 下创建如下视图模版:<!DOCTYPE html><html> <head> <title>Beta</title> </head> <body> <p>This is the Beta page.</p> </body></html>现在再一次执行 PHPUnit,结果应该再一次回到绿色。> ./vendor/bin/phpunitPHPUnit 4.8.19 by Sebastian Bergmann and contributors…….Time: 142 ms, Memory: 14.00MbOK (6 tests, 15 assertions)这样我们就通过在完成新的页面之前写测试的方式,对 测试驱动开发 进行了实践。click() 和 seePageIs()Laravel 也提供一个辅助函数 (click()) 允许测试点击页面中存在的连接 ,以及一个方法 (seePageIs()) 检查点击展示的结果页面。让我们使用这两个辅助函数去执行在 Alpha 和 Beta 页面的链接。首先,我们更新我们的测试。打开 AlphaTest 类,我们将添加一个新的测试方法,这将点击 「alpha」页面上的「Next」链接跳转到 「beta」页面。新的测试代码如下:<?phpclass AlphaTest extends TestCase{ public function testDisplaysAlpha() { $this->visit(’/alpha’) ->see(‘Alpha’) ->dontSee(‘Beta’); } public function testClickNextForBeta() { $this->visit(’/alpha’) ->click(‘Next’) ->seePageIs(’/beta’); }}注意到,在我们新建的 testClickNextForBeta() 方法中,我们并没有检查每一个页面的内容。 其他测试都成功的检查了两个页面的内容,所以这里我们只关心点击 「Next」链接将发送到 /beta。你现在可以运行测试组件了,但就像预料的一样测试将不通过,因为我们还没有更新我们的 HTML。接下来,我们将更新 BetaTest 来做类似的事情:<?phpclass BetaTest extends TestCase{ public function testDisplaysBeta() { $this->visit(’/beta’) ->see(‘Beta’) ->dontSee(‘Alpha’); } public function testClickNextForAlpha() { $this->visit(’/beta’) ->click(‘Previous’) ->seePageIs(’/alpha’); }}接下来,我们更新我们的 HTML 模版。./resources/views/alpha.blade.php:<!DOCTYPE html><html> <head> <title>Alpha</title> </head> <body> <p>This is the Alpha page.</p> <p><a href="/beta">Next</a></p> </body></html>./resources/views/beta.blade.php:<!DOCTYPE html><html> <head> <title>Beta</title> </head> <body> <p>This is the Beta page.</p> <p><a href="/alpha">Previous</a></p> </body></html>保存文件,再一次执行 PHPUnit:> ./vendor/bin/phpunitPHPUnit 4.8.19 by Sebastian Bergmann and contributors.F….F..Time: 175 ms, Memory: 14.00MbThere were 2 failures:1) AlphaTest::testDisplaysAlphaFailed asserting that ‘<!DOCTYPE html><html> <head> <title>Alpha</title> </head> <body> <p>This is the Alpha page.</p> <p><a href="/beta">Next</a></p> </body></html>’ does not match PCRE pattern “/Beta/i”.2) BetaTest::testDisplaysBetaFailed asserting that ‘<!DOCTYPE html><html> <head> <title>Beta</title> </head> <body> <p>This is the Beta page.</p> <p><a href="/alpha">Previous</a></p> </body></html>’ does not match PCRE pattern “/Alpha/i”.FAILURES!Tests: 8, Assertions: 23, Failures: 2.然而测试失败了。如果你仔细观察我们的新 HTML,你将注意到我们分别有术语 beta 和 alpha 在 /alpha 和 /beta 页面。这意味着我们需要稍微更改我们的测试让它们与误报不匹配。在每一个 AlphaTest 和 BetaTest 类,更新 testDisplays* 方法去使用 dontSee(’<page> page’)。通过这种方式,这将仅仅匹配字符串而不是那个术语。两个测试文件如下所示:./tests/AlphaTest.php:<?phpclass AlphaTest extends TestCase{ public function testDisplaysAlpha() { $this->visit(’/alpha’) ->see(‘Alpha’) ->dontSee(‘Beta page’); } public function testClickNextForBeta() { $this->visit(’/alpha’) ->click(‘Next’) ->seePageIs(’/beta’); }}./tests/BetaTest.php:<?phpclass BetaTest extends TestCase{ public function testDisplaysBeta() { $this->visit(’/beta’) ->see(‘Beta’) ->dontSee(‘Alpha page’); } public function testClickNextForAlpha() { $this->visit(’/beta’) ->click(‘Previous’) ->seePageIs(’/alpha’); }}再一次运行你的测试,所有的测试都应该通过了。我们现在已经测试我们所有的新文件,包括页面中的 Next/Previous 链接。通过 Semaphore 对 PHPUnit 持续集成通过 Semaphore设置 持续集成你可以自动执行你的测试。这样每一次你进行 git push 提交代码的时候都会执行你的测试,并且 Semaphore 预装了所有最新的 PHP 版本。如果你还没有一个 Semaphore 账户, 先去 注册一个免费的 Semaphore 账户 。接下来需要做的是将它 添加到你的项目,并按照提示逐步去做来执行你的测试:composer install –prefer-sourcephpunit关于 PHP 持续集成 的更多信息,请参照 Semaphore 文档。结语你应该注意到本教程中的所有测试都有一个共同的主题:它们都非常简单。 这是学习如何使用基本的测试断言和辅助函数,并且尽可能的使用它们的好处之一。编写测试越简单,测试就越容易理解和维护。掌握了本教程中介绍的 PHPUnit 断言之后,你还可以去 PHPUnit 文档 找到更多内容。 所有的断言都遵循基本的模式,但你会发现,在大多数测试中都会返回基本的断言。对于 PHPUnit 断言来说,Laravel 的测试辅助函数是极好的补充,这让应用程序的测试变的非常容易。也就是说,重要的是要认识到,对于我们写测试,我们只检查关键信息,而不是整个页面。这使得测试变得简单,并允许页面内容随着应用程序的变化而变化。如果关键信息仍然存在,测试仍然通过,每个人都会满意。文章转自: https://learnku.com/laravel/t… 更多文章:https://learnku.com/laravel/c… ...

March 1, 2019 · 5 min · jiezi

Laravel接入Apollo

废话不说,直接上代码namespace App\Console\Commands\Apollo;use Illuminate\Console\Command;use Illuminate\Support\Arr;use Org\Multilinguals\Apollo\Client\ApolloClient;class SyncCommand extends Command{ /** * The name and signature of the console command. * * @var string / protected $signature = ‘ue:apollo:sync’; /* * The console command description. * * @var string / protected $description = ‘阿波罗同步’; protected $config = []; /* * Create a new command instance. * * @return void / public function __construct() { parent::__construct(); } /* * Execute the console command. * * @return mixed */ public function handle() { while (true) { $this->doSync(); sleep(10); } } protected function doSync() { if (!$this->config) { $this->config = config(‘apollo’); } $server = Arr::get($this->config, ‘server’); $appid = Arr::get($this->config, ‘app_id’); $namespaces = Arr::get($this->config, ’namespace’); $apollo = new ApolloClient($server, $appid, $namespaces); $error = $apollo->start(); logger()->error(null, [’error’ => $error, ‘class’ => CLASS]); }} ...

March 1, 2019 · 1 min · jiezi

Laravel接入Prometheus

在原有的基础上增加Counter计数器:namespace App\Http\Middleware;use Closure;use Illuminate\Http\Request;use traumferienwohnungen\PrometheusExporter\Middleware\AbstractResponseTimeMiddleware;class PrometheusMonitor extends AbstractResponseTimeMiddleware{ protected function getRouteNames() { $routeNames = []; foreach (\Route::getRoutes() as $route){ $routeNames[] = ‘/’.ltrim($route->uri(), ‘/’); } return $routeNames; } /** * Handle an incoming request. * * @param \Illuminate\Http\Request $request * @param \Closure $next * @return mixed / public function handle(Request $request, Closure $next) { if (defined(‘LARAVEL_START’)){ $start = LARAVEL_START; } elseif (defined(‘LUMEN_START’)){ $start = LUMEN_START; } else { $start = microtime(true); } $this->request = $request; /* @var \Illuminate\Http\Response $response */ $response = $next($request); $route_name = $this->getRouteName(); $method = $request->getMethod(); $status = $response->getStatusCode(); $duration = microtime(true) - $start; $duration_milliseconds = $duration * 1000.0; $this->countRequest($route_name, $method, $status, $duration_milliseconds); $this->initRequestMetrics($method, $status); return $response; } public function getRouteName(){ return request()->getRequestUri(); } public function initRequestMetrics($method, $status) { $namespace = config(‘prometheus_exporter.namespace_http_server’); $labelNames = $this->getRequestCounterLabelNames(); $name = ‘request_wuc’; $help = ‘http_requests count’; $counter = $this->registry->getOrRegisterCounter( $namespace, $name, $help, $labelNames ); $counter->incBy(1, [$this->getRouteName(), $method, $status]); }} ...

March 1, 2019 · 1 min · jiezi

laravel-admin 使用记录(2) - 快速搭建 CURD

导语安装完成之后,简单的改了下配置和页面效果。接下来快速的搭建 CURD。就用之前测试用的 faker_users 表进行。控制器和路由指定 model 生成控制器 php artisan admin:make Database\FakerUserController –model=App\Models\FakerUser。执行之后,生成了文件 Database/FakerUserController,并且已经有了相关的操作方法在 admin/route.php 文件中添加路由,最终代码如下<?phpuse Illuminate\Routing\Router;Admin::registerAuthRoutes();Route::group([ ‘prefix’ => config(‘admin.route.prefix’), ’namespace’ => config(‘admin.route.namespace’), ‘middleware’ => config(‘admin.route.middleware’),], function (Router $router) { $router->get(’/’, ‘HomeController@index’); Route::group([‘prefix’ => ‘database’, ’namespace’ => ‘Database’], function ($route) { $route->resource(‘faker_user’, ‘FakerUserController’); });});添加菜单代码修改完成后,添加相对应的菜单添加完成后,来访问一下看起来还不错,CURD 的功能都有了,而且有导出、筛选等功能。细节优化当然还是有些不足的,例如在添加的时候,年龄这里是个开关 简单修改如下/** * Make a form builder. * * @return Form / protected function form() { $form = new Form(new FakerUser); // FakerUser::labels() 是对应的显示标签 $form->text(’name’, FakerUser::labels()[’name’]); $form->email(’email’, FakerUser::labels()[’email’]); $form->number(‘age’, FakerUser::labels()[‘age’]); $form->text(‘city’, FakerUser::labels()[‘city’]); return $form; }看起来好点了????。再改下列表页/* * Make a grid builder. * * @return Grid */ protected function grid() { $grid = new Grid(new FakerUser); $grid->id(FakerUser::labels()[‘id’]); $grid->name(FakerUser::labels()[’name’]); $grid->email(FakerUser::labels()[’email’]); $grid->age(FakerUser::labels()[‘age’])->sortable();// 字段排序 $grid->city(FakerUser::labels()[‘city’]); $grid->created_at(FakerUser::labels()[‘created_at’]); $grid->updated_at(FakerUser::labels()[‘updated_at’]); // 默认倒序 $grid->model()->orderBy(‘id’, ‘desc’); $grid->filter(function ($filter) { // 禁止默认的 id 筛选 $filter->disableIdFilter(); // 姓名筛选 $filter->like(’name’, FakerUser::labels()[’name’]); // 城市筛选 $filter->like(‘city’, FakerUser::labels()[‘city’]); }); return $grid; }结语其余的代码修改请查看 Github。本文只做了简单的修改,laravel-admin 支持很多 CURD 功能,可以看下官方文档。参考资料:laravel-admin 文档。 ...

February 28, 2019 · 1 min · jiezi

上线清单 —— 20 个 Laravel 应用性能优化项

让我们开始吧!假若你的 laravel 应用已经投入生产环境中。从第一个用户,到第十,第一百,直到成千上万的用户!慢慢地,随着用户越多,你的网站会越来越慢那我们应该如何做?细节决定成败经过一番搜索,我决定写下这20个使你网站提升速度的小提示我将从基础开始,大部分都是可以瞬间完成的操作。然后,我将逐步提高难度。最后,就是更高级的内容了。如果你跟着我的步骤一步一步来,我相信你的网站会得到质的提升。享受你的学习之旅!如果你有什么建议,可以在下方留言!我很高兴跟大家共同探讨。基础的优化项让我们看看我们能够在短短几秒钟内做些什么。1. 路由缓存每次服务器执行请求时,都会注册所有的路由,这会花费一些时间。但是,你可以选择缓存路由列表来跳过这个步骤。缓存路由列表是非常简单的。你需要做的是在部署应用程序后,执行下面的这个命令:php artisan route:cache但是,如果你添加或修改了任意一个路由信息,请不要忘记清除之前的缓存以及重新执行缓存命令。php artisan route:clear# 然后,再次执行php artisan route:cache注意,这只对控制器类路由有效。2. 缓存配置就如路由一样,你同样可以在应用中缓存配置文件。设想一下这种场景:每次你发送一个请求到 App 中,Laravel 都需要去加载不同的配置文件,并且要去打开.env 文件读取其中的内容。这种方式性能低下,是不?不过不用担心,这里有个 Artisan 命令专治这个。php artisan config:cache你在部署之后可以使用它。和路由差不多,别忘了编辑东西的时候清理一下缓存。php artisan config:clear# 然后,再来一次…php artisan config:cache3. 优化 Composer 自动加载通常,Composer 生成自动加载文件非常快。但是,在生产环境中,如果设置了PSR-4 和 PSR-0 自动加载规则,这可能会变慢。您可以通过将下面命令添加到部署脚本来优化自动加载器文件创建过程。$ composer dump-autoload -o不要忘记它。4. 谣言:「不要大量使用 Blade 视图」这个谣言我都听到头大了。“千万不要大量使用 Blade 视图,因为它会使得应用性能降低!“彻头彻尾的大谎言!认真脸!铭记这个:Laravel 编译 Blade 视图。编译就是说,在流程结束时,你将拥有一个已编译的完整文件,而非使用多个文件。所以,丝毫不需要担心。*中级干货5. 换个其他/更好的 Cache/Session 驱动默认的,当你新建一个 Laravel 项目的时候Cache 和 Sessions 的驱动默认为 「文件」。在本地开发环境和小项目中它没啥问题,但是项目增长时事情就大条了。所以,考虑下换个更好的驱动例如 Redis。 Laravel 有内置支持它的方式,而你要做的就是 安装 Predis。更多细节在 这里和 此处。6. 尽快升级 Laravel 版本当新版本发布时,请记得尽快升级 Laravel。这不仅关乎新功能:在可能的情况下,所有贡献者都花时间修复代码库周边的性能问题。所以,要持续关注并保持代码更新。7. 删除未使用的服务这是很多人经常忘记的小技巧,要向自己提问:“我需要它吗?*我们知道 Laravel 自带了很多服务,毕竟,这是一个全栈框架,每一个服务都有其用武之地。所以,请花一些时间检查 config/app.php 文件,看看你是否能找到一个你不需要的服务。如果一切正常,请尝试将其删除并测试您的应用程序。它应该有所帮助(一点点)!8. 使用预加载进行查询如果你知道 Laravel 是什么,你可能也知道预加载是什么。 如果您信息不够及时,预加载是一种通过使用特定语法来减少发送到数据库的查询数量来提高 Eloquent 性能的方法。此问题称为N + 1查询问题。 让我们举个例子。 你有两个模型:Book 和 Author。 每本 book 都有它的 author。$books = App\Book::all();foreach ($books as $book) { echo $book->author->name;}想象一下,您的数据库中有1000本书。 现在,此代码将执行 1001 次查询以检索这1000本书的作者姓名。1(查询以获取1000本书的数据)+ 1000(查询以获取每本书的作者数据)= 1001。但是,如果你编写这样的代码$books = App\Book::with(‘author’)->get();foreach ($books as $book) { echo $book->author->name;}更改基础查询以避免此性能问题。 您将只执行两个查询而不是1001! 这是巨大的性能提升。9. 缓存查询结果有时候, 缓存一个具体的查询结果可能是一个好主意。 想象这样一个场景:你准备在你的应用主页上展示 “十大专辑” 排行榜。 这项工作是通过从数据库中执行查询完成的(查询可能涉及到artists表以及其他的一些表)。 你的主页访问量是 1000 次/小时 。如果这个排行榜数据的查询次数是 1000次每小时,那么一天下来执行的查询次数就是24000次。现在,让我们假设这个排行榜是每小时更新一次 。那么,将每次的查询结果缓存一小时如何 ?$value = Cache::remember(’top_10_albums’, 60, function () { return Album::with(‘artist’, ‘producer’)->getTopTen();});这个缓存组件的 remember 方法在未找到缓存的情况下将会先从数据库中获取数据,并缓存60分钟。到期后,将会再次从数据库中获取最新的数据,更新缓存。查询次数 从 24000 到 24 次/天 。10. 为你的数据表建立索引记住,必要的时候请为您的数据表建立索引。 这看起来像是个没什么卵用的提示,但实际上这很有必要。 因为我见过非常多的应用,它们的数据表没有索引。实现起来很简单,您可以创建一个新的数据库迁移并使用里面的方法来添加索引.Schema::table(‘my_table_name’, function(Blueprint $table){ $table->index(‘field_to_be_indexed’);});当然,索引不是您喜欢在哪建就直接创建一个就是了。您必须研究您的业务、代码和查询,去分析哪里才是最需要索引的地方,然后再建立索引。11. 中间件太多?Laravel 会对你注册的中间件进行大量的(前/后)调用。所以,请你仔细检查它们,并且去掉那些你不需要的中间件。通常中间件列表在 Kernel.php 。12. 是时候使用队列了!有些时候,Laravel 比预期慢,这时你可以考虑异步执行任务。最常见的情况就是发送一封欢迎邮件,让我们一起看看任务流程。用户填写我们的表单;将他/她的详细信息写入数据库;发送一封写有欢迎语和确认链接的邮件给他/她;并展示感谢页面;很多时候,这些任务完全是在控制器中并且按照顺序执行。我的建议是学会如何使用事件和队列,可以将发送邮件任务交给专门的流程,以致于改善用户使用体验。*高级干货13. 使用 Pusher 改进异步队列上面我写了一些跟队列有关的内容。有时,你也可以使用队列来改善面向用户的任务。想象一下,你正在创建一个繁重的(在计算方面)进程,并且希望给用户显示这个任务的进度条。你可以轻松地使用队列的异步任务并集成 Pusher 来向前端发送消息以达到目的,即使这个任务没有完成。另一个经常使用的示例是向用户发送消息不需要刷新页面。考虑一下吧!14. 使用 Logs / Debugbars / Laravel Telescope 测量调试工具在提升自己方面,有一句我自己非常喜欢的引用。是从我的 CEO (感谢 Massimo !)引用 Peter Drucker 的话那听来的。如果你无法衡量它,你就无法改进它。这个概念非常适合 Web 应用程序的上下文。要想改善 Web 应用的请求管理时间,需要测量很多东西。幸运地,我们有许多非常优秀的工具来完成这件事。慢日志: MYSQL , MariaDB 和其他数据库可以启用慢日志来追踪那些语句花了大量的时间。你可以使用这些数据来判定是否必须更改或优化特定的代码(或查询);Debugbar : Laravel Debugbar 是一个很棒的扩展包。在很多应用程序方面,你可以使用它来收集数据。比如查询,视图,时间等等;Laravel Telescope : 另一个非常酷的工具是 Laravel Telescope ,对 Laravel 应用,有“优雅的调试助手”的美称。如果你感兴趣, 我已经在这里写了一篇关于它的文章 ;15. 更新你的PHP版本虽然这看起来很简单,但是如果你的项目够大的话,这执行起来会很麻烦,所以我觉得把这条加入高级技巧里面。如果你的 PHP 版本在7.0以下,更新你的 PHP 和 laravel 版本。16. 在服务器上使用 Lumen如果你的应用程序数据量增长很大,你可以考虑为你的系统做服务拆分了。不过,这并没有一个明确的方法指南来引导你完成它:拆分标准的高与低取决于来自应用程序的领域到拆分所有必需的内容所需的工作中的许多因素。但是,一旦你拆分成功,你的项目将获得新生。如果你对这个主题感兴趣的话,可以从 https://medium.com/@munza/lar… 开始。17. 为静态资源提供 CDN 服务我非常肯定你有很多前端的资源,比如 CSS 文件和 JS 脚本。你可以通过多种方式来减少发送给用户的数据量:压缩静态资源;捆绑静态资源(将多个 CSS 文件或者 JS 脚本合并为一个,以减少请求次数);开启 gzip 压缩;然而,如果你遇到大量的流量,则你可以将你的静态资源托管到专用的 CDN 服务器上,比如:Akamai(阿卡迈);Max CDN;Cloudflare;亚马逊 AWS 服务 (S3 + CloudFront);译者注:国内可以使用又拍云和七牛云18. 使用高级测量工具安装 Laravel Debugbar 或 Telescope 将是一个良好的开端,但对于更重大的项目,这还不够。你需要选择更高级的工具,如下:New Relic;AppOptics;Datadog;Sentry;以上列表的应用程序不做同样的事情:他们被设计用于不同目的。花些时间去学习他们以理解他们如何提供帮助。19. 垂直扩展你已经对代码的细枝末节都进行了彻底优化,但是你的应用体量在不断增长。迟早你都要进行垂直扩展。有个简单的说法就是:更多的 RAM,更多的空间,更多的带宽就,以及更多的 CPU注意这个只是对许多没有足够时间来安排重构/优化的初创公司的通常做法。法子是不错,所以你可以认为这是能让你喘口气的临时解决方案。20. 水平扩展水平扩展是另一种扩展的方式,它不同于传统的垂直扩展,主要有两点:取代在现有配置上增加硬件资源的方式,你可能将会添加更多的功能模块来处理日益增加的流量。 在垂直扩展的环境中,你只需要增加服务器配置就行,但是水平扩展应用就意味着你的应用将会部署运行在不同的机器上,有可能是在一个负载均衡机器或者其他服务之后。这就意味着需要更多的设置和配置;此时事情就没那么简单了;并非所有的应用都可以在短时间内扩展完毕,有时候你需要重构隔离一些代码;有时候你需要把应用拆分为不同规模的小型服务。水平扩展会有有很多事情要做,但是一旦你能对应用进行水平扩展时,好处也是超乎想象的。结论今天有足够的内容了!我通过合并我的个人经验和以前做过的一些研究创建了在这个列表。如果你愿意,请尽管提出一些新东西,我很乐意相应更新一下此文章。转自 https://learnku.com/laravel/t… ...

February 28, 2019 · 2 min · jiezi

laravel接入Consul

配置文件:return [ ‘url’ => ‘http://127.0.0.1:8500/v1/kv/’,];命令行:namespace App\Console\Commands\Consul;use GuzzleHttp\Client;use Illuminate\Console\Command;class DeamonCommand extends Command{ /** * The name and signature of the console command. * * @var string / protected $signature = ‘ue:consul:deamon { path : 路径 } ‘; protected $items = []; /* * The console command description. * * @var string / protected $description = ‘consul 守护进程’; /* * Create a new command instance. * * @return void */ public function __construct() { parent::__construct(); } public function handle() { $path = trim($this->argument(‘path’), ‘/’); $url = rtrim(config(‘consul.url’), ‘/’) . ‘/’ . $path . ‘/?recurse’; $client = new Client(); try { $response = $client->request(‘GET’, $url); $rows = json_decode($response->getBody()->getContents(), true); if (count($rows) == 1) { return true; } $name = null; foreach ($rows as $key => $row) { if (substr($row[‘Key’], -1) == ‘/’) { $name = substr($row[‘Key’], 0, -1); $this->items[$name] = []; continue; } $key = trim(str_replace($name, ’ ‘, $row[‘Key’]), ’ /’); $this->items[$name][$key] = base64_decode($row[‘Value’]); } $this->doSave(); } catch (\Exception $ex) { $this->error($ex->getMessage()); return true; } } protected function doSave() { foreach ($this->items as $path => $item) { if (!$item) { $this->error($path .’ : Is Empty.’); continue; } $content = [ “<?php”, “return”, var_export($item, true) . ‘;’ ]; try { \Storage::disk(‘config’)->put($path . ‘.php’, implode("\r\n", $content)); $this->line($path . ’ : Write Success.’); } catch (\Exception $ex) { $this->error($path .’ : ’ .$ex->getMessage()); } } }} ...

February 28, 2019 · 2 min · jiezi

Laravel 5.8 正式发布(文档翻译已启动)

Laravel 5.8 现在面向所有人正式发布了。这个版本包括了几个新特性以及最新的错误修复和对框架核心的改进。一些新特性如下:PHP dotenvLaravel 5.8 集成了 PHP 的 dotenv 3.0 ,下面是 PHP dotenv 3.0 的新特性:在阅读和更改环境变量部分具有更大的灵活性对多行变量的一流支持不再格式化值,你获取到的值就是它们现在的样子支持按顺序多行查找 dotenv 文件,以前只支持一行更强的变量名称验证,避免静态变量或模糊变量造成的错误支持 Carbon 2.0Laravel 5.8 上可以使用 Carbon 1.0 或 Carbon 2.0, 包括可以使用 CarbonImmutable, 甚至可以默认使用 CarbonImmutable 。本地化 Carbon 2.0 做了很大改变,2.0 版本相比较 1.0 版本提供了更友好的国际化支持。了解更多资讯。 Carbon 类在 Laravel 5.8 上的升级.Cache TTL 的改变可能产生中到高影响的重大改变是 来自 Laravel 5.8 的 Cache TTL 的改变 。现在将整型传到缓存的方法由分改为秒。如果你想要在迁移过程中将整型改为 Carbon 或 \DateInterval 实例,请查看我的文章。已弃用的字符串和数组辅助函数不用太担心这个修改,在使用上虽然变更为类的方式,但是具体的使用方法与之前一致。并且 Laravel 有计划将 Helper 作为可选扩展包发布,你仍然可以在项目中使用它们。参考: Laravel 5.8 已弃用的字符串和数组辅助函数自动解析策略从 Laravel 5.8 开始,只要解析策略和模型位于传统位置,您就不需要在 AuthServiceProvider 类中注册它们。如果您更喜欢将非常规路径用于模型和解析策略,则可以注册回调以注册策略或继续手动配置它们:Gate::guessPolicyNamesUsing(function ($class) { // Do stuff return $policyClass;});更多相关信息: Laravel 5.8 将支持授权 Policy 类的自动解析更多新功能Nexmo 和 Slack 信息通知通道Blade 模板文件路径Markdown 文件目录的改变随着今天的发布, Laravel 5.7 将不再接收功能错误修复和更新。 但是,5.7 将在2019年8月之前收到安全更新。Laravel 5.8 是最新的稳定版本,将在2019年8月左右处理收到的错误修复和更新,并在2020年2月左右之前进行安全修复。了解更多可以访问 laravel.com 查看「官方文档」。需要从 Laravel 5.7 升级到 Laravel 5.8,请查看 「升级指南」。升级指南提供了预估的升级影响级别,以帮助你了解升级中最有影响的内容。请确保通读整篇升级指南,以使升级顺利进行。中文翻译中文翻译已启动,请关注:https://learnku.com/laravel/t…更多翻译文章请见 Laravel 开发者社区 https://learnku.com/laravel/c… ...

February 27, 2019 · 1 min · jiezi

f-admin——基于Laravel框架开发的基础权限后台系统

f-admin基础权限后台❤️ 本项目 GitHub / Gitee(码云),目前已在公司产品应用,运行在数个客户服务器内。f-admin基础权限后台是一套基于Laravel框架开发的系统,不需要开发者重复不必要的工作,就可以实现后台功能的快速开发,其主要特点包括:[x] 集成 Composer,安装使用方便。[x] 用户管理可以配置自己的权限。[x] 角色管理可以配置用户及权限。[x] 权限控制可以精确到某一个请求的控制。[x] 菜单可以设置自己的图标,可以控制哪些角色可以看到。[x] 日志查看搜索。[x] 严格的前端后端输入验证。[x] pc端和手机端都能适配。[ ] 其它优化,持续进行中 ……f-admin的运行环境要求PHP5.4以上;laravel框架要求为5.4。线上DEMO f-admin 你也可以用手机扫下二维码查看手机效果 导航效果预览- 首页- 用户管理- 角色管理- 权限管理- 菜单管理- 日志管理安装步骤- 1.获取代码- 2.安装依赖- 3.生成APP_KEY- 4.修改env配置- 5.数据库迁移- 6.访问首页环境配置- 1.windows- 2.linux(apache)- 3.linux(nginx)感谢效果预览(pc/mobile)首页用户管理角色管理权限管理菜单管理日志管理安装步骤1.获取代码新建一个文件夹,进入该文件夹,利用git等工具输入以下命令:git init git clone https://github.com/fangzesheng/f-admin.git2.安装依赖composer install 3.生成APP_KEYcp .env.example .envphp artisan key:generate 4.修改 .env 配置DB_CONNECTION=mysqlDB_HOST=your_hostDB_PORT=your_portDB_DATABASE=your_dbDB_USERNAME=your_usernameDB_PASSWORD=your_pwdCACHE_DRIVER=array //将file改为array5.数据库迁移php artisan migratecomposer dump-autoloadphp artisan db:seed如果在执行php artisan migrate增加表操作出现字段长度过长错误时,则可能是因为mysql版本低于5.5.3,解决方法:a.升级mysqlb.手动配置迁移命令migrate生成的默认字符串长度,在appProvidersAppServiceProvider中调用一下方法来实现配置记得先将新建数据库里面的表清空!!!use Illuminate\Support\Facades\Schema; public function boot(){ Schema::defaultStringLength(191);}6.访问首页访问自己的配置好的域名 用户名:admin 密码:f123456环境配置(仅供参考)1.windows<VirtualHost *:80> DocumentRoot E:\test\public ServerName www.test.com <Directory “E:\test\public”> AllowOverride All order deny,allow Require all granted </Directory></VirtualHost>2.linux(apache)<VirtualHost *:80> DocumentRoot /data/wwwroot/default/f-admin/public ServerName www.fang99.cc <Directory “/data/wwwroot/default/f-admin/public”> AllowOverride All order deny,allow Require all granted </Directory></VirtualHost>3.linux(nginx)server { listen 8088; server_name demo.fang99.cc; location / { index index.php index.html; root /var/www/f-admin/public/; try_files $uri $uri/ /index.php?$query_string; } location ~ .php$ { root /var/www/f-admin/public/; fastcgi_pass 127.0.0.1:9000; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_intercept_errors on; include /etc/nginx/fastcgi.conf; }}感谢layerlaravel如果你觉得这个开源项目对你有用,欢迎star你懂的!谢谢:) ...

February 26, 2019 · 1 min · jiezi

三个月可更改用户昵称两次

前言在实际的项目需求中,我相信很多人都能遇到如标题所说的问题,比如:一个月可修改昵称一次,或者一年可修改昵称三次;我下面的方法也比较简单,是在与朋友的讨论中得到的。需求背景为了表述的更清晰,我这里就简化了需求,如下:每三个月(这里按一个月30天来算, 也就是90天)可更改用户昵称两次,如果三个月内没有用完两次,则下一个三个月拥有的更改次数重置,还是两次。准备工作建立用户数据表 users (这里只列出该文章需要的字段):CREATE TABLE users ( id int(10) unsigned NOT NULL AUTO_INCREMENT, username varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT ‘添加时间’, create_time timestamp NULL DEFAULT NULL COMMENT ‘添加时间’, username_update_num int(10) unsigned NOT NULL DEFAULT ‘0’ COMMENT ‘用户昵称修改次数’, PRIMARY KEY (id),) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci COMMENT=‘用户主表’;我使用的是 laravel 框架,使用其内置中间件进行过滤应用程序 HTTP 请求;具体代码先上中间件里的代码,可跟着注释看。这里不着重写中间件的实现方式了,如需了解,请点击 中间件 public function handle($request, Closure $next) { /** * 目前要解决的问题是: 每三个月(90天)可更改昵称 2 次 * 下面是解决逻辑 / // 得到该用户信息 $user = User::where(‘id’, session(‘uid’))->first(); // 用户注册的时间,create_time 使用的是 timestamp 类型,所以要转换一下,方便计算 $create_time = strtotime($user->create_time); /* * 计算从注册时间起一共过去了几个 90天,也就是过了几轮 * 当前时间减去注册时间 除以 90天的秒数 = n 轮 * 得到的数值 n 很少有整数,比如:1.2 ; * 此时需要进一法处理,因为只要比90天多,哪怕多一秒也要进入下一轮 / $n = ceil( round( (time() - $create_time) / (90 * 24 * 3600), 2) ); /* * 每 90 天可修改两次,每修改一次,数据表 username_update_num + 1 * 现总修改次数:用户自注册时间起至今,共修改的多少次 * 每轮拥有修改次数:每 90天用户有两次修改机会 * 现总修改次数 / 每轮拥有修改次数 = 现修改到第几轮;用 $a 表示 / $a = $user->username_update_num / 2; // 这里写的是 >=,实际情况下,$a 是不可能大于 $n 的 if($a >= $n){ return response()->json([‘code’ => 0, ‘message’ => ‘用户昵称三个月内只能修改两次,您的次数已用完’, ‘data’ => ‘’]); }else{ // 说明前 ($n - 1) 轮中有未用完的次数 if( ($n - $a) > 1){ // 手动更改数据库,补全修改次数,也就是默认以前的每轮都把两次机会用完 $user->update([‘username_update_num’ => (($n - 1) * 2)]); } } /* * 这里是判断必传参数,与上面逻辑没有联系 / if(empty($request->post(‘username’))){ return [‘code’ => 0, ‘message’ => ‘用户昵称不能为空’, ‘data’ => ‘’]; } if($request->post(‘username’) === $user->username){ return [‘code’ => 0, ‘message’ => ‘修改后的昵称不能与原昵称一致’, ‘data’ => ‘’]; } return $next($request); }上面中间件的内容已经写完了,可能会让人有些迷糊,先别急,因为还没有写完,上面只是中间件的内容,是为了拦截已经没有机会修改昵称的用户,以及处理没有用完次数的用户,请接着看下面的控制器 UserController.php 的内容:UserController.php /* * 用户昵称修改 (三个月可修改两次) * * @param \Illuminate\Http\Request * @return \Illuminate\Http\Response */ public function usernameUpdate(Request $request) { $user = User::where(‘id’, session(‘uid’))->first(); $data = [ ‘username’ => $request->post(‘username’), ‘username_update_num’ => $user->username_update_num + 1, ]; if( !$user->update($data) ){ return [‘code’ => 0, ‘message’ => ‘更改用户昵称失败’, ‘data’ => ‘’]; } return [‘code’ => 1, ‘message’ => ‘更改用户昵称成功’, ‘data’ => ‘’]; }如上述 UserController.php 控制器,因为用户是否满足更改昵称条件已经在中间件里做过判断,所以能进来控制器的请求,均是有修改昵称次数的用户,只需直接更改更改昵称且更改次数 + 1 即可。总结这篇文章所讲述的方法适合同种类型的需求,可根据需求更改相应参数。细节上的处理不多,比如:实际上每个月的天数不一定是 30天,这里不做讨论,可相应处理时间即可。主要还是记录该种处理方法,也一定有比这种更好的方法!道路阻且长,仍需不断前行! ...

February 26, 2019 · 2 min · jiezi

Laravel 编码实践分享

将任何 PHP 框架称为最好的框架都是错误的,因为不同的框架都有各自的优点。 通常来说,一个PHP开发者会根据项目需求来选择合适的框架。 但相信我, 我现在已经完全爱上了 Laravel。关于 Laravel,它使用起来简单且舒适,适用于编写产品代码,并能极大的推动开发过程。 Laravel 中我最喜欢的一点是它是使用当下编程中的最佳实践所构建的。我个人更喜欢保持 Laravel 推荐的基本代码结构。当然你也可以选择其他可用的方法,但这可能会在之后的使用中出现一些问题。这里有一些在 Laravel 开发中值得记住的简单建议:最大限度的使用你的 .env 文件;不要破坏框架核心,不要编辑 vendor 文件夹中的文件,你可以选择继承相关函数来实现。扩展优于修改。不要直接通过 PHPMyAdmin 或者其他数据库控制台创建表和索引。 请使用数据库迁移表来创建表、增加修改字段,然后提交到 Git 仓库。测试的时候不要直接向数据库插入假值。 创建填充文件(Seeder 文件)来填充数据库。更倾向于使用 Artisan 脚手架而不是手动创建东西,这会极大的提升你的生产力。确保使用一些 artisan 命令来提升性能:php artisan route:cache // 路由缓存php artisan config:cache // 配置信息缓存php artisan optimize — force // 类映射加载优化尽量不要将闭包写在 routes.php 文件中,而是将它们移到你的控制器中。创建自定义的类和函数时要特别注意命名规范,尤其是对于模型。 Laravel 的工作原理是这样的,对于一个命名为 users 的表, Laravel 希望该表的模型被命名为 User 。尽量为每一个请求创建 Validation Requests 。尽管 PHP 有一个能够帮助你读取、写入、比较或者计算日期的 DateTime 类,但还是建议你使用 Carbon 扩展来处理日期。始终保持使用最新的版本, Laravel 更新得很快,所以跟上节奏。为了更好的性能,始终使用 gulp、 Elixir 来将你的脚本和 sass 文件编译为压缩版, Laravel 已经为你做好了底层的工作。欢迎在评论里推荐更多内容…更多翻译文章请见 Laravel 开发者社区 https://learnku.com/laravel/c… ...

February 26, 2019 · 1 min · jiezi

laravel-admin 使用记录(1) - 安装

导语网站搭建已经好几个月了,起初没有想着用后台。一来是没有开放访问,二是也没有保存什么数据。突发奇想要用 laravel-admin 是因为工作中正好用到,借此机会熟悉下。安装按照文档进行安装就可以了,注意前提是 laravel 使用正常,并且数据库已经链接成功。composer require encore/laravel-adminphp artisan vendor:publish –provider=“Encore\Admin\AdminServiceProvider"这个时候已经生成了 config/admin.php 配置文件,可以根据需求进行修改。最后一步是 php artisan admin:install 登录测试经过如上步骤的安装之后,来测试一下。网址为 you_site/admin/,页面如下输入账号 admin 和密码 admin 就登录进去了。参考资料:laravel-admin 文档。

February 25, 2019 · 1 min · jiezi

Laravel 5.8 前瞻

无论是从零开始创建新项目还是升级现有的项目,你都应该了解一下 Laravel 5.8 的新特性和变化。Laravel 5.0 发布于 2015 年 2 月,从那时起大约每六个月会发布一次 5.x 的新版本。上一个版本(Laravel 5.7)于 2018 年 9 月发布,因此我们预计可以在 2019 年 3 月左右看到 Laravel 5.8的发行版。当你创建一个新项目或升级现有的项目时,你应该注意到 Laravel 5.8中的新功能和一些重要的变更,在本文中,我们会为你快速的介绍一遍。和往常一样,在升级 Laravel 版本之前,请务必仔细阅读并理解 升级指南,以确保升级过程的顺利。下面,让我们一起了解一下Laravel 5.8 中的一些重要更新。邮箱字段验证:在 Laravel 5.8 中内置的email 验证规则将支持国际字符如果你的项目中有如下表单验证规则:$request->validate([ ’email’ => ’email’, ]);并尝试验证邮箱hej@bär.se,在5.7及以前版本中,验证会失败,但是在5.8中将能通过验证。在5.7版本中表单验证逻辑与 SwiftMailer(Laravel使用的PHP mailer库) 的逻辑并不匹配,但是现在它们都符合 RFC6530 规范。dotenv 3.0:Laravel 5.8 将会支持 相对较新 的 dotenv 3.0 来管理项目中的 .env 环境文件。dotenv 3.0 中的关键更新是支持环境文件中支持多行字符串和保留字符串末尾的空格,例如:DEVELOPMENT_APP_KEY=“specialstringforthisapp"在之前的版本中,这仅会返回 specialstringfor,但在 Laravel 5.8 里,它会解析整个 specialstringfor thisapp。新版本还会保留字符串末尾的空格,而在之前的版本里,空格会被忽略。对于需要多行 API 秘钥以提高安全性的场景来说,这是一个很棒的更新。更改 Mailables 的目录名称:这不是一个新功能,而是升级项目时需要注意的重要关键点。如果您的项目中有可填写的东西,并且您使用 php artisan vendor:publish 命令定制了组件,则文件夹名称稍有变化,即 /resources/views/vendor/mail/markdown 目录现在名为 /resources/views/vendor/mail/text 。 这是因为两个文件夹都可以包含 markdown 代码,用于制作带有纯文本的漂亮响应式的 html 模板。 调用 markdown 文件夹文本更合乎逻辑。新的错误页面模板:Laravel 5.8 将附带新的错误页面,其中包含极简主义的设计,旨在更适合各种网站和网络应用程序,而无需重新设计以适应主题。Laravel 5.7 404 视图 「上面」 和 5.8 404 视图 「下面」如果你愿意,仍然可以自定义错误页面或者导入以前的设计(请查看 自定义laravel错误页面的教程)。弃用 Array 和 String 辅助函数:所有的 array_ * 和 str_ * 全局辅助函数都已弃用,将在 Laravel 5.9 中删除。 应该使用 Arr :: 和 Str :: 方法。 如果您不能或不想重新编写现有的代码和有可用于维护功能的软件包,但如果您需要使用它们,现在习惯于使用新的命令行是一种好习惯。当前版本搜索 array_* 方法:function array_add($array, $key, $value)应该换成:Arr::add($array, $key, $value)当前版本搜索 str_* 方法:function str_contains($haystack, $needles)应该换成:Str::contains($haystack, $needles);事实上,如果 你检查了 array_ 和 str_ 全局助手函数的 5.8 代码 ,你会看到 他们已经使用了静态代理版本了。Caching — 过期时间(ttl)现在是以秒钟而不是分钟来描述:请注意,如果你正在使用 Laravel 的缓存组件,当你传入一个 integer 型的 ttl 参数给缓存函数时,5.8 中会被设置为以秒为单位生存时间,而不是 5.7 中的分钟,例如:Cache::put(‘foo’, ‘bar’, 30);在 Laravel 5.7 中,foo 会被存储 30 分钟,而在 5.8 里仅仅会存储30秒。这是一个简单但 非常重要 的更新。MySQL 中的 JSON 值:如果您在 MySQL 和 MariaDB 数据库列中存储 JSON 值,则在 5.7 Laravel 中将返回用双引号括起来的值。 5.8 将返回更干净的相同值。以下是 Laravel 升至指南中说明更改的示例:$value = DB::table(‘users’)->value(‘options->language’);dump($value);// Laravel 5.7…’“en”’// Laravel 5.8…’en’Carbon 2 的版本支持您现在可以选择在 Laravel 5.8 中使用 Carbon 1 或 Carbon 2 作为 DateTime 函数。点击这里 Carbon migration guide 来确定你是否真的要启用 Carbon 2。Nexmo 和 Slack Notification 通知:Nexmo 和 Slack Notification 通知已从 Laravel 主项目中删除,并提取到第三方软件包中。要在项目中继续使用 Slack 或 Nexmo 功能,您需要使用:composer require laravel/nexmo-notification-channelcomposer require laravel/slack-notification-channel然后可以像以前一样配置和使用它们。所以这几乎涵盖了你应该注意的关键变化。我们总是喜欢在 Welcm Software 上查看新的软件版本,并期待很快发布 5.8 版本。更多翻译文章请见 Laravel 开发者社区 https://learnku.com/laravel/c… ...

February 25, 2019 · 2 min · jiezi

后端_Laravel

Laravel搭建开发环境Laragon是集成开发工具,作为开箱即用的工具,内置APache,Cmder,Composer,Git,HeidiSQL,Laragon,MYSQL,Nginx,Node.jsNotepad++,PHP,Redis,Yarn等.

February 23, 2019 · 1 min · jiezi

Laravel核心解读--完结篇

过去一年时间写了20多篇文章来探讨了我认为的Larave框架最核心部分的设计思路、代码实现。通过更新文章自己在软件设计、文字表达方面都有所提高,在刚开始决定写Laravel源码分析地文章的时候我地期望是自己和读者通过学习Laravel核心的代码能在软件设计上带来提高,这些提高主要是指两方面:通过学习Laravel核心的代码来辅助理解软件设计行业中经常提及的核心概念,通过学习像IocContainer、面向对象的五大原则SOLID 是怎么应用到框架设计中去的来指导应该如何去做软件开发设计。这方面对你的收益应该是跳出Laravel框架和PHP语言层面的,当你需要切换到其他框架和语言时这些收益仍会反馈给你。熟练掌握Laravel的使用,虽然很多人说框架只是一个工具不应该花太多时间在工具的研究上,但是现实时开发者群体大部分人并没有在头部的那几家大公司,也不架构师,我们多数的工作还是在写业务代码,那么既然你需要Laravel这个工具帮你完成每天的任务,那么为了尽可能高效率高质量的完成项目,确实是需要多了去看看框架的源码,了解一些框架常用的方法在positive和negative时的行为到底是什么(各种情况下的返回值和抛出的异常),知道怎么使用ORM才能让查询更高效等等,这些内容往往在框架的文档都是很少提及的,需要去看源码了解一下,如果你只会文档里提到的那些典型的用法显然不能算是熟练掌握的。Laravel整个框架设计到的内容有很多,其他的组件我也就不再一一去写文章梳理了, 相信你在认真看完这个系列的文章后,假如你在使用其他组件过程中遇到了诡异的问题,或者好奇框架是怎么帮你实现功能的?你完全有能力去梳理其他组件的源码实现来解决你的疑惑。为了大家阅读方便,我把这些源码学习的文章汇总到这里。类地反射和依赖注入IocContainer服务提供者FacadesRouteMiddleware控制器RequestResponseDatabase基础QueryBuilder模型CRUD模型关联事件系统Auth认证系统(基础介绍)Auth认证系统(实现细节)自定义你的Auth认证系统SessionCookieContracts契约加载ENV配置HTTP内核Console内核异常处理最后还是回到上面说的,框架只是工具如果想要在软件行业有所发展还是要把更多的精力投入到内功修炼上,所谓内功就是这些经过时间沉淀下来的基础知识,框架层出不穷,但是它们应用的基础知识却甚少改变。数据库、HTTP、算法和数据结构这些都是编程的内功,只有内功深厚了才能解决遇到的复杂问题。推荐几个我认为挺好的修炼内功的专栏给大家:程序员的数据基础课MySQL实战45讲数据结构与算法算法面试通关40讲Spring boot和Spring Cloud实战教程当然还有日新月异的前端知识也是需要会基础的用法的,最起码了解一下团队内部使用的前端框架的基础知识,这样对咱们做系统设计也会有帮助,最近在另外一个平台上看到分享的一个免费教程使用Laravel和Vue构建API驱动的应用,讲的非常好,希望Vue能快速入门的可以跟着教程一起动手练习练习。

February 22, 2019 · 1 min · jiezi

Laravel - Auth验证流程以及guard守卫和自定义驱动driver驱动,使用web-token验证

不同认证方式我们先来看 config/auth.phpproviderproviders 数组让我们可以配置一个提供者,每个提供者可以选择不同的 driver.driver可以选择eloquent 或者 database ,对应的驱动之后选择对应的配置项,eloquent:model,database:tableguard在拥有provider之后我们可以配置guards 守卫,守卫可以配置一个驱动者和一个提供者提供者就是我们上面配置的provider而驱动者则有session(session认正),token(token认正)可供选择默认api使用的是token认正,而web用户使用session认正session认正在认证时我们可以使用Auth::attempt([’email’ => $email, ‘password’ => $password])方法,此方法在验证成功后会自动为这个用户设置一个认证 Session,标识该用户登录成功后面就可以使用Auth::guard()->check()方式验证用户是否已经登录token认正此认正方式laravel虽然提供了驱动方法,但是并没有默认它为验证方式,也没有提供自动生成token的方法,要使用此方法要自定义login方法通过查看底层的\vendor\laravel\framework\src\Illuminate\Auth\TokenGuard.php方法,我们可以发现laravel5.5底层默认的是token字段,我们也可以在此自定义此字段,在此我使用了web_token作为认正字段然后我们还要在数据库里建立相应的字段web_token注:如果使用redis等nosql保存web_token的话也是需要web_token的,为了使laravel自带的Auth门面可以使用建立完字段以后就可以写登录方法了:在这里还是使用了redis去保存token,便于设置token的过期时间至于为什么还要保存在数据库里,在注销或者token过期的时候还要更新数据库的token,是因为Auth底层获取user的方法是从数据库进行获取的贴上源码来看一波首先还是\vendor\laravel\framework\src\Illuminate\Auth\TokenGuard.php文件这里插一下,如果是想把token放在header头里传值,还要在TokenGuard.php加入这一段如果不加入这一段只能从body里面获取token,头里传的token获取不到,如果是我理解有误,希望指出回到原来,我们要说Auth::user()方法, 这个方法会先实例化一个guard守卫指定的驱动,不指定的话就是默认的可以参考这段代码\vendor\laravel\framework\src\Illuminate\Auth\AuthManager.php指定的话,就会去实例化指定的guard,比如Auth::guard(‘user’)->user()我们这里默认的就是守卫adminToken的驱动就是token当我们调用Auth::user( ) 时会调用\vendor\laravel\framework\src\Illuminate\Auth\TokenGuard.php里的然后我们找到retrieveByCredentials()这个方法在vendor\laravel\framework\src\Illuminate\Auth\EloquentUserProvider.php可以看出这个方法用token为条件在elquentModel里查出了一条userObject并返回给了我们所以我们Auth::user() 得到的user对象是在model里用token查出来的,所以如果想使用此功能的话,数据库里的token字段一定要保持更新当然你也可以抛弃不用,或者改变源码让他从redis中取到token和对应的id,再用id去model中取数据这里理解了之后我们在写一个middleware用来验证在访问网站时token是否正确就行了把新建的middleware加入kernel.php中最后在要被验证的方法里的构造方法里调用这个middleware就可以开启我们得token验证了如果你有某个方法不想使用验证, 可以使用except()方法把其排除了

February 22, 2019 · 1 min · jiezi

Laravel+Dingo/Api 自定义响应

在最近的开发开发项目中,我使用了Dingo/Api这个第三方Api库。Dingo是个很强大的Api库, 但在开发的过程中,需要自定义响应字段。刚开始使用Ding/Api时,返回如下:{ “message”: “422 Unprocessable Entity”, “errors”: { “mobile”: [ “手机号格式不正确” ] }, “status_code”: 422}这是输入字段验证错误时,Dingo返回的结果。这样看上去没什么问题。因为这边 status_code 是比较规范的。对于 PHP 来说,直接 json_decode 之后,并没有什么难办的地方。但是对面安卓和 IOS 则是使用的强类型语言。尤其是 Java,需要对每一个 Json 对象进行新建,然后序列化。所以,这种格式不统一的返回结果,是无法接受的解决方法: 我们需要将所有的异常信息归总到一个地方,在AppServiceProvider的boot()方法中添加// 将所有的 Exception 全部交给 App\Exceptions\Handler 来处理app(‘api.exception’)->register(function (Exception $exception) { $request = Illuminate\Http\Request::capture(); return app(‘App\Exceptions\Handler’)->render($request, $exception);}); 然后在App\Exceptions\Handler.php中的render()方法中:$class = get_class($exception);switch ($class) { case ‘Dingo\Api\Exception\ValidationHttpException’: if ($request->expectsJson()) return $this->errorRespond($exception->getErrors()->first(), $exception->getStatusCode()); break; default: if ($request->expectsJson()) return $this->errorRespond(‘系统休息了’, 500000); break;}再次访问接口:{ “response_status_code”: 422, “response_message”: “请填写手机号”, “data”: []}

February 16, 2019 · 1 min · jiezi

laravel-admin 开发 bootstrap-treeview 扩展包

laravel-admin 扩展开发文档https://laravel-admin.org/doc…效果图:开发过程:1、先创建Laravel项目,并集成laravel-admin,教程:http://note.youdao.com/notesh…2、生成开发扩展包php artisan admin:extend csp/cascade –namespace=Csp\Cascade其中, csp/cascade 是包名, CspCascade 是命名空间,生成的结构如下(删减版):3、删除没必要的目录,以及添加CSS、JS资源4、修改CascadeServiceProvider4.1、修改视图的命名空间if ($views = $extension->views()) { $this->loadViewsFrom($views, ’laravel-admin-cascade’);}4.2、修改资源发布的位置,这里将资源发布到/public/vendor/laravel-admin-ext/cascade 目录下。if ($this->app->runningInConsole() && $assets = $extension->assets()) { $this->publishes( [$assets => public_path(‘vendor/laravel-admin-ext/cascade’)], ’laravel-admin-cascade’ );}4.3、编写视图文件,在views/目录下创建 cascade.blade.php<div class=“form-group {!! !$errors->has($label) ?: ‘has-error’ !!}"> <label for=”{{$id}}" class=“col-sm-2 control-label”>{{$label}}</label> <div class="{{$viewClass[‘field’]}}"> @include(‘admin::form.error’) <div id=“csp-bootstrap-tree”></div> <input type=“hidden” name="{{$id}}" id="{{$id}}"> @include(‘admin::form.help-block’) </div></div>4.4、编写 CascadeTreeView 继承 Field<?php/** * Created by PhpStorm. * User: chenshaoping * Date: 2019/2/10 * Time: 10:02 */namespace App\Admin\Extensions\csp\cascade\src;use Encore\Admin\Form\Field;class CascadeTreeView extends Field{ protected $view = ’laravel-admin-cascade::cascade’; protected static $css = [ ‘/vendor/laravel-admin-ext/cascade/bootstrap-treeview.min.css’ ]; protected static $js = [ ‘/vendor/laravel-admin-ext/cascade/bootstrap-treeview.min.js’ ]; public function render() { $this->script = <<<EOT var set = new Set();var tree = [ { text:“Parent 1”, id:1, nodes: [ { text:“Child 1”, id:2, nodes: [ { text:“Grandchild 1”, id:3, nodes: [ { text:“122”, id:4, nodes: [ { text:“qweqw”, id:5, } ] } ], }, { text:“Grandchild 2”, id:6, } ] }, { text:“Child 2”, id:7, } ] }, { text:“Parent 2”, id:8, }, { text:“Parent 3”, id:9, }, { text:“Parent 4”, id:10, }, { text:“Parent 5”, id:11, }]; $(’#csp-bootstrap-tree’).treeview({data: tree, showIcon: false,showCheckbox: true,‘showTags’:true});$(’#csp-bootstrap-tree’).on(’nodeChecked’, function(event,node) { set.add(node.id); $(’#{$this->id}’).val(Array.from(set).toString());});$(’#csp-bootstrap-tree’).on(’nodeUnchecked’, function(event,node) { set.delete(node.id); $(’#{$this->id}’).val(Array.from(set).toString());});EOT; return parent::render(); }}4.5、在laravel-admin 启动时,添加资源,添加扩展FormAdmin::booting(function () { Admin::js(‘vendor/laravel-admin-ext/cascade/bootstrap-treeview.min.js’); Admin::css(‘vendor/laravel-admin-ext/cascade/bootstrap-treeview.min.css’); Form::extend(‘cascade’, CascadeTreeView::class);});5、准备本地安装5.1、此时如果输入composer require csp/cascade 会报以下错误Could not find a version of package laravel-admin-ext/cascade matching your minimum-stability (stable). Require it with anexplicit version constraint allowing its desired stability.原因很简单,composer的最小稳定性设置不满足,require 需要的是稳定版本,我们这里的确实 dev的版本;这里有2种解决方式:1、修改项目的composer.json"minimum-stability": “dev”,“prefer-stable”: true,2、修改扩展包的composer.json"version": “1.0.0”,5.2、开始本地安装composer require csp/cascade5.3、发布资源php artisan vendor:publish –provider=“Csp\Cascade\CascadeServiceProvider"此时会看到在 public/vendor/laravel-admin-ext/cascade 目录下面有静态资源。6、使用$form->cascade(‘parent_id’,‘权限’)->help(‘陈少平’);提交表单的时候,会将 parent_id 以 ,(逗号) 分割提交所有被选中的值。 ...

February 11, 2019 · 2 min · jiezi

关于PHP在企业中处理数字加减乘除和对比运算方案

如果在PHP中对数字或者字符串加减乘除处理不当的话、会导致结果不够严谨,通常的、假如你需要处理加减乘除应该会是这样:$a = 1;$b = 2;$a * $b;$a + $b;$a - $b;$a / $b;比如出现问题:4.35-4.34等于0.0099999999999998比如出现问题:‘4.35’-‘4.34’等于0.0099999999999998但假如两个类型不一致或者有精确度缺失就会导致一些问题的存在、我们可以使用PHP自带的函数来做加减运算处理:<?php // 设置默认小数点保留位数 bcscale(2); // 加法 echo bcadd(1234567890.123,987654321987654321), PHP_EOL; // 减法 echo bcsub(1234567890.123,987654321987654321), PHP_EOL; // 乘法 echo bcmul(1234567890.123,987654321987654321), PHP_EOL; // 除法,指定保留小数后20位,否则小数点不够结果会是0 echobcdiv(1234567890.123, 987654321987654321, 20), PHP_EOL;或者这时候、你需要对比两个数值的大小范围、我建议你这样做,使用bccomp(‘1.00’,‘1.00’,2)比较两个数字的大小上面都可以参考这一页的手册:http://php.freehostingguru.co…

February 11, 2019 · 1 min · jiezi

laravel常用命令整理

##创建项目laravel new blog || composer create-project –prefer-dist laravel/laravel blog##安装组件composer install ##刷新组件composer update ##删除组件composer remove chensuilong/toastr composer dump-autoload##查看artisan命令php artisanphp artisan list ##启动PHP的Web服务php artisan serve ##查看某个帮助命令php artisan help make:modelphp artisan make:model User –migration 创建模型并创建新迁移 ##查看laravel版本php artisan –version ##使用 PHP 内置的开发服务器启动应用php artisan serve ##生成一个随机的 key,并自动更新到 app/config/app.php 的 key 键值对(刚安装好需要做这一步)php artisan key:generate ##开启Auth用户功能(开启后需要执行迁移才生效)php artisan make:auth ##开启维护模式和关闭维护模式(显示503)php artisan downphp artisan up ##进入tinker工具php artisan tinker ##列出所有的路由php artisan route:list##生成路由缓存以及移除缓存路由文件php artisan route:cachephp artisan route:clear ##重新生成签名php artisan passport:install##自动生成Laravel密钥php artisan key:generate ##Auth 系统php artisan make:auth##创建控制器php artisan make:controller StudentController ##创建Rest风格资源控制器(带有index、create、store、edit、update、destroy、show方法)php artisan make:controller PhotoController –resource ##创建模型php artisan make:model Studentphp artisan make:model User –migration //创建模型并创建新迁移 ##创建新建表的迁移和修改表的迁移php artisan make:migration create_users_table –create=students //创建students表php artisan make:migration add_votes_to_users_table –table=students//给students表增加votes字段 ##执行迁移php artisan migratephp artisan migrate:rollback //回滚最新一次迁移 ##创建模型的时候同时生成新建表的迁移php artisan make:model Student -m ##回滚上一次的迁移php artisan migrate:rollback ##回滚所有迁移php artisan migrate:resetphp artisan migrate:refresh //更新表结构 ##创建填充php artisan make:seeder StudentTableSeeder 执行单个填充php artisan db:seed –class=StudentTableSeeder ##执行所有填充php artisan db:seed ##创建中间件(app/Http/Middleware 下)php artisan make:middleware Activity ##创建队列(数据库)的表迁移(需要执行迁移才生效)php artisan queue:table ##创建队列类(app/jobs下):php artisan make:job SendEmail ##创建请求类(app/Http/Requests下)php artisan make:request CreateArticleRequestphp artisan:显示详细的命令行帮助信息,同 php artisan listphp artisan –help:显示帮助命令的使用格式,同 php artisan helpphp artisan –version:显示当前使用的 Laravel 版本php artisan changes:列出当前版本相对于上一版本的主要变化php artisan down:将站点设为维护状态php artisan up:将站点设回可访问状态php artisan optimize:优化应用程序性能,生成自动加载文件,且产生聚合编译文件 bootstrap/compiled.phpphp artisan dump-autoload:重新生成框架的自动加载文件,相当于 optimize 的再操作php artisan clear-compiled:清除编译生成的文件,相当于 optimize 的反操作php artisan migrate:执行数据迁移php artisan routes:列出当前应用全部的路由规则php artisan serve:使用 PHP 内置的开发服务器启动应用 【要求 PHP 版本在 5.4 或以上】php artisan tinker:进入与当前应用环境绑定的 REPL 环境,相当于 Rails 框架的 rails console 命令php artisan workbench 组织名/包名:这将在应用根目录产生一个名为 workbench 的文件夹,然后按 组织名/包名 的形式生成一个符合 Composer 标准的包结构,并自动安装必要的依赖【需要首先完善好 app/config/workbench.php 文件的内容】php artisan cache:clear:清除应用程序缓存php artisan command:make 命令名:在 app/commands 目录下生成一个名为 命令名.php 的自定义命令文件php artisan controller:make 控制器名:在 app/controllers 目录下生成一个名为 控制器名.php 的控制器文件php artisan db:seed:对数据库填充种子数据,以用于测试php artisan key:generate:生成一个随机的 key,并自动更新到 app/config/app.ph 的 key 键值对php artisan migrate:install:初始化迁移数据表php artisan migrate:make 迁移名:这将在 app/database/migrations 目录下生成一个名为 时间+迁移名.php 的数据迁移文件,并自动执行一次 php artisan dump-autoload 命令php artisan migrate:refresh:重置并重新执行所有的数据迁移php artisan migrate:reset:回滚所有的数据迁移php artisan migrate:rollback:回滚最近一次数据迁移php artisan session:table:生成一个用于 session 的数据迁移文件 ...

January 30, 2019 · 2 min · jiezi

laravel项目一次发布导致的BUG(环境变量问题)

laravel项目一次发布导致的BUG背景laravel项目的某一次发布后,项目中连接数据库突然报错,而用同样的数据库账号密码在机器上连接是可以的。临时解决方案经过短暂时间的排查,没找到原因,原数据库密码DB_PASSWORD=abcde#142!,修改数据库密码为DB_PASSWORD=abcde2019后,恢复正常。排查思路变更密码后,数据库能正常连接,可见是密码问题,同时同样的密码在项目中访问数据库失败而在机器上可以访问成功,可判断是环境问题导致的密码问题。在项目中打印数据库连接配置的日志,如下:Array( [driver] => mysql [host] => xxx [port] => xxx [database] => xxx [username] => xxx [password] => abcde [unix_socket] => [charset] => utf8mb4 [collation] => utf8mb4_unicode_ci [prefix] => [strict] => 1 [engine] => )可见密码配置在env中为DB_PASSWORD=abcde#142!,但是在PHP代码中读取的数据库密码配置为abcde,可见#后面的内容代码中认为是注释,从而忽略了。继续查看jenkins发布日志,发现了有一段日志输出:Package operations: 0 installs, 3 updates, 0 removals - Updating vlucas/phpdotenv (v2.5.2 => v2.6.0): Downloading (connecting…)Downloading (0%) Downloading (15%)Downloading (100%)发布过程中,有一个依赖包的升级。查看vlucas/phpdotenv的文档,看到以下说明:CommentsYou can comment your .env file using the # character. E.g.# this is a commentVAR=“value” # commentVAR=value # comment解决方案.env文件中,对密码字段加上双引号,如DB_PASSWORD=“abcde#142!*",然后一切恢复正常。建议.env文件中,环境变量的配置,最好都加上”",避免出现意外的灾难。 ...

January 30, 2019 · 1 min · jiezi

PHP 转 Go,用 Laravel、thinkphp 的用法造了一个 ThinkGo 框架

ThinkGo 是一个轻量级的 Go 语言 MVC 框架,目前支持路由、中间件、控制器、请求、响应、Session、视图、日志等 web 框架应该具备的基本功能,致力于让代码简洁、富于表达力,帮助开发者快速构建一个 Web 应用。安装go get -u github.com/thinkoner/thinkgo用法package mainimport ( “github.com/thinkoner/thinkgo” “fmt” “github.com/thinkoner/thinkgo/router” “github.com/thinkoner/thinkgo/context”)func main() { app := thinkgo.BootStrap() app.RegisterRoute(func(route *router.Route) { route.Get("/", func(req *context.Request) *context.Response { return thinkgo.Text(“Hello ThinkGo !”) }) route.Get("/ping", func(req *context.Request) *context.Response { return thinkgo.Json(map[string]string{ “message”: “pong”, }) }) // Dependency injection route.Get("/user/{name}", func(req *context.Request, name string) *context.Response { return thinkgo.Text(fmt.Sprintf(“Hello %s !”, name)) }) }) // listen and serve on 0.0.0.0:9011 app.Run()}项目地址GitHub: https://github.com/thinkoner/...Gitee: https://gitee.com/thinkgo/thi…大佬们来指点指点,贡献贡献代码。。。 ...

January 29, 2019 · 1 min · jiezi

Laravel 5通过中间件实现JSON_UNESCAPED_UNICODE和跨域控制

做 json 接口的使用 JSON_UNESCAPED_UNICODE,能在返回大量非 ascii 字符数据的时候节约大量流量(其实就是把 \uxxxx 转换成人能看懂的中文)。在 Laravel 框架里最易懂的办法就是用return response()->json($data, 200, [], JSON_UNESCAPED_UNICODE)返回接口数据。但是这种方法可复用性非常低,而且不太好处理 http 状态码问题。作为一个喜欢装牛逼的程序员,我需要研究一个看起来很牛逼的方法,我的目标是高复用、低耦合。经过连续施展 Google 大法,遂得出以下方法:1 php artisan make:middleware JsonCors建立中间件,然后在handle方法里加入下面的代码:$data = $next($request);if ($data instanceof \Illuminate\Http\JsonResponse) { $data->setEncodingOptions(JSON_UNESCAPED_UNICODE); // 下面是跨域控制代码 $data->withHeaders([ ‘Access-Control-Allow-Origin’ => ‘*’, ‘Access-Control-Allow-Credentials’ => ’true’, ]);}return $data;2 修改app/Http/Kernel.php,在protected $routeMiddleware数组里加入’jsoncors’ => \App\Http\Middleware\JsonCors::class,然后在路由里引用test中间件即可。3 在路由中引用中间件Route::middleware([‘jsoncors’])4 有关跨域控制的更多知识请访问HTTP访问控制(CORS)。钻牛角尖:如果需要对程序返回数据作统一加工,都可以通过middleware实现更灵活的响应管理?

January 29, 2019 · 1 min · jiezi

php无限分类树扩展组件

PHP系统树图github地址dendrogram Laravel PHP v1.0 5.* >=5.6.4 安装composer require dendrogram/dendrogram:v1.0配置首先往Laravel应用中注册ServiceProvider,打开文件config/app.php,在providers中添加一项:‘providers’ => [ DenDroGram\DendrogramServiceProvider::class ]然后发布拓展包的配置文件,使用如下命令:php artisan vendor:publish此时config目录下会生成dendrogram.php配置文件数据导入(两表三个自定义函数)php artisan migrateadjacency结构 以父节点为基准的链式查询 增删容易 查询不便nested结构 以左右值包容形式 增删不便 查询容易图片描述方法说明调用 构造参数 方法说明 方法参数 返回 备注 (new DenDroGram(AdjacencyList::class))->buildTree($node_id,[’name’]) adjacency数据格式 adjacency格式数据生成目录式结构树 根节点id , 每个节点显示信息 返回html文本string 视图的相关在dendrogram.php中配置 如操作节点方法的路由 (new DenDroGram(AdjacencyList::class))->operateNode($action,$data) adjacency数据格式 adjacency格式数据的节点操作 action增删改标识 , data节点详情数据 返回boolean 注意视图与之对应的数据结构AdjacencyList::class (new DenDroGram(AdjacencyList::class))->getTreeData($node_id); adjacency数据格式 adjacency数据构造成多维数组 根节点id 返回array 多维数组结构 (new DenDroGram(NestedSet::class))->buildTree($node_id,[’name’]) NestedSet数据格式 NestedSet格式数据生成根茎式结构树 根节点id , 每个节点显示信息 返回html文本string 视图的相关在dendrogram.php中配置 如操作节点方法的路由 (new DenDroGram(NestedSet::class))->operateNode($action,$data) NestedSet数据格式 NestedSet格式数据的节点操作 action增删改标识 , data节点详情数据 返回boolean 注意视图与之对应的数据结构NestedSet::class (new DenDroGram(NestedSet::class))->getTreeData($node_id); NestedSet数据格式 NestedSet数据构造成多维数组 根节点id 返回array 多维数组结构 举个栗子adjacency数据结构生成的视图图片描述nested数据结构生成的视图 ...

January 28, 2019 · 1 min · jiezi

laravel 任务调度实战 数据库备份

我们要一分钟备份一次数据库。让我们开始吧。创建命令文件php artisan make:comman BackupDatabase打开刚刚创建的文件,并修改为以下内容:<?phpnamespace App\Console\Commands;use Illuminate\Console\Command;use Symfony\Component\Process\Exception\ProcessFailedException;use Symfony\Component\Process\Process;class BackupDatabase extends Command{ /** * The name and signature of the console command. * * @var string / protected $signature = ‘db:backup’; /* * The console command description. * * @var string / protected $description = ‘Backup the database’; protected $process; /* * Create a new command instance. * * @return void / public function __construct() { parent::__construct(); $file_name = date(‘Y-m-d-H:i:s’) . ‘-’ . config(‘database.connections.mysql.database’) . ‘.sql’; $this->process = new Process(sprintf(‘mysqldump -u%s –password=%s %s > %s’, config(‘database.connections.mysql.username’), config(‘database.connections.mysql.password’), config(‘database.connections.mysql.database’), storage_path(‘backups/’ . $file_name) )); } /* * Execute the console command. * * @return mixed / public function handle() { try { $this->process->mustRun(); $this->info(‘The backup has been proceed successfully.’); } catch (ProcessFailedException $exception) { $this->error($exception); } }}配置命令在storage创建一个backups文件夹,打开app/Console/Kernel.php修改schedule方法,如下protected function schedule(Schedule $schedule) { $schedule->command(‘db:backup’) ->everyMinute(); }服务器配置进入服务器 执行crontab -e如果是第一次打开crontab的话,会让你选择编辑器,这里(选vim)就可以了,我选的第三个。但是如果你选错了,就可能会遇到点麻烦,没有办法正常编辑,crontab -e。 怎么办?执行这个命令:select-editor (针对crontab的一个命令), 可以让你重新选一次。复制如下内容 * * * * php /home/vagrant/code/laravel/artisan schedule:run >> /dev/null 2>&1/home/vagrant/code/laravel/ 是项目的目录一分钟后可以检查storage/backups文件夹内是否有生成备份的sql文件。 ...

January 25, 2019 · 1 min · jiezi

利用 Laravel Resources 来整合第三方 API 数据

对于某些应用程序,可能需要第三方服务或者 API 来提取某些数据,将该数据转换为所需的响应,并将其传送到客户端界面。为此,我们需要找到一种方法,方便从控制器发送到视图或最终用户界面的数据保持一致性。因此,可能需要构建一个代表应用程序中所需资源的新对象或类。您或许可能会想『为什么我需要它?』,因为,您不希望在应用程序中公开所有的 API 响应数据,此外,你可能需要转换该响应的某些字段等。在本文中,我将向您展示一种简单的方法,将来自第三方 API 传入的数据转换为应用程序中的资源,以帮您保持一致性。在进一步讨论之前:在这篇文章中,我假设您至少已经基本了解了什么是 API 以及该如何使用 API ,如何使用 Laravel 框架及其某些组件作为 Eloquent ORM 。 如果你不知道上面的文章大概在说明写什么,你可能会发现一些挑战性的概念,但是,嘿,不要气馁,我相信你会发现这篇文章会给你带来一定的价值。一些关于 “Laravel resources” 的消息’API Resources’ 在 Laravel 5.5 中引入,作为是“将您的模型和模型集合表达并轻松转换为 JSON 数据格式”的一种方式。虽然这是官方的说明,并且您发现此部分在官方网站的 Eloquent 文档上没有此目录索引,但您必须知道这些资源并未严格附加到 Eloquent ORM 当中。在最基本的意义上来说,Eloquent 允许您将给指定对象转换为不同的对象。<?phpnamespace App\Http\Resources;use Illuminate\Http\Resources\Json\Resource;class UserResource extends Resource{ /** * 将资源转换为数组。 * * @param \Illuminate\Http\Request * @return array */ public function toArray($request) { return [ ‘id’ => $this->id, ’name’ => $this->name, ’email’ => $this->email, ‘created_at’ => $this->created_at, ‘updated_at’ => $this->updated_at, ]; }}您可以通过阅读官方文档了解有关 Resources 的所有信息:Eloquent: API Resources使用第三方 API在使用第三方 API 时,您需要找到一种方法将传入的响应数据转换为结构一致的数据。有关 Laravel 的最新消息:不久前 Eric L. Barnes 发表了一篇文章,描述了他如何使用 Laravel 为 laravel-news 网站建立一个前端页面,然后用 WordPress 作为后端并从 WordPress API 读取数据。你可以点击这里查看所有文章。 https://laravel-news.com/word…因此,以具体案例为例。 假设您的应用程序中有一个 WordPress 存储库,它从 WordPress API 中提取数据。<?phpclass WordpressRepository { pubic function getPost($id) { $response = $this->apiClient->get( ‘post’, $query = [‘id’ => $id] ); // return as array return json_decode($response, true); }}假设您从 WordPress API 接收此对象(数据)// wordpress version 0.1{ ID: 123 post_title: “some title” post_content: “some content”, post_author: “joe”, publish_date: “01-01-2001”}您可以将此响应包装到一个数组中,然后在所有控制器或视图上使用此数据。响应格式一致性不妨想一想,如果 WordPress 的 API 更新了怎么办。假如新版本会返回一个不同格式的数据。// wordpress version 0.1{ post_id: 123 title: “some title” content: “some content”, author: “joe”, date: “01-01-2001”}那么你就需要在项目的多个位置把 $post[‘post_title’] 替换成 $post[’title’] 。使用中间件来处理响应数据可以确保数据库的一致性。当响应的格式增加时,你只需要更新某段代码即可。使用 API 资源批量处理数据正如我之前提到的,你可以使用没有Eloquent的 「Resources」,下面就是一个很好的例子。您需要做的第一件事是创建一个新的「Post」资源; 使用 artisan:$ php artisan make:resource Post<?phpnamespace App\Resources;use Illuminate\Http\Resources\Json\Resource;class Post extends Resource{ public function toArray($request) { return [ ’title’ => $this->resource[’title’], ‘content’ => $this->resource[‘content’], ‘slug’ => $this->resource[‘slug’] ]; }}返回单个资源实例现在可以参照相同的例子,在你的 API 容器类中,你可以创建一个此资源新的实例,然后使用 resolve() 方法来返回转换后的对象(这将返回一个数组)。<?phpclass WordpressRepository { pubic function getPost($id) { $response = $this->apiClient->get( ‘post’, $query = [‘id’ => $id] ); $data = json_decode($response, true); return Post::make($data)->resolve(); }}返回数据集合我们可以创建一个专用的资源类 「PostCollection」。$ php artisan make:resource PostCollection<?phpnamespace App\PublisherPlus\Resources;use Illuminate\Http\Resources\Json\ResourceCollection;class PostCollection extends ResourceCollection{ public function toArray($request) { return [ ‘data’ => $this->collection ->map ->toArray($request) ->all(), ’links’ => [ ‘self’ => ’link-value’, ], ]; }}在上面的例子中,data 将会包含一个 Posts 数组,该数组的结构跟你在 Post 资源中定义的一样。你可以在这里了解更多关于 「resource collections」 的信息。API 资源总结!因此,如果你仔细研究 「resources」 的定义。你可以将其视为中间件,用于将已有数据转为新的、不同格式的对象或数组。更多翻译文章请见 PHP / Laravel 开发者社区 https://laravel-china.org/top… ...

January 25, 2019 · 2 min · jiezi

专为 Laravel 定制的 Visual Studio Code 编辑器

嗨 工匠,我从 Laravel4.1 到 5.4 一直再用它,我相信它仍然是最流行的PHP框架。它提供许多功能为快速开发 web 和 Api ,以及5.3支持 VueJs 前端开发。你也有很多神奇的功能在这吧?我已经尝试使用了很多编辑器如 sublime,phpstorm(在用vs code之前都用它),atom 和现在用的 visual studio code 。每个编辑器都有它各自的优点,但是我第一次试用 visual studio code 的时候,我印象它又酷有强大,特别在 Git 管理,Debug(下面有尝试)及各种扩展插件??设置 Laravel 的 Vscode 环境安装下面的插件:Auto Close Tag自动添加 HTML/XML 的闭合标签,像 Visual Studio IDE 或 Sublime Text 一样。 Beautify在 Visual Studio Code 中格式化 javascript 、JSON 、 CSS 、Sass,以及 HTML。 Better MergeVisual Studio Code 中非常好用的可视化合并冲突工具,灵感来自于 Atom 中的 merge-conflicts 插件。 Debugger For Chrome用于在谷歌浏览器中调试 JavaScript 代码的 VS Code 扩展,或支持 Chrome Debugging Protocol 其他功能。 Eslint此扩展使用安装在已打开的工作区文件夹内的 ESLint 库。如果文件夹没有提供这个库,将会匹配全局安装的版本。如果既没有局部安装、也没有全局安装 ESLint,可以通过运行npm install eslint 进行局部安装或者npm install -g eslint进行全局安装。 Npm此扩展支持定义在package.json文件里的 npm 脚本,并根据定义在package.json里的依赖项验证已安装的模块。 Laravel Blade SnippetsLaravel blade 代码片段和语法高亮支持 Visual Studio Code。 PHP Debug此扩展由 Derick Rethan 开发,是一个 VS Code 与 XDebug 之间的调试适配器。XDebug 是一个 PHP 扩展(Linux 下的.so文件或 Windows 下的.dll),需要安装在你的服务器上。 PHP Intellisense CraneCrane 是 Visual Studio Code 的生产力增强扩展,提供了 PHP 代码的自动完成。它具有零依赖性,并可以极大程度地工作于任何规模的项目里。它仍在开发中,可能存在 Bug 或缺失某些功能。 Git History使用图表查看 Git 历史,查看 commit 的详情信息,例如作者名、邮件、日期、提交者的作者名、邮件、日期和提交注释。查看先前文件的拷贝或者将其与工作区版本或先前版本进行比较,查看编辑器(Git Blame)里对活动行的更改。我使用的 Dracula 主题和 Material Icon Theme 图标主题,现在尝试使用 Vscode 在 laravel 里进行调试吧,运行得好吗?更多翻译文章请见 PHP / Laravel 开发者社区 https://laravel-china.org/top… ...

January 24, 2019 · 1 min · jiezi

无头浏览器测试可视化:Laravel Dusk 控制台入门指南

Laravel Dusk 控制台是一款 Laravel 扩展包,能够为你的 Dusk 测试套件提供漂亮的可视面板。通过它,你可以可视化运行 Dusk 测试时涉及的各个步骤,以及查看每个步骤的 DOM 快照。这对于调试浏览器测试、并搞清楚后台做了什么十分有用。同时,你还可以使用浏览器的调试工具来检查 DOM 快照。<img src=“https://user-gold-cdn.xitu.io…;h=728&f=png&s=505665” class=“rm-style”>除了可视面板,此扩展包还提供了 Laravel Dusk 测试监视器。在你对 Dusk 测试进行修改后,便会自动执行测试过程。该扩展包受到 Javascript 前端测试框架 —— Cypress 的强烈启发。查看本扩展包,请移步 GitHub 。什么是 Laravel Dusk?Laravel Dusk 提供了富有表现力的、易于使用的浏览器自动化和测试 API。使用 Laravel Dusk编写测试用例,像在真正的浏览器上一样。比如,当你想在网站上测试拖放功能时,想要测试Vue组件或其他与 Javascript 相关功能,那么你无法使用 Laravels HTTP 测试 API 本身进行测试。我认为 Laravel Dusk 是一个非常棒的软件包并且可以简化浏览器测试。以下是一个用户注册的示例测试,以便你可以了解 Laravel Dusk 的功能:public function test_can_register(){ $faker = Factory::create(); $this->browse(function($browser) use ($faker) { $password = $faker->password(9); $browser->visit(’/register’) ->assertSee(‘Register’) ->type(’name’, $faker->name) ->type(’email’, $faker->safeEmail) ->type(‘password’, $password) ->type(‘password_confirmation’, $password) ->press(‘Register’) ->assertPathIs(’/home’); });}要了解更多关于 Laravel Dusk 以及如何开始使用自己的浏览器测试的更多信息,请查看 官方文档。使用 Laravel Dusk 控制台在介绍 Laravel Dusk 控制台内部如何运行之前,让我们先瞄一眼如何在 Laravel 应用内安装并使用这个扩展包。如下步骤假定你已经按照 官方文档 成功地安装了 Laravel Dusk;或者甚至你已经写好了一些 Dusk 测试。首先,使用 Composer 安装本扩展包。composer require –dev beyondcode/dusk-dashboard接下来,打开 Laravel Dusk 生成的 DuskTestCase.php。你可以在 tests 目录里找到这个文件。请务必使用本扩展包的测试用例(Test case)作为基类,而不是 Laravel Dusk 的测试用例。稍后我再告诉你内部原理。找到此行:use Laravel\Dusk\TestCase as BaseTestCase;使用如下内容替换:use BeyondCode\DuskDashboard\Testing\TestCase as BaseTestCase;搞定。现在你可以使用如下命令启动 Laravel Dusk 控制台,并执行你的测试了。php artisan dusk:dashboard类似这样的界面便会展示在你的面前:<img src=“https://user-gold-cdn.xitu.io…;h=728&f=png&s=287914” class=“rm-style”>开始测试只需按下「Start Tests」按钮,即可运行 Laravel Dusk 测试,并观察到你的应用被测试时的输出,以及所发生的行为。随后,你便会看到 Dusk 测试产生的各种事件出现在你的控制台上。还有一种启动 Dusk 测试的方法是,只要编辑任意一个测试文件然后保存即可。Laravel Dusk 控制台内置了文件监视器。调试测试步骤你可以通过点击展示在列表中的测试行为,来调试和检查它们。点击后,你将会看到 DOM 快照,表示当此行为被记录时的 HTML 页面状态。若此行为以某种方式操作过 DOM,那么你也可以点击 「Before」和「After」按钮在事件发生「之前」或「之后」的 DOM 快照之间进行切换。如下,一个按下「Register」按钮的小例子:检查XHR请求有时候,查看运行测试时发生的有关 XHR 请求的其他信息可能会很有用。例如:你网站上又一个按钮,它将对某个服务端执行 GET 请求。Dusk Dashboard 允许您记录 XHR 事件,并显示响应状态和响应路径。默认情况下 XHR 请求检查不会启用,因为它需要你修改浏览器功能。要启用 XHR 的请求记录,打开你的 DuskTestCase.php ,在文件里,有个 driver 方法,用于设置不同测试操作的 WebDriver。由于此程序包需要对此驱动程序的功能进行一些调整,因此需要使用 $this->enableNetworkLogging 方法调用来封装 DesiredCapabilities 对象。protected function driver(){ $options = (new ChromeOptions)->addArguments([ ‘–disable-gpu’, ‘–headless’, ‘–window-size=1920,1080’, ]); return RemoteWebDriver::create( ‘http://localhost:9515’, $this->enableNetworkLogging( DesiredCapabilities::chrome()->setCapability( ChromeOptions::CAPABILITY, $options ) ) );}通过添加此功能,该程序包将启用记录 XHR 请求和响应信息所需的功能。工作原理基本思路十分简单:运行一个 WebSocket 服务,控制台用户连接到这个 WebSocket 服务,接着 PHPUnit 便会将浏览器事件和失败信息发送至所有 WebSocket 连接。以下是具体的实现方式:在内部,此扩展包向你的 Laravel 应用内添加了一个名为 StartDashboardCommand 的命令。当此命令被执行时,就会 启动 一个由 Ratchet 开发的 WebSocket 服务。最初我考虑基于我同 Freek 一起开发的 Laravel Websockets 实现此功能,然而随后就毙了这个想法。原因很简单,此扩展包仅能用作开发依赖项,并且我不需要 Pusher 或 Laravel 广播功能,因为广播是通过 PHPUnit 内部实现的。译者注:Freek 意指 Freek Van der Herten。另,截至目前,此扩展包也已经发布 v1.0.x 稳定版本。接下来,我添加两条路由到 WebSocket 服务。$dashboardRoute = new Route(’/dashboard’, [’_controller’ => new DashboardController()], [], [], null, [], [‘GET’]);$this->app->routes->add(‘dashboard’, $dashboardRoute);$eventRoute = new Route(’/events’, [’_controller’ => new EventController()], [], [], null, [], [‘POST’]);$this->app->routes->add(’events’, $eventRoute);$dashboardRoute 是一条普通 HTTP 控制器路由,用于输出 Laravel Dusk 控制台的 HTML 视图。就是这么简单,它只做一件事——返回 HTML 视图:class DashboardController extends Controller{ public function onOpen(ConnectionInterface $connection, RequestInterface $request = null) { $connection->send( str(new Response( 200, [‘Content-Type’ => ’text/html’], file_get_contents(DIR.’/../../../resources/views/index.html’) )) ); $connection->close(); }}$eventRoute 同样是一个 HTTP 路由,但只允许 POST 请求。它被用来在 PHPUnit 和 WebSocket 客户端之间通讯。同样十分简单,也只做一件事——接收 POST 数据,并广播给所有已连接的 WebSocket 客户端:class EventController extends Controller{ public function onOpen(ConnectionInterface $conn, RequestInterface $request = null) { try { /* * 如下即为从 PHPUnit 测试发来的 POST 数据, * 发送到已连接的客户端。 / foreach (Socket::$connections as $connection) { $connection->send($request->getBody()); } $conn->send(str(new Response(200))); } catch (Exception $e) { $conn->send(str(new Response(500, [], $e->getMessage()))); } $conn->close(); }}收集浏览器行为这是整个扩展包最乏味的部分。因为若想收集所有 Laravel Dusk 方法,并将它们广播到 WebSocket 连接,那么必须代理所有的消息再收集它们。在本扩展包自定义的 TestCase 类里,我们能够重写(override)浏览器实例被创建的过程。那么,此处就是我注入自定义的浏览器(Browser)类的地方。它负责代理现有方法并收集所有行为,同时转发给 WebSocket 连接。protected function newBrowser($driver){ return new Browser($driver);}没什么高端操作。接下来,我原本想直接创建一个新类,传给它 Laravel Dusk 的浏览器类,随后使用 __call 魔术方法代理所有的方法。这能够省下一大堆代码,但也会引出两个问题:用户无法使用 IDE 自动完成、方法提示功能。对我来说有点忍不了,我认为这是个非常重要的特性 —— 尤其是对于测试工具来说。开发者并不了解 API 的输入和输出,因此需要 IDE 的提示。另一个问题是,我不仅仅想在浏览器行为发生后记录 DOM 快照,在某些特定的行为发生前,同样想记录快照。所以这就是我为何不得不像下面这样,代理所有 Laravel Dusk 方法:/* @inheritdoc */public function assertTitle($title){ $this->actionCollector->collect(FUNCTION, func_get_args(), $this); return parent::assertTitle($title);}好了,这样我便能收集并记录各个行为,且依然维持着 IDE 自动完成功能。棒棒哒!现在你能看到这里的 actionCollector 是 PHPUnit 和 WebSocket 客户端之间的桥梁。它收集获得的信息,并用例如测试名称和 WebSocket POST 推送的端点数据来丰富它:protected function pushAction(string $name, array $payload){ try { $this->client->post(‘http://127.0.0.1:’.StartDashboardCommand::PORT.’/events’, [ RequestOptions::JSON => [ ‘channel’ => ‘dusk-dashboard’, ’name’ => $name, ‘data’ => $payload, ], ]); } catch (\Exception $e) { // Dusk-Dashboard 服务器可能是关闭的。不必惊慌。 }}它由 try-catch 包裹来保证即使在 Dusk Dashboard 服务器关闭时 Laravel Dusk 也能正常运行。UI 界面最后,值得注意的是,此扩展包在它的面板界面里也有很多说道。它由 TailwindCSS 和 Vue 驱动来展示到来的事件以及过滤它们等等。你可以在这 这 查看起始页面的代码。差不多就这些了。更多翻译文章请见 PHP / Laravel 开发者社区 https://laravel-china.org/top… ...

January 23, 2019 · 3 min · jiezi

Laravel 深入核心系列教程

前言年底了不太忙,最近一段时间也一直在研究laravel,就想写篇关于laravel比较深一点的教程系列啥的,于是就找到站长给开了写教程的渠道。由于第一次写,写的不好,还忘大家理解。如果看的过程中有什么疑问。都可以在帖子下留言,大家相互沟通。,希望您可以到github star 。^_^ github地址为什么选择laravel?,下面分别去拿优点缺点来对比。优点laravel的就是为 WEB 艺术家创造的 PHP 框架,它也是php工程化的趋势。社区非常完善,帖子质量都相对都比较高。基于composer构建,丰富的扩展包。github star在php分类排名第一缺点laravel性能不怎么样?比其他框架运行起来差几倍?laravel上手难?针对性能这块呢?我们可以思考一下,php能做的功能java,.net,甚至c++也都能做到吧,那为什么我们还要使用php开发呢,不就是因为php开发比较高效嘛。一个框架好不好不能只从性能上看,还要从开发效率和代码健壮,维护方面来看。laravel里面确实增加了很多实用的功能,在牺牲运行效率得前提下,但是对我们开发项目得效率提高了不少,各种composer包也比较完善。项目维护方便也是事实。等接触一段时间后你会发现laravel开发效率绝对碾压其他语言或框架。laravel上手难这个是肯定的,laravel采用了php比较新的特性,闭包等等,IOC容器,中间件,事件,通知前端模块整合等等。这些知识在大多数同学刚接触php时候也都不会涉及,所以会觉得上手难,但是想要提高写代码的水平,还是建议去学习下laravel,对自己的编程也会有一定的帮助。我相信学会laravel之后,其他的框架也都会随着你的的积累很容易上手的。你能学到什么?具体该教程涉及的知识,可以看下面的文章内容,每个章节都会有相对应的例子,由浅入深分析功能是怎么实现的。当然在看文章的前提你需要去懂php基础知识,面向对象和laravel的基本使用。如果有遇到问题可以直接在教程下面留言。文章内容规划[依赖注入,控制翻转,反射各个概念的理解和使用][如何实现Ioc容器和服务提供者是什么概念][Facades外观模式背后实现原理][Contracts契约之面向接口编程][中间件,管道之面向切面编程][Laravel生命周期][Laravel事件之观察者模式][新特性trait在Laravel中的应用][为什么laravel没有规划models目录?][Eloquent ORM中的方法find方法的实现流程][如果开发Laravel扩展包和发布到composer][Laravel与前端最佳实践][基于JWT的api认证][负载均衡,分布式,集群的理解,多台服务器代码如何同步][基于教程实现的一个简单网站实例]说明项目仅作为学习使用,代码在每个章节代码都会有相应的链接。会放到github上面,大家可以clone github下载源代码。

January 22, 2019 · 1 min · jiezi

分享 10 个你可能不知道的 Laravel Eloquent 小技巧

Laravel 是一个功能丰富的框架。但是,你无法从官方文档中找到所有可用的功能。以下是一些你可能不知道的功能。1.获取原始属性当修改一条 Eloquent 模型记录的时候你可以通过调用 getOriginal() 方法获取记录的原始属性$user = App\User::first();$user->name; //John$user->name = “Peter”; //Peter$user->getOriginal(’name’); //John$user->getOriginal(); //原始 $user 记录2. 检查模型是否被修改使用 isDirty() 方法确定模型或给定属性是否已被修改$user = App\User::first();$user->isDirty(); //false$user->name = “Peter”;$user->isDirty(); //true也可以检查指定属性是否被修改。$user->isDirty(’name’); //true$user->isDirty(‘age’); //false3. 获取更改的属性使用 getChanges() 获取更改的属性$user->getChanges()//[ “name” => “Peter”, ]注:仅当您使用 syncChanges() 保存模型或同步更新时,才生效4. 定义 deleted_at 字段默认情况下,Laravel使用deleted_at字段处理软删除。 您可以通过定义DELETED_AT属性来更改它。class User extends Model{ use SoftDeletes; * The name of the “deleted at” column. * * @var string */ const DELETED_AT = ‘is_deleted’;}或者定义访问class User extends Model{ use SoftDeletes; public function getDeletedAtColumn() { return ‘is_deleted’; }}5. 保存模型和关系您可以使用push()方法保存模型及其关联。class User extends Model{ public function phone() { return $this->hasOne(‘App\Phone’); }}$user = User::first();$user->name = “Peter”;$user->phone->number = ‘1234567890’;$user->push(); // 这将更新数据库中的用户和电话6. 重新加载模型使用 fresh() 重新从数据库加载一个模型。$user = App\User::first();$user->name; // John// user 表被其他进程修改。 例:数据库又插入一条 “name” 为 “Peter” 的数据。$updatedUser = $user->fresh();$updatedUser->name; // Peter$user->name; // John7. 重新加载现有模型你可以使用 refresh() 方法从数据库重新加载具有新值的现有模型。$user = App\User::first();$user->name; // John// user 表被其他进程修改。例: “name” 被修改为 “Peter” 。$user->refresh();$user->name; // Peter注: refresh() 也会更新模型的关联模型数据。8. 检查模型是否为同一个使用 is() 方法确定两个模型是否拥有相同主键并且属于同一张表。$user = App\User::find(1);$sameUser = App\User::find(1);$diffUser = App\User::find(2);$user->is($sameUser); // true$user->is($diffUser); // false9. 克隆一个模型你可以使用 replicate() 方法来复制一个模型到一个新的对象中。$user = App\User::find(1);$newUser = $user->replicate();$newUser->save();10. 在 find() 方法中指定查找的属性当使用 find() 或 findOrFail() 方法时,传入第二个参数可以指定需要查找的属性。$user = App\User::find(1, [’name’, ‘age’]);$user = App\User::findOrFail(1, [’name’, ‘age’]);如果你发现这篇文章有帮助,通过点赞来表达你的喜欢。也很乐意听到你对此的看法和想法。你可以在 Twitter 上找到我。转自 PHP / Laravel 开发者社区 https://laravel-china.org/top… ...

January 22, 2019 · 1 min · jiezi

刚接触一个 Laravel 项目,你可以从这些地方入手

当你接手一个新项目的时候,可能会感到无从下手,如果不熟悉编程,则更是如此。那么,我们该从哪儿入手呢?项目代码的哪些部分我们需要着重了解?下面我们看看 Laravel 项目的几个通用的部分。项目文档面对新项目时,文档可能是最有帮助的。如果项目包含文档,恭喜你,你非常幸运。但是,也别高兴地太早,因为文档可能早已经过时或覆盖不全面。项目文档通常编写在 readme 文件中、wiki,或者发布在 Confluence 和 Google Docs 之类共享平台上。如果你基于一个项目做开发,不要犹如,请积极的为项目文档做贡献:补充空白部分或者使其表达得更清晰明了。如果你不够幸运的话(大多数时候都是如此),你接触的项目没有任何文档。缺少文档并不完全是一件坏事,因为在这种情况下,你有机会亲自为你的团队撰写文档。你和你的同事,以及你带来的新开发者,都将会在未来对你感激不尽。撰写文档确实不是一件有趣的工作,但它对于保持项目的长期运行是很有必要的。项目文档不仅要列举使用的技术和初始安装方法,同时也应该阐述项目 “为什么这样” 以及 “如何进行” ,这通常不能清晰地用代码自身表达出来。某些高层次的设计选择及其原因也应该被写入文档,以帮助更好地理解代码。composer.jsonComposer 是一个 PHP 包管理工具,在过去的几年中帮助推动了 PHP 生态系统的快速前进。 Laravel 从版本4开始使用 Composer ,所以在项目基本都存在 composer.json 文件。你能够在项目根目录下找到 composer.json 文件和 composer.lock 文件。lock 文件包含了项目中所需要的所有依赖包的准确版本,而 JSON 文件显示了依赖包的发布内容。目前,我们只对 JSON 文件中的版本信息感兴趣,如果你想学习这些文件的更多知识,可以阅读 这里。在浏览 composer.json 文件时,注意到有一个 require 区块,看起来内容类似如下所示。{ “require”: { “php”: “>=7.1.3”, “fideloper/proxy”: “~4.0”, “laravel/framework”: “5.6.”, “laravel/tinker”: “~1.0” }}在这个样例中,我们有一个基于 Laravel 5.6 的项目。它同时依赖于另外两个包,以及不低于7.1.3版本的 PHP 。在你的项目中,你很可能会看到更多依赖包,并且版本号可能会有所变化。现在你知道了项目中依赖了哪些扩展包,去搞明白它们各自的功能。我推荐从 Laravel 依赖开始,因为它们拥有详细的文档。且文档就发布在网络上,很容易就能找到:https://laravel.com/docs/{VERSION} 和 https://laravel.com/api/{VERSION},如下这种链接 https://laravel.com/docs/5.6>…。文档 docs 对 laravel 功能及各个主要部分的工作原理作了比较全面的介绍。同时 api 文档将 laravel 框架中所用到的类及方法以清单的形式呈现出来。在查看了 Laravel 文档之后,可以继续查看其它依赖的文档。你可以前往 Packagist (这是 Composer 所使用的扩展包仓库)获取关于依赖的更多信息,各扩展对应的地址为https://packagist.org/packages/{VENDOR}/{PACKAGE},比如 https://packagist.org/package…。在每一个 Packagist 的项目主页上,展示了扩展包的介绍、版本号、仓库地址(如 GitHub)、完整的 readme 文件,以及其他一些有用的信息。从项目主页上获得的信息足够使你了解这个扩展包是什么,在你的项目中又承担哪部分功能。通过这种方式,继续去了解你项目应用的 composer.json 文件中所罗列出的其他依赖。路由路由是应用某个具体功能的入口。路由表现为一个链接,浏览器访问链接时,最终由绑定的控制器或闭包来处理。由路由找到具体对应的控制器,就能清楚控制器所依赖的其他模块以及实现的具体功能。遇到新的路由,继续重复这一动作,就能逐步搞清楚整个应用是怎么工作的。你可以在项目的如下位置找到路由配置文件:Laravel 5.3+ routes/.phpLaravel 5.0-5.2 app/Http/routes.phpLaravel 4.2 app/routes.php路由 “陷阱"某些时候,根据具体 URL 定位路由需要费些脑子。比如 URI /users/123/profile。你可能想要去搜索 users/{id}/profile 的路由定义。事实上,它是定义在 路由分组 中,这使得路由比较难定位。Route::prefix(‘users’)->group(function () { Route::get(’{id}/profile’, ‘UsersController@profile’);});在这个例子中,users 和 {id}/profile 并没有被写在一起,这是难以定位的原因。如果路由不多,还能比较轻易的找出。但是,当路由文件有成百上千条定义时,这将会变得非常困难。另外一个坑是 Route::resource() (还有新版本中的 Route::apiResource())。Route::resource() 将自动根据指定参数生成路由。举个例子,在路由文件中添加代码 Route::resource(‘dogs’, ‘DogController’); 将完成与下述代码相同的功能。Route::group([‘prefix’ => ‘dogs’], function () { Route::get(’/’, ‘DogsController@index’)->name(‘dogs.index’); Route::get(‘create’, ‘DogsController@create’)->name(‘dogs.create’); Route::post(’/’, ‘DogsController@store’)->name(‘dogs.store’); Route::get(’{id}’, ‘DogsController@show’)->name(‘dogs.show’); Route::get(’{id}/edit’, ‘DogsController@edit’)->name(‘dogs.edit’); Route::put(’{id}’, ‘DogsController@update’)->name(‘dogs.update’); Route::delete(’{id}’, ‘DogsController@destroy’)->name(‘dogs.destroy’);});然而,如果你尝试查找类似 dogs/{id}/edit 的内容,这是找不到的,因为它的定义是作为 Route::resource() 的其中一部分。有时通过 Route::resource() 方式直接定义路由是挺方便的,但我更倾向于单独地定义每一个路由,这样能使每个 URI 更容易被直接搜索到。了解更多路由资源和资源控制器的相关信息,可以查阅这些 文档 。预览项目中的所有路由的最简单方式是使用 artisan 命令 route:list :php artisan route:listroute:list 命令提供了每个路由的完整细节,包括 HTTP 请求方式,具体的 URI ,路由名称,动作信息(也就是控制器及其方法),以及为每个路由配置的中间件信息。服务提供者服务提供者是 Laravel 释放魔法之地。 官方文档 给出了总结:服务提供者是所有 Laravel 应用程序引导中心。你的应用程序以及 Laravel 的所有核心服务都是通过服务提供器进行引导。在这里,我们说的「引导」其实是指注册,比如注册服务容器绑定、事件监听器、中间件,甚至是路由的注册。服务提供者是配置你应用程序的中心。你可以浏览位于 app/providers 目录下的所有应用程序服务提供者。围绕应用自定义增加的相关代码,理应在这里。例如,一些情况下要查找视图合成器,宏,并做配置调整。在旧版本的 Laravel 中,如 4.2,你会在 global.php 文件中发现类似的功能,因为那时服务提供者通常只在包中使用。测试代码库包含的测试套件能向你展示应用程序如何工作以及接下来的响应。对应用的边界处理情况,它可以提供有价值的线索。当然,就像代码库文档一样,应用配套的测试文件有可能不存在,或者很少,甚至是无用的过时文件。同写项目文档一样,写应用配套测试同样可以更好的学习项目应用,提升代码质量。你可能偶然发现并修复一些缺陷,移除无用的代码,或者为项目中重要的类新增测试覆盖。利器对 Laravel 开发者而言,Barry vd. Heuvel 发布的 Laravel Debugbar 是值得拥有的调试和追溯工具。它功能强大,安装便易。可以将应用程序中所发生的事情一览无余:经过的路由和控制器,数据库查询和执行时间,数据展示,异常,查看执行内容和执行过程时间线等等。尝试过使用这个包后,你将在之后的 Laravel 应用开发中对它爱不释手。尾声在这篇文章中,我提出了一些方法,方便你很快上手新的 Laravel 项目代码。这篇文章并非一份包含所有细节的清单,只是一个起步。我鼓励你使用这些建议,看看它能把你带到哪里。如果您有任何交流的想法,我很乐意听到它们!欢迎随时联系 Twitter。转自 PHP / Laravel 开发者社区 https://laravel-china.org/top… ...

January 18, 2019 · 1 min · jiezi

laravel-admin表格数据来源(来自复杂的sql查询)

//数据来自以下两种情况都可以 $behind = time()-606024*3; $grid->model()->where([ [‘approve_time’,null],[‘created_at’,’<’,$behind] //第一种:业务员提交放款申请是否超过三天未处理 ])->orWhere(function ($query)use($behind){ //第二种:提交放款申请银行是否超过三天未处理 $query->where([ [‘approve_time’,’!=’,null], [‘approve_time’,’<’,$behind] ])->whereNotIn(‘apply_id’, function ($query){ $query->select(‘apply_id’) ->from(‘make_loan_log’); })->orWhere(function ($query){ $query->whereNotIn(’loan_id’, function ($query){ $query->select(’loan_id’) ->from(‘make_loan_log’); }); }); }); 以上语句表示:第一种:业务员提交放款申请是否超过三天未处理查询apply_make_loan的approve_time为空时表示处于未审核状态再用fcreated_at时间和当前时间对比是否超过三天;第二种:提交放款申请银行是否超过三天未处理查询apply_make_loan表字段approve_time不为空时先用approve_time时间和当前时间对比是否超过三天如果超过再用apply_id和loan_id连表make_loan_log查询是否有一条相应的记录如果没有,表示提交放款超三天未处理

January 17, 2019 · 1 min · jiezi

在 Laravel 应用中构建 GraphQL API

代码示例:产品列表和用户列表的 API 例子昨天我们学习了 在 Visual Code 中搭建 Laravel 环境,现在我们来学习 Facebook 的 GraphQL 。GraphQL 是一种 API 查询语言,还是一种根据你为数据定义的类型系统执行查询的服务器端运行时。GraphQL 不依赖于任何指定的数据库或存储引擎,而是由你的代码和数据来作支持的。 graphql.orgGraphQL 可以提升 API 调用的灵活性,我们可以像写数据库查询语句一样来请求 API 来获取所需要的数据,这对构建复杂的 API 查询来说非常有用。GraphQL 还提供了可视化界面来帮助我们编写查询语句,还提供了自动补全的功能,这让编写查询更加简单。https://github.com/graphql/gr…从以下图片可以看出,GraphQL 和 Rest 一样都是运行在业务逻辑层以外的:开始1. 安装 Laravel使用下面命令安装最新版本的 Laravel :# 在命令行中执行composer global require “laravel/installer"laravel new laravel-graphql2. 添加 GraphQL 的包使用 composer 安装 graphql-laravel,这个包提供了非常多的功能用于整合 Laravel 和 GraphQL 。3. 创建模型像下面这样创建模型和表 user_profiles, products, product_images,别忘了还要创建模型间的关系。4. 创建查询和定义 GraphQL 的类型GraphQL 中的查询与 Restful API 中的末端路径查询是一样的,查询只是用于获取数据,以及创建、更新、删除操作。我们把它称作 Mutation 。GraphQL 中的 类型 用于定义查询中每个字段的类型定义,类型会帮助我们格式化查询结果中的有格式的字段,例如布尔类型,字符串类型,浮点类型,整数类型等等,以及我们的自定义类型。下面是查询和类型的目录结构:这是 UsersQuery.php 和 UsersType.php 文件完整的源代码:<?phpnamespace App\GraphQL\Query;use App\User;use GraphQL\Type\Definition\Type;use Rebing\GraphQL\Support\Facades\GraphQL;use Rebing\GraphQL\Support\Query;use Rebing\GraphQL\Support\SelectFields;class UsersQuery extends Query{ protected $attributes = [ ’name’ => ‘Users Query’, ‘description’ => ‘A query of users’ ]; public function type() { // 带分页效果的查询结果 return GraphQL::paginate(‘users’); } // 过滤查询的参数 public function args() { return [ ‘id’ => [ ’name’ => ‘id’, ’type’ => Type::int() ], ’email’ => [ ’name’ => ’email’, ’type’ => Type::string() ] ]; } public function resolve($root, $args, SelectFields $fields) { $where = function ($query) use ($args) { if (isset($args[‘id’])) { $query->where(‘id’,$args[‘id’]); } if (isset($args[’email’])) { $query->where(’email’,$args[’email’]); } }; $user = User::with(array_keys($fields->getRelations())) ->where($where) ->select($fields->getSelect()) ->paginate(); return $user; }}<?phpnamespace App\GraphQL\Type;use App\User;use GraphQL\Type\Definition\Type;use Rebing\GraphQL\Support\Facades\GraphQL;use Rebing\GraphQL\Support\Type as GraphQLType;class UsersType extends GraphQLType{ protected $attributes = [ ’name’ => ‘Users’, ‘description’ => ‘A type’, ‘model’ => User::class, // 定义用户类型的数据模型 ]; // 定义字段的类型 public function fields() { return [ ‘id’ => [ ’type’ => Type::nonNull(Type::int()), ‘description’ => ‘The id of the user’ ], ’email’ => [ ’type’ => Type::string(), ‘description’ => ‘The email of user’ ], ’name’ => [ ’type’ => Type::string(), ‘description’ => ‘The name of the user’ ], // 数据模型 user_profiles 中的关联字段 ‘user_profiles’ => [ ’type’ => GraphQL::type(‘user_profiles’), ‘description’ => ‘The profile of the user’ ] ]; } protected function resolveEmailField($root, $args) { return strtolower($root->email); }}在编写完查询语句和类型之后,我们需要编辑 config/graphql.php 文件,将查询语句和类型注册到 Schema 中。<?phpuse App\GraphQL\Query\ProductsQuery;use App\GraphQL\Query\UsersQuery;use App\GraphQL\Type\ProductImagesType;use App\GraphQL\Type\ProductsType;use App\GraphQL\Type\UserProfilesType;use App\GraphQL\Type\UsersType;return [ ‘prefix’ => ‘graphql’, ‘routes’ => ‘query/{graphql_schema?}’, ‘controllers’ => \Rebing\GraphQL\GraphQLController::class . ‘@query’, ‘middleware’ => [], ‘default_schema’ => ‘default’, // 注册查询命令 ‘schemas’ => [ ‘default’ => [ ‘query’ => [ ‘users’ => UsersQuery::class, ‘products’ => ProductsQuery::class, ], ‘mutation’ => [ ], ‘middleware’ => [] ], ], // 注册类型 ’types’ => [ ‘product_images’ => ProductImagesType::class, ‘products’ => ProductsType::class, ‘user_profiles’ => UserProfilesType::class, ‘users’ => UsersType::class, ], ’error_formatter’ => [’\Rebing\GraphQL\GraphQL’, ‘formatError’], ‘params_key’ => ‘params’];5. Testing我们可以使用 GraphiQL 来十分简单地编写查询语句,因为在编写的时候它可以自动补全,或者我们也可以使用 postman 来请求 API,下面是自动补全的示例:下面是查询结果的示例如果你想查阅源代码,可以访问以下地址 :)。https://github.com/ardani/lar…转自 PHP / Laravel 开发者社区 https://laravel-china.org/top… ...

January 16, 2019 · 2 min · jiezi

lar-trace为服务之间调用提供链路追踪

https://github.com/laravelclo...Laravel Version CompatibilityLaravel 5.x.x is supported in the most recent version (composer require laravelcloud/lar-trace)Installation安装 Laravel 5.x安装laravelcloud/lar-trace包:$ composer require laravelcloud/lar-trace在 config/app.php 中做如下配置’providers’ => array( /* * Package Service Providers… */ LaravelCloud\Trace\TraceLaravel\TracingServiceProvider::class,)创建Trace的配置文件(config/trace.php)$ php artisan vendor:publish –provider=“LaravelCloud\Trace\TraceLaravel\TracingServiceProvider"添加变量至.envTRACE_ENABLED=1TRACE_ENDPOINT_URL=http://127.0.0.1:9411/api/v2/spansTRACE_RATE=1.0TRACE_SERVICE_NAME=lar-examplesTRACE_SQL_BINDINGS=falseLumen 5.x…链路追踪系统阿里云-链路追踪zipkinContributingDependencies are managed through composer:$ composer installTests can then be run via phpunit:$ vendor/bin/phpunitCommunityBug TrackerCode

January 16, 2019 · 1 min · jiezi

Laravel 5.7 最佳实践和开发技巧分享

Laravel 因可编写出干净,可用可调试的代码而为广大的 PHP 开发者所熟知。它同样也支持许许多多的功能,有时却未能在文档中体现,或者由于某种原因它们出现过又被移除了。我已经在生产环境中使用 Laravel 2 年了,从中我学到如何把代码变得更好,从我首次使用它以来我都充分发掘它的优势。接下来我将向你展示一些可能对你在用 Laravel 写代码时很有帮助的奥义之招。*查询数据时使用本地范围Laravel 有一种非常棒的方式来使用 查询构造器 编写查询。就像这样:$orders = Order::where(‘status’, ‘delivered’)->where(‘paid’, true)->get();很不错。这让我专注于编写更友好的代码而不是 SQL 语句。但如果用 本地范围 ,我们可以让这行代码变得更好些。当查询数据时, 本地范围 允许我们创建自己的 查询构造器 链式方法。举个例子,取代 ->where() ,我们可以用更简洁的 ->delivered() 和 ->paid() 。首先在 Order 模型,我们加入一些方法:class Order extends Model{ … public function scopeDelivered($query) { return $query->where(‘status’, ‘delivered’); } public function scopePaid($query) { return $query->where(‘paid’, true); }}当声明本地范围时,你应该使用 scope[Something] 来命名。这样 Laravel 便会知道这是一个本地范围并且可以在查询构造器中使用。请确保你在方法中传入了第一个参数 $query,也就是由 Laravel 自动注入的查询构造器实例。$orders = Order::delivered()->paid()->get();对于可接受额外参数的查询,你可以使用动态范围。每个范围都允许你传入额外的参数。class Order extends Model{ … public function scopeStatus($query, string $status) { return $query->where(‘status’, $status); }}$orders = Order::status(‘delivered’)->paid()->get();在本文的后面,你会知道为什么数据库字段应该使用 蛇形命名,但这里有第一个原因:Laravel 默认用 where[Something] 来替换 scope[Something] 。所以作为 scopeStatus 范围的代替,你可以这样做:Order::whereStatus(‘delivered’)->paid()->get();对于 where[Something] ,Laravel 会搜索 蛇形命名 版本的数据库字段。如果你的数据库中有个 status 字段,你可以用上面那个例子。如果有个 shipping_status 字段,你可以用:Order::whereShippingStatus(‘delivered’)->paid()->get();由你决定!必要的时候使用请求类Laravel 提供了一种优秀的方式来验证表单提交的数据。如果你需要它,不管是 POST 还是 GET 请求,它都可以验证。在控制器中,你可以这样做:public function store(Request $request){ $validatedData = $request->validate([ ’title’ => ‘required|unique:posts|max:255’, ‘body’ => ‘required’, ]); // 如果这篇博客的内容无效……}但是当控制器中已经有很多代码时,再把验证表单数据的代码加进去就会显得很凌乱。你想尽可能地减少控制器的代码 —— 至少这是我在控制器中写很多逻辑时想到的第一件事。Laravel 提供了一种很萌的方式来验证表单请求,那就是创建并使用专门的 请求类 而不是用原始的 Request 。你只需要创建你的请求类:php artisan make:request StoreBlogPost在 app/Http/Requests/ 目录中可以找到你刚创建的请求类:class StoreBlogPostRequest extends FormRequest{ public function authorize() { return $this->user()->can(‘create.posts’); } public function rules() { return [ ’title’ => ‘required|unique:posts|max:255’, ‘body’ => ‘required’, ]; }}现在,你应该用新创建的 App\Http\Requests\StoreBlogPostRequest 来代替原先的 Illuminate\Http\Request 类:use App\Http\Requests\StoreBlogPostRequest;public function store(StoreBlogPostRequest $request){ // 如果这篇博客的内容无效……}请求类中的 authorize() 方法应返回一个布尔值。如果返回了 false,它会抛出一个 403 异常,请确保你在 app/Exceptions/Handler.php 的 render() 方法中捕获了这个异常:public function render($request, Exception $exception){ if ($exception instanceof \Illuminate\Auth\Access\AuthorizationException) { // } return parent::render($request, $exception);}请求类中还有一个 messages() 方法,当验证失败时,它会返回一个包含了错误信息的数组:class StoreBlogPostRequest extends FormRequest{ public function authorize() { return $this->user()->can(‘create.posts’); } public function rules() { return [ ’title’ => ‘required|unique:posts|max:255’, ‘body’ => ‘required’, ]; } public function messages() { return [ ’title.required’ => ‘The title is required.’, ’title.unique’ => ‘The post title already exists.’, … ]; }}@if ($errors->any()) @foreach ($errors->all() as $error) {{ $error }} @endforeach@endif如果你想得到某个字段的验证信息,你可以这样做(当这个字段验证通过时 $errors->has() 会返回一个 false):<input type=“text” name=“title” />@if ($errors->has(’title’)) <label class=“error”>{{ $errors->first(’title’) }}</label>@endif魔术范围构建查询时,可以使用已有的魔术范围:根据 created_at 倒序查询:User::latest()->get();根据任意字段倒序查询:User::latest(’last_login_at’)->get();随机查询(即 SQL 语句中的 ORDER BY RAND())User::inRandomOrder()->get();使用关联关系代替冗长的查询(或者写得不好的查询)你是否曾经为了获取更多的信息而在查询语句中使用大量的 join 操作?即使在使用查询构造器的情况下,编写这样的 SQL 语句也是困难的,但是数据模型已经使用 关联关系 来实现同样的功能。由于文档提供了太多的信息,因此刚开始时你可能对关联关系并不熟悉,但是这些内容可以帮助你更好的理解事物的运行原理,同时让你的程序运行得更加顺畅。通过 这里 查询关联关系的文档。为耗时的任务使用任务系统Laravel 的任务 是后台运行程序必用的功能强大的工具。你要发送电子邮件? 任务系统。你要广播一个消息? 任务系统。你要处理一张图片? 任务系统。任务系统能够帮助你实现,在执行上述这些任务时,减少你的用户的应用加载时间。这些任务可以被放进命名的队列,它们能够被安排优先级,Laravel 几乎在所有可能的地方都实现了队列:无论在后台执行一些 PHP 任务,或者发送消息,或者广播事件,队列都在这些场景中出现。你可以在 这里 查询队列的文档。在使用队列时,我喜欢使用 Laravel Horizon ,因为它很容易安装,它能够通过 Supervisor 工具或者配置文件实现后台运行,同时我能够告诉 Horizon 我希望每个队列产生多少个进程。遵守数据库标准 & 访问器Laravel 从一开始就教给你变量和方法应使用像 $camelCase camelCase() 这样的小驼峰命名而数据库字段应使用像 snake_case 这样的蛇形命名。为什么呢?因为这有助于我们构造更好的 访问器。访问器是可以直接在模型中构造的自定义字段。如果我们的数据库包含了 first_name、last_name、age 这几个字段,我们可以增加一个叫做 name 的自定义字段来把 first_name 和 last_name 拼接起来。别担心,这个 name 不会被写入到数据库。它只是某个模型的自定义属性。所有的访问器,和 范围 一样,都有自定义命名语法:getSomethingAttribute:class User extends Model{ … public function getNameAttribute(): string { return $this->first_name.’ ‘.$this->last_name; }}当使用 $user->name,访问器会返回拼接好的字符串。*默认情况下,用 dd($user) 是看不到 name 属性的,但是通过 $appends 变量我们可以使它一直可用:class User extends Model{ protected $appends = [ ’name’, ]; … public function getNameAttribute(): string { return $this->first_name.’ ‘.$this->last_name; }}现在每次 dd($user),我们都可以看到 name 了。(不过仍然,这个属性不是从数据库取得的,而是每次使用时将 first_name 和 last_name 拼接得到的)。要注意下,如果你数据库里已经有 name 这个字段了,那情况就会有点不一样:$appends 数组里的 name 元素就不需要了,然后访问器需要传入一个参数,这个参数就是数据库中的 name (也就是说我们用不着再使用 $this 了)。 举个例子,我们也许想用 ucfirst() 来使名字的首字母转为大写:class User extends Model{ protected $appends = [ // ]; … public function getFirstNameAttribute($firstName): string { return ucfirst($firstName); } public function getLastNameAttribute($lastName): string { return ucfirst($lastName); }}现在当我们用 $user->first_name,它会返回一个首字母大写的字符串。由于这个特性,数据库字段最好是用 snake_case 这种蛇形命名。不要在配置文件中存储模型相关的静态数据我喜欢把与模型相关的静态数据存放在模型文件中。让我们一起来看一下。不要像下面这样:BettingOdds.phpclass BettingOdds extends Model{ …}config/bettingOdds.phpreturn [ ‘sports’ => [ ‘soccer’ => ‘sport:1’, ’tennis’ => ‘sport:2’, ‘basketball’ => ‘sport:3’, … ],];使用下面的方式访问:config(‘bettingOdds.sports.soccer’);我更喜欢这样做:BettingOdds.phpclass BettingOdds extends Model{ protected static $sports = [ ‘soccer’ => ‘sport:1’, ’tennis’ => ‘sport:2’, ‘basketball’ => ‘sport:3’, … ];}然后访问它们:BettingOdds::$sports[‘soccer’];为什么这样?因为这样有益于后续操作:class BettingOdds extends Model{ protected static $sports = [ ‘soccer’ => ‘sport:1’, ’tennis’ => ‘sport:2’, ‘basketball’ => ‘sport:3’, … ]; public function scopeSport($query, string $sport) { if (! isset(self::$sports[$sport])) { return $query; } return $query->where(‘sport_id’, self::$sports[$sport]); }}现在我们可以使用范围查询:BettingOdds::sport(‘soccer’)->get();使用集合替代原始的数组处理在过去,我们通常以一种原始的方式使用数组:$fruits = [‘apple’, ‘pear’, ‘banana’, ‘strawberry’];foreach ($fruits as $fruit) { echo ‘I have ‘. $fruit;}现在,我们可以使用一种高级的方法(译者注:集合的方式)处理数组中的数据。我们可以过滤、转换、遍历和修改数组中数据:$fruits = collect($fruits);$fruits = $fruits->reject(function ($fruit) { return $fruit === ‘apple’;})->toArray();[‘pear’, ‘banana’, ‘strawberry’]想要了解细节, 请查看 集合的文档.当使用 查询构造器时,->get() 方法返回一个 Collection 实例。但要注意别搞混了 Collection 和 Query builder:从 Query Builder 中,我们无法获取任何数据.。但我们有大量的查询相关的方法可以使用:orderBy(), where(),等等。最终调用 ->get() 方法之后,数据被获取到,内存空间被消耗。它返回一个 Collection 实例。某些查询构造器不可用或者说可用但是方法名不同,关于这些请查阅 所有集合的方法。如果你能在 Query Builder 层次过滤数据,就去做吧!不要依赖于等到结果 Collection 实例返回时再过滤—你将会消耗更多的内存空间。 使用 Limit 限制结果条数,在 DB 层使用索引来加快查询。善用扩展包、不要重复造轮子如下是一些我在用的扩展包:Laravel Blade DirectivesLaravel CORS(跨域请求时,将你的路由限制为指定域名访问)Laravel Tag Helper(在 Blade 内更方便地使用 HTML 标签)Laravel Sluggable(在 Eloquent 模型内生成 Slug 时十分实用)Laravel Responder(更容易地构建 JSON API)Image Intervention(处理图片)Horizon(使用少量配置即可管理队列)Socialite(使用少量配置即可集成第三方社交媒体登录)Passport(集成 OAuth 路由)Spatie’s ActivityLog(追踪模型的修改活动)Spatie’s Backup(备份文件和数据库)Spatie’s Blade-X(定义你自己的 HTML 标签;可与 Laravel Tag Helper 结合)Spatie’s Media Library(快速将模型与文件关联)Spatie’s Response Cache(缓存控制器的完整响应内容)Spatie’s Collection Macros(给集合添加更多宏)以下是我(原文作者)编写的一些扩展包:Befriended(类似社交媒体的点赞、收听、屏蔽操作)Schedule(创建日程表并检查某个时间点是否可用)Rating(为模型增加评分功能)Guardian(易于使用的权限系统)太难理解?联系我吧!如果你有更多关于 Laravel 的问题,如果你需要运维方面的帮助,或者只是想说声 谢谢,你可以在 Twitter @rennokki 上找到我!转自 PHP / Laravel 开发者社区 https://laravel-china.org/top… ...

January 14, 2019 · 3 min · jiezi

基于 Module 的 Laravel API 架构

转自 PHP / Laravel 开发者社区 https://laravel-china.org/top…我非常喜欢编写基于模块化设计的软件和编程方式,但我不太喜欢依赖第三方软件包和类库来处理一些琐碎的事情,因为它们不会让你的编程水平得到很好的提升。所以这两年来,我一直在用Laravel编写基于模块的软件,现在我对这个结果非常满意。推动我走向基于模块化设计的软件和编程方式的决定性因素是我想持续提升我的编程水平。想象一下,你构建了一个项目结构,6个月后你发现这个项目存在很多bug。在不影响6个月现有代码的情况下,通常不会轻易改变项目架构。在分析这个项目时,我注意到了两个要点:你要么在整个项目中都有一个标准,要么坚持下去,要么模块化并逐个模块地改进。有些人倾向于不惜一切代价、固守标准地开发,即使这可能意味着要坚持一个你不再喜欢的标准。就我个人来言,我更喜欢持续地改进,若是第 20 个模块和第 1 个模块写得完全不一样也没关系。如果某天我需要回到模块 1 修复 BUG 或重构,我可以将其改进为第 20 个模块使用的最新标准。假设,你也像我一样喜欢基于模块化开发 Laravel 应用、尽可能避免在项目中添加不必要的第三方依赖——本文是我的一点经验。1- 路由服务提供者Laravel 路由系统可以说是整个应用的入口。首先需要修改的是默认的 RouteServiceProvider.php 文件,它应当将现有路由模块化。<?phpnamespace App\Providers;use Illuminate\Support\Facades\Route;use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;class RouteServiceProvider extends ServiceProvider{ /** * 定义应用路由。 * * @return void */ public function map() { $this->mapModulesRoutes(); } protected function mapModulesRoutes() { // 如果你在编写传统 Web 应用而非 HTTP API,请使用 web 中间件。 Route::middleware(‘api’) ->group(base_path(‘routes/modules.php’)); }}如上,我们可以直接摆脱该文件的整个样板,只需设置一个模块化的路由文件即可。2- 模块文件Laravel 在 routes 文件夹中自带了一些文件。由于我们已经不在 RouteServiceProvider 中映射这些路由,所以可以直接删除它们。接下来,我们创建一个 modules.php 路由文件。<?phpuse Illuminate\Support\Facades\Route;Route::group([], base_path(‘app/Modules/Books/routes.php’));Route::group([], base_path(‘app/Modules/Authors/routes.php’));3- Books 模块在 app 文件夹中,创建 Modules/Books/routes.php 文件。在此文件中,我们可以定义该应用 Books 模块的路由规则。<?phpuse App\Modules\Books\ListBooks;use Illuminate\Support\Facades\Route;Route::get(’/books’, ListBooks::class);你可以使用基于控制器——也就是 Laravel 中默认标准的路由方式,但我个人更喜欢 Good bye controllers, hello Request Handlers(放弃控制器,采用请求处理器) 的方式。 如下是 ListBooks 的实现。<?phpnamespace App\Modules\Books;use App\Eloquent\Book;use App\Modules\Books\Resources\BookResource;class ListBooks{ public function __invoke(Book $book) { return BookResource::collection($book->paginate()); }}以上代码中 BookResource 是 Laravel 的资源转换层。按照官方对于命名空间的建议,我们可以在 app/Modules/Books/Resources 文件夹中创建它。<?phpnamespace App\Modules\Books\Resources;use Illuminate\Http\Resources\Json\Resource;class BookResource extends Resource{ public function toArray($request) { return [ ‘id’ => $this->resource->id, ’title’ => $this->resource->title, ]; }}4- Authors 模块我们还可以通过 Routes 文件来启动 Authors 模块。<?phpuse App\Modules\Authors\ListAuthors;use Illuminate\Support\Facades\Route;Route::get(’/authors’, ListAuthors::class);注意: app/Modules/Authors 这个命名空间正表示我们所编写的文件,对于请求处理程序来说也是非常简单的。<?phpnamespace App\Modules\Authors;use App\Eloquent\Author;use App\Modules\Authors\Resources\AuthorResource;class ListAuthors{ public function __invoke(Author $author) { return AuthorResource::collection($author->paginate()); }}最后,我们将编写的 Resource 类转变为响应式的 JSON 格式。<?phpnamespace App\Modules\Authors\Resources;use App\Modules\Books\Resources\BookResource;use Illuminate\Http\Resources\Json\Resource;class AuthorResource extends Resource{ public function toArray($request) { return [ ‘id’ => $this->resource->id, ’name’ => $this->resource->name, ‘books’ => $this->whenLoaded(‘books’, function () { return BookResource::collection($this->resource->books); }) ]; }}注意资源是如何进入另一个模块以重用 BookResource 。 这通常不是一个比较好的选择,因为模块应该是完全自给自足的,并且只能重用标准类,例如 Eloquent Models 或设计用于在任何模块上通用的通用的组件。 这个问题的解决方案通常是将 BookResource 复制到 Authors 模块中,从而可以在不使用另一个模块的情况下进行更改,反之亦然。 我决定保留这个跨模块的用法,这个例子表现出一个很好的经验方法,就是让模块之间彼此隔离,但是如果你认为上面的例子很简单并且不太可能带来任何问题。 始终确保编写测试以涵盖您编写的功能,以避免其他人在不知不觉中修改您的应用程序。5- 结语虽然这是一个非常简单的例子,但我希望它能够让人们根据自己的需要来轻松操作使用 Laravel 框架的结构标准。您可以非常轻松地更改文件的位置,以便构建基于模块化的应用程序。我的大多数项目都附带了 App / Components 模块,可以适用于任何模块可重用的泛类型的基础类; App / Eloquent ,Modules 文件夹可以用于保存 Eloquent 模型和数据库关系模型,我们可以在其中构建任何基于模块化的功能。 这是我最近开始研究的应用程序的文件夹目录结构:我希望每个人都能从中得到这个概念,每个模块都有自己的需求,并且可以拥有自己的文件夹/实体/类/方法/属性。没有必要将所有模块标准化完全相同,因为某些模块比其他模块简单得多,并且不需要大量的结构设计。此示例显示AccountChurn模块通过 HTTP 文件夹提供 API,同时仍通过控制台提供 Artisan 命令。另一方面,AccountOverview则仅提供 HTTP API,并且依赖仓库、值对象(bags)以及服务类(paginators)来提供更大的数据价值。 ...

January 11, 2019 · 2 min · jiezi

使用 Kubernetes 来部署你的 Laravel 程序

Laravel 是开发 PHP 应用程序的优秀框架。 无论您是需要构建新想法的原型,开发 MVP(最小可行产品)还是发布成熟的企业系统,Laravel 都可以促进所有开发任务和工作流程。如何处理部署应用程序是一个很有选择性的问题。 Vagrant 非常适合搭建类似于远程服务器的本地环境。 但是,在生产环境中,您很可能需要的不仅仅是一个 Web 主机和一个数据库。 您可能会针对多个要求提供单独的服务。 您还需要有适当的机制来确保应用程序始终在线运行,并且服务器可以有效地均衡负载。在本文中,我将解释如何在 Kubernetes 上搭建一个简单的 Laravel 应用程序的环境。Kubernetes 是什么?为什么使用它?Kubernetes 是一款由 Google 发起的开源系统,目的在于提高集群环境下管理容器化应用的效率。有些人将其称为容器编排平台,而 Kubernetes 并非唯一的此类平台。不过,相比其它对手,其享誉已盛,且知名度仍在不断提高;更别说你一旦习惯上它,就会发现它真的十分易用。如果你依然好奇为何有人能够愉快地和 Kubernetes 玩耍,答案就是——简单。Kubernetes 能够让部署、管理多个项目所需的大量集群变得更加容易。将 Laravel 应用部署到 Minikube正如我之前提到的,我将会在本文展示如何部署一个简单、无状态的 Laravel 应用到 Kubernetes。我将详细说明此过程中涉及到的步骤,同时向大家解释为何需要执行某项操作。此外,我还将展示如何快速横向扩展应用,并使用 Ingress Controller 使其能够通过特定域名或 IP 访问。你可以在多个云平台上面运行 Kubernetes ,例如 Google Cloud Engine 和 Amazon Web Services。在这个例子中,你会使用 Minikube 运行你的程序,Minikube 是一个让你在本地更容易运行 Kubernetes 的工具。与 Vagrant 类似,Minikube 仅仅是一个包含了 Kubernetes 运行平台和 Docker 的虚拟机。如果使用真正的 Kubernetes 的话,你需要使用 Docker 部署你的应用,同时你需要将运行平台扩展到三个节点。应用我已经准备了一个简单的 Laravel 程序,你可以从 GitHub 克隆下来。它只是一个全新的 Laravel 安装程序。因此,你可以使用本例中的演示程序,也可以自己创建一个新的 Laravel 程序。如果使用本例中的演示程序,请按照下面的命令将其克隆到项目目录里面。cd /to/your/working/directorygit clone git@github.com:learnk8s/laravel-kubernetes-demo.git .预备条件要实现本示例,你需要在你的本地系统中安装如下软件:1) Docker2) Kubectl3) Minikube如果你在Windows系统中安装上述软件遇到问题,请查阅 Windows 10 中 Docker 和 Kubernetes 入门教程,这是一个手把手教学的入门教程。Docker 镜像Kubernetes 部署容器化的应用,因此首先你需要为示例应用创建一个 Dcoker 镜像。由于本例中你在本地运行 Minikube,因此你只能用示例代码中的 Dockerfile 文件创建一个本地 Docker 镜像。FROM composer:1.6.5 as build WORKDIR /app COPY . /app RUN composer installFROM php:7.1.8-apache EXPOSE 80 COPY –from=build /app /app COPY vhost.conf /etc/apache2/sites-available/000-default.conf RUN chown -R www-data:www-data /app \ && a2enmod rewrite该 Dockerfile 文件由两部分组成:第一部分扩展了一个 PHP 的 composer 镜像,因此你能够安装应用依赖。第二部分创建了一个包含 Apache 服务的镜像, Apache 服务将会为示例应用服务。在测试 Docker 镜像前,你需要使用如下的命令创建镜像:cd /to/your/project/directory docker build -t yourname/laravel-kubernetes-demo .然后使用下面的命令运行示例程序:docker run -ti \ -p 8080:80 \ -e APP_KEY=base64:cUPmwHx4LXa4Z25HhzFiWCf7TlQmSqnt98pnuiHmzgY= \ laravel-kubernetes-demo示例程序可以通过 http://localhost:8080 访问。在这个安装中,容器是通用的,同时 APP_KEY 并不是写死或共享的。在 Minikube 中创建镜像cd /to/your/project/directoryeval $(minikube docker-env)docker build -t yourname/laravel-kubernetes-demo .别忘记执行上面的 eval 命令。 要在虚拟机中创建镜像,执行上面的 eval 命令是必须的。你只需要在当前的终端中执行一次这个命令。部署镜像现在示例应用的镜像已经创建完成,并且在 Minikube 中是可用的,因此你可以接下来继续部署这个镜像。我总是一开始就要确保 kubectl 在正确的上下文环境中。在这个例子中,上下文环境是 Minikube。你可以使用下面的命令快速的切换上下文环境:kubectl config use-context minikube然后你可以部署容器镜像:kubectl run laravel-kubernetes-demo \ –image=yourname/laravel-kubernetes-demo \ –port=80 \ –image-pull-policy=IfNotPresent \ –env=APP_KEY=base64:cUPmwHx4LXa4Z25HhzFiWCf7TlQmSqnt98pnuiHmzgY=上述的命令告诉 kubectl 从 Docker 镜像中运行我们的示例程序。上述命令的第一个参数告诉 kubectl 如果在本地存在镜像,就不要去登记处(例如 Docker Hub)拉取镜像。请注意,你仍然需要登录到 Docker 中,因为这样 kubectl 才能检查镜像是否是最新的。通过下面的命令,你会看到有一个 Pod 是为示例程序而创建的:kubectl get pods该命令会返回类似如下的输出:NAME READY STATUS RESTARTS AGElaravel-kubernetes-demo-7dbb9d6b48-q54wp 1/1 Running 0 18m你也可以使用 Minikube 的 GUI 控制面板来监控集群。GUI 还有助于可视化大多数经常讨论的指标。要查看该控制面板,请执行下属命令:minikube dashboard或者获取控制面板的 URL 地址:minikube dashboard –url=true暴露一个服务到目前为止,你已经创建了一个运行示例程序容器的部署。在集群中运行的 Pod 有一个动态的 IP。如果你使用该 IP 并直接把流量路由到那里,在每次重启 Pod 的时候,你可能每次都要更新路由表。事实上,在每次部署或者容器重启的时候,一个新的 IP 会关联到这个 Pod 中。为了避免需要手动的管理 IP 地址,你需要使用服务。服务在 Pods 集合中充当负载均衡器的角色。所以,尽管一个 Pod 的 IP 地址改变了,但是服务总是指向该 Pod。同时,由于服务总是拥有一个固定的 IP,因此你不需要手动更新任何东西。你可以使用下面的命令创建一个服务:kubectl expose deployment laravel-kubernetes-demo –type=NodePort –port=80倘若一切顺利,你会看到一个与下面信息相似的确认信息:service “laravel-kubernetes-demo” exposed执行下面的命令:kubectl get services上述命令显示了正在运行中的服务列表。你也可以通过控制面板中的 「服务」 导航菜单查看正在运行中的服务。很显然,一个更加令人兴奋的验证部署和服务暴露的方法就是在浏览器中运行示例程序。 ?要获取应用(服务)的URL地址,你可以使用下面的命令:minikube service –url=true laravel-kubernetes-demo上述命令会输出 IP 地址和端口号,例如:http://192.168.99.101:31399或者直接在浏览器中启动程序:minikube service laravel-kubernetes-demo不想错过接下来的故事,实验或者小提示。 如果你欣赏这篇文章,敬请期待接下来更多的文章内容。 希望新的内容直接发到你的邮箱并提升在 Kubernetes 方面的专业技能。 现在请订阅扩展你已经成功在 Kubernetes 中部署了应用。这是令人兴奋的。但是做这一切的重点是什么?你只是在一个 Pod 中做了一个部署,在一个节点上面暴露了网页服务。让我们把目前的应用多部署两个实例。现在你应该明白你正在处于什么位置,执行下面的命令获取希望得到的和现在已有的 Pod 列表:kubectl get deploymentNAME DESIRED CURRENT UP-TO-DATE AVAILABLEAGE laravel-kubernetes-demo 1 1 1 1 57m上面的输出中,每一项都是「1」。你希望获得三个 Pod。因此,我们通过下面的命令进行扩展:kubectl scale –replicas=3 deployment/laravel-kubernetes-demo deployment “laravel-kubernetes-demo” scaled命令执行完成。你已经将第一个 Pod 复制另外两个,系统为你提供了三个 Pod 来运行这个服务。执行 get deployment 可以检验这一切:kubectl get deploymentNAME DESIRED CURRENT UP-TO-DATE AVAILABLEAGE laravel-kubernetes-demo 3 3 3 3 59m你也可以在控制面板中的 Pods 页面或服务页面查看这些内容。现在,你正在使用三个 Pod 运行三个应用实例。想象一下这种场景,你的应用越来越受欢迎。成千上万的访客使用你的网页或软件。过去,你可能都焦头烂额在编写脚本创建更多实例的事情上。但是在 Kubernetes 中,您可以快速扩展出多个实例:kubectl scale –replicas=10 deployment/laravel-kubernetes-demo deployment “laravel-kubernetes-demo” scaled你看看使用 Kubernetes 扩展你的网站是何其便捷。Ingress你已经实现了不错的功能,部署了应用并扩展之。当你指向群集的(Minikube)IP地址和节点的端口号时,你就已经可见浏览器中正在运行的程序了。 现在,你将看到如果通过指定的 URL 访问应用程序,就如同之前部署到云端那样。为了在 Kubernetes 中使用 URL,你需要一个 Ingress。 Ingress 是一组允许入站连接到达 Kubernetes 集群的规则。Ingress 是非常必要的,因为在 Kubernetes 中,诸如 Pod 之类的资源仅具有可在集群内和集群内路由的IP地址。也就是说它们是无法进出外部环境的。我在演示应用源码中包含了一个有如下内容的 ingress.yaml 文件:apiVersion: extensions/v1beta1 kind: Ingress metadata: name: laravel-kubernetes-demo-ingress annotations: ingress.kubernetes.io/rewrite-target: / spec: backend: serviceName: default-http-server servicePort: 80 rules: - host: laravel-kubernetes.demo - http: paths: - path: / backend: serviceName: laravel-kubernetes-demo servicePort: 80在你所期望的 Kubernetes 资源文件基本内容里,该文件定义了一组路由流量入站的规则。 laravel-kubernetes.demo URL 会指向应用运行的 Service ,就像之前在 8181 端口上标记 laravel-kubernetes-demo 那样。没有集成 Ingress 资源, Ingress 控制器是无法使用的,因此您需要创建一个新的控制器或使用现有控制器。 本教程使用的是 Nginx Ingress 控制器来管理路由资源。 Minikube(v0.14及以上版本) 附带 Nginx 设置作为插件,您需要手动启用这个插件:minikube addons enable ingress注意,Minikube 可能需要几分钟才能下载并安装 Nginx 作为 Ingress 路由控制器。启用 Ingress 插件后,您可以通过这种方式来创建 Ingress 实例:kubectl create -f path-to-your-ingress-file.yaml您可以通过运行以下命令来验证并获取 Ingress 的实例信息:kubectl describe ing laravel-kubernetes-demo-ingress输出一些配置相关的信息:Name: laravel-kubernetes-demo-ingress Namespace: default Address: 192.168.99.101 Default backend: default-http-server:80 (<none>) Rules: Host Path Backends —- —- ——– * / laravel-kubernetes-demo:8181 (172.17.0.6:8181) Annotations: rewrite-target: / Events: Type Reason Age From Message —- —— —- —- ——- Normal CREATE 39s nginx-ingress-controller Ingress default/laravel-kubernetes-demo-ingressNormal UPDATE 20s nginx-ingress-controller Ingress default/laravel-kubernetes-demo-ingress您现在可以通过 minikube IP地址访问应用程序,如上所示。 要通过 URL https://laravel-kubernetes.demo 访问网站应用,您需要在 hosts 文件中添加一条解析记录。结论希望这篇文章能帮助您熟悉 Kubernetes 的部署和搭建。 根据我自己的经验,如果你经常进行类似的环境搭建,这会让你的搭建过程更加得心应手且有趣。转自 PHP / Laravel 开发者社区 https://laravel-china.org/top… ...

January 10, 2019 · 3 min · jiezi

[译]使用Laravel访问前端Cookie

镜像地址: https://juejin.im/post/5c35c1…在我们的应用程序中,我们可以在JS端设置cookie,但我们也希望在后端使用。我们可以使用$_COOKIE 全局魔术变量,但如果我们使用Laravel,我们会使用它提供的方法。让我们下Laravel中是如何使用的在前端设置Cookie在这篇文章中,我们关注现有的cookie。如果对如何从 JavasScript 处理它们感兴趣,请阅读文档。现在,假设我们有一个带有“ is-collapsed ”键的现有cookie 。我们想检查后端的值,以便在服务器端执行某些操作。Laravel和Cookies我们可以通过 request()->cookie() 方法或使用 Cookie Facade 来访问我们的cookie 。问题是,如果我们想要访问我们在前端设置的 cookie,我们会得到 null。但是我们使用 $_COOKIE 变量,我们可以访问它,这证明 cookie 是存在的。那问题在什么地方呢?默认情况下,框架带有用于加密cookie的中间件。如果我们从后端设置一个cookie,它会自动加密,因此Laravel可以读取它。从JS我们没有任何加密,这就是我们无法从框架中访问它们的原因。如何解决这个问题?在 app/Http/Kernel.php 中, 在 web 中间件分组中(5.2+),我们可以找到 EncryptCookies::class 行。通过注释这个中间件,可以关闭 cookie 的自动加密,但这种方法不是我们想要的解决方案。建议的方法是使用中间件并添加一些不需要加密的排除项,Laravel无论怎样都应该访问它们。我们可以在 app/Http/Middlewares/EncryptCookies.php. 插入中间件的排除项。/** * The names of the cookies that should not be encrypted. * * @var array */protected $except = [ ‘is-collapsed’,];通过将 cookie 的名称添加到 except 数组,我们可以使用 Cookie Facade 或 request()->cookie() 方法读取cookie 。如果您对更多信息感兴趣,请查看 文档 或查看有关 加密如何工作 的章节。

January 9, 2019 · 1 min · jiezi

Laravel框架FormRequest中重写错误处理

laravel 框架中默认的validate验证,在处理错误的时候,默认是返回上一页,当为ajax的时候才会返回Json。如果我们要一直返回Json的话,那么需要重写错误处理如下:在Requests目录只用 新建BaseRequest类代码如下<?php/** * @文件名称: BaseRequest.php. * @author: daisc * @email: jiumengfadian@live.com * @Date: 2019/1/8 /namespace App\Http\Requests\Front;use Illuminate\Foundation\Http\FormRequest;use Illuminate\Http\Exceptions\HttpResponseException;class BaseRequest extends FormRequest{ public function failedValidation($validator) { $error= $validator->errors()->all(); // $error = $validator; throw new HttpResponseException(response()->json([‘code’=>1,‘message’=>$error[0]])); }}重写了failedValidation方法,将抛出错误处理为了json格式的。然后在自定义的处理验证类中,继承该类就行了,如:RegisterForm中<?phpnamespace App\Http\Requests\Front;class RegisterForm extends BaseRequest{ /* * Determine if the user is authorized to make this request. * * @return bool / public function authorize() { return true; } /* * Get the validation rules that apply to the request. * * @return array */ public function rules() { return [ ‘phone’=>‘required|regex:"^1\d{10}"’, ’email’ => ‘required|email’, ‘password’=>‘required|confirmed’ ]; } public function messages() { return [ ‘phone.required’=>‘手机号不能为空’, ‘phone.regex’=>‘请输入正确的手机号’, ]; }}当我们在控制器中调用RegisterForm的时候,就回返回Json格式的错误信息。不分是否是AJAX原文地址 ...

January 9, 2019 · 1 min · jiezi

laravel cache get 是如何调用的?

本文使用版本为laravel5.5cache getpublic function cache() { $c=\Cache::get(‘app’); if(!$c) { \Cache::put(‘app’, ‘cache’, 1); } dump($c);//cache } config/app.php ‘aliases’ => [ ‘App’ => Illuminate\Support\Facades\App::class, ‘Artisan’ => Illuminate\Support\Facades\Artisan::class, ‘Auth’ => Illuminate\Support\Facades\Auth::class, ‘Blade’ => Illuminate\Support\Facades\Blade::class, ‘Broadcast’ => Illuminate\Support\Facades\Broadcast::class, ‘Bus’ => Illuminate\Support\Facades\Bus::class, ‘Cache’ => Illuminate\Support\Facades\Cache::class, ]使用cache实际调用的是Illuminate\Support\Facades\Cache,这个映射是如何做的?public/index.php$response = $kernel->handle($request = Illuminate\Http\Request::capture());bootstarp/app.php$app->singleton( Illuminate\Contracts\Http\Kernel::class, App\Http\Kernel::class);app/http/kernel.phpuse Illuminate\Foundation\Http\Kernel as HttpKernel;class Kernel extends HttpKernel{}Illuminate/Foundation/Http/Kernel.phppublic function handle($request){ try { $request->enableHttpMethodParameterOverride(); $response = $this->sendRequestThroughRouter($request); } catch (Exception $e) { $this->reportException($e); $response = $this->renderException($request, $e); } catch (Throwable $e) { $this->reportException($e = new FatalThrowableError($e)); $response = $this->renderException($request, $e); } $this->app[’events’]->dispatch( new Events\RequestHandled($request, $response) ); return $response;}protected 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()); } public function bootstrap() { if (! $this->app->hasBeenBootstrapped()) { $this->app->bootstrapWith($this->bootstrappers()); } }Illuminate/Foundation/Application.phppublic function bootstrapWith(array $bootstrappers) { $this->hasBeenBootstrapped = true; foreach ($bootstrappers as $bootstrapper) { $this[’events’]->fire(‘bootstrapping: ‘.$bootstrapper, [$this]); $this->make($bootstrapper)->bootstrap($this); $this[’events’]->fire(‘bootstrapped: ‘.$bootstrapper, [$this]); } }Illuminate/Foundation/Bootstrap/RegisterFacades.phppublic function bootstrap(Application $app) { Facade::clearResolvedInstances(); Facade::setFacadeApplication($app);//将config/app.php 里的aliases数组里面的Facades类设置别名 AliasLoader::getInstance(array_merge( $app->make(‘config’)->get(‘app.aliases’, []), $app->make(PackageManifest::class)->aliases() ))->register(); }Illuminate/Foundation/AliasLoader.phppublic function load($alias) { if (static::$facadeNamespace && strpos($alias, static::$facadeNamespace) === 0) { $this->loadFacade($alias); return true; } // $alias来自于config/app.php中aliases数组 if (isset($this->aliases[$alias])) { //‘Route’ => Illuminate\Support\Facades\Route::class, // class_alias 为一个类创建别名 return class_alias($this->aliases[$alias], $alias); } }Illuminate/Support/Facades/Cache.phpclass Cache extends Facade{ /** * Get the registered name of the component. * * @return string */ protected static function getFacadeAccessor() { return ‘cache’; }}Illuminate/Support/Facades/Facade.php这个文件没有get set ,只有__callStaticpublic static function __callStatic($method, $args) { $instance = static::getFacadeRoot(); if (! $instance) { throw new RuntimeException(‘A facade root has not been set.’); } return $instance->$method(…$args); } public static function getFacadeRoot() { return static::resolveFacadeInstance(static::getFacadeAccessor()); } protected static function resolveFacadeInstance($name) { //这里$name为cache if (is_object($name)) { return $name; } if (isset(static::$resolvedInstance[$name])) { return static::$resolvedInstance[$name]; } //$app是容器对象,实现了ArrayAccess接口,最终调用的还是容器的make方法 return static::$resolvedInstance[$name] = static::$app[$name]; }IlluminateContainerContainer.phppublic function make($abstract, array $parameters = []) { return $this->resolve($abstract, $parameters); } protected function resolve($abstract, $parameters = []) { $abstract = $this->getAlias($abstract); $needsContextualBuild = ! empty($parameters) || ! is_null( $this->getContextualConcrete($abstract) ); // If an instance of the type is currently being managed as a singleton we’ll // just return an existing instance instead of instantiating new instances // so the developer can keep using the same objects instance every time. if (isset($this->instances[$abstract]) && ! $needsContextualBuild) { return $this->instances[$abstract]; } $this->with[] = $parameters; $concrete = $this->getConcrete($abstract); // We’re ready to instantiate an instance of the concrete type registered for // the binding. This will instantiate the types, as well as resolve any of // its “nested” dependencies recursively until all have gotten resolved. if ($this->isBuildable($concrete, $abstract)) { $object = $this->build($concrete); } else { $object = $this->make($concrete); } // If we defined any extenders for this type, we’ll need to spin through them // and apply them to the object being built. This allows for the extension // of services, such as changing configuration or decorating the object. foreach ($this->getExtenders($abstract) as $extender) { $object = $extender($object, $this); } // If the requested type is registered as a singleton we’ll want to cache off // the instances in “memory” so we can return it later without creating an // entirely new instance of an object on each subsequent request for it. if ($this->isShared($abstract) && ! $needsContextualBuild) { $this->instances[$abstract] = $object; } $this->fireResolvingCallbacks($abstract, $object); // Before returning, we will also set the resolved flag to “true” and pop off // the parameter overrides for this build. After those two things are done // we will be ready to return back the fully constructed class instance. $this->resolved[$abstract] = true; array_pop($this->with); return $object; }Illuminate/Cache/CacheServiceProvider.php public function register() { $this->app->singleton(‘cache’, function ($app) { return new CacheManager($app); }); $this->app->singleton(‘cache.store’, function ($app) { return $app[‘cache’]->driver(); }); $this->app->singleton(‘memcached.connector’, function () { return new MemcachedConnector; }); }get set$instance->$method(…$args) ...

January 8, 2019 · 3 min · jiezi

laravel 使用 composer 加载自定义函数和自定义类

导语在开发中,会封装一些自定义函数以及自定义的类,本篇文章讲一下怎么使用 composer 实现自动加载。自定义函数实现自动加载,共有三步。创建文件。在 app 目录下创建 Helpers.php 文件,用于自定义函数;修改 composer.json 文件,添加如下语句最后是在项目目录中执行 composer dump-autoload接下来就可以在代码中使用自定义的函数了,需要注意的是自定义函数要检查是否已经定义,具体可参看 GitHub 中的代码。自定义类自定义类同上,也是三步,一些小改动。同样是创建文件,不同的是在 app 下创建 Libraries 目录,方便管理。在 Libraries 中可以创建自定义的类,注意 要添加命名空间 namespace app\Libraries ;同样是修改 composer.json 文件,修改如下执行 composer dump-autoload 即可。关于 laravel 的自动加载机制,可以看这篇文章。参考资料:laravel自定义函数和自定义类。

January 8, 2019 · 1 min · jiezi

Laravel Pipeline解读

大家好,今天给大家介绍下Laravel框架的Pipeline。它是一个非常好用的组件,能够使代码的结构非常清晰。 Laravel的中间件机制便是基于它来实现的。通过Pipeline,可以轻松实现APO编程。官方GIT地址https://github.com/illuminate…下面的代码是我实现的一个简化版本:class Pipeline{ /** * The method to call on each pipe * @var string / protected $method = ‘handle’; /* * The object being passed throw the pipeline * @var mixed / protected $passable; /* * The array of class pipes * @var array / protected $pipes = []; /* * Set the object being sent through the pipeline * * @param $passable * @return $this / public function send($passable) { $this->passable = $passable; return $this; } /* * Set the method to call on the pipes * @param array $pipes * @return $this / public function through($pipes) { $this->pipes = $pipes; return $this; } /* * @param \Closure $destination * @return mixed / public function then(\Closure $destination) { $pipeline = array_reduce(array_reverse($this->pipes), $this->getSlice(), $destination); return $pipeline($this->passable); } /* * Get a Closure that represents a slice of the application onion * @return \Closure */ protected function getSlice() { return function($stack, $pipe){ return function ($request) use ($stack, $pipe) { return $pipe::{$this->method}($request, $stack); }; }; }}此类主要逻辑就在于then和getSlice方法。通过array_reduce,生成一个接受一个参数的匿名函数,然后执行调用。简单使用示例class ALogic{ public static function handle($data, \Clourse $next) { print “开始 A 逻辑”; $ret = $next($data); print “结束 A 逻辑”; return $ret; }}class BLogic{ public static function handle($data, \Clourse $next) { print “开始 B 逻辑”; $ret = $next($data); print “结束 B 逻辑”; return $ret; }}class CLogic{ public static function handle($data, \Clourse $next) { print “开始 C 逻辑”; $ret = $next($data); print “结束 C 逻辑”; return $ret; }}$pipes = [ ALogic::class, BLogic::class, CLogic::class];$data = “any things”;(new Pipeline())->send($data)->through($pipes)->then(function($data){ print $data;});运行结果:“开始 A 逻辑"“开始 B 逻辑"“开始 C 逻辑"“any things"“结束 C 逻辑"“结束 B 逻辑"“结束 A 逻辑"AOP示例AOP 的优点就在于动态的添加功能,而不对其它层次产生影响,可以非常方便的添加或者删除功能。class IpCheck{ public static function handle($data, \Clourse $next) { if (“IP invalid”) { // IP 不合法 throw Exception(“ip invalid”); } return $next($data); }}class StatusManage{ public static function handle($data, \Clourse $next) { // exec 可以执行初始化状态的操作 $ret = $next($data) // exec 可以执行保存状态信息的操作 return $ret; }}$pipes = [ IpCheck::class, StatusManage::class,];(new Pipeline())->send($data)->through($pipes)->then(function($data){ “执行其它逻辑”;}); ...

January 7, 2019 · 2 min · jiezi