分类目录归档:PHP

设计模式

在软件工程中,设计模式(Design Pattern)是对软件设计中普遍存在(反复出现)的各种问题,所提出的解决方案。设计模式是描述在各种不同场景下,要怎么解决问题的一种方案。

常用设计模式
1 创建型(对象实例化)
抽象工厂模式(Abstract Factory)****
建造者模式(Builder)****
工厂方法模式(Factory Method)****
多例模式(Multiton)
对象池模式(Pool)
原型模式(Prototype)
简单工厂模式(Simple Factory)****
单例模式(Singleton)****
静态工厂模式(Static Factory) ****

2 结构型(类和对象的组合)
适配器模式(Adapter) ****
桥梁模式(Bridge)
组合模式(Composite)
数据映射模式(Data Mapper)
装饰模式(Decorator)
依赖注入模式(Dependency Injection) ****
门面模式(Facade) ****
流接口模式(Fluent Interface)
代理模式(Proxy) ****
注册模式(Registry)

3 行为型(类的对象间通信)
责任链模式(Chain Of Responsibilities) ****
命令行模式(Command)
迭代器模式(Iterator)
中介者模式(Mediator)
备忘录模式(Memento)
空对象模式(Null Object)
观察者模式(Observer) *****
规格模式(Specification)
状态模式(State)
策略模式(Strategy)
模板方法模式(Template Method)
访问者模式(Visitor)

4 其它
委托模式(Delegation)
服务定位器模式(Service Locator) ****
资源库模式(Repository)

来源:https://github.com/domnikl/DesignPatternsPHP

1.1 简单工厂模式、静态工厂模式、工厂方法模式、抽象工厂模式
简单工厂模式、静态工厂模式类似,都是直接约定使用某个方法生产产品;工厂方法模式是将生成产品的方法进行抽离,放入到一个抽象类中,工厂类继承该类并实现其中的工厂方法,本质上和简单工厂模式、静态工厂模式没有差别,不同是由于对生成产品的方法进行来抽离,方便产生不同类型的工厂;

抽象工厂模式与其它模式有较大不同,它是对工厂的抽象;在简单工厂模式、静态工厂模式、工厂方法模式中,工厂是和产品直接相关的,产品和工厂是不相关的(被抽象了,或者说只跟抽象工厂相关)。具体来说:抽象工厂不过是对不同类型的工厂进行抽象,比如某汽车公司可以生产轿车和汽车,由于复杂性,通常不会在一个工厂中生产,会分到轿车厂和汽车厂,但是轿车厂和汽车厂都具备了生产车的能力,当要生产汽车时,抽象工厂决定由汽车厂生产,用户并不知道汽车是由汽车厂生产的(产品和工厂不相关)。

抽象工厂模式通常用来解决较复杂的产品制造逻辑,绝大部分情况,简单工厂模式(包括静态工厂模式和工厂方法模式)可以很好适用。

Laravel中的Manager大量应用简单工厂模式(具体来说是工厂方法模式),比如Auth组件中,Guard管理:

// Illuminate\Contracts\Auth\Factory
<?php

namespace Illuminate\Contracts\Auth;

interface Factory
{
    /**
     * Get a guard instance by name.
     *
     * @param  string|null  $name
     * @return mixed
     */
    public function guard($name = null);

    /**
     * Set the default guard the factory should serve.
     *
     * @param  string  $name
     * @return void
     */
    public function shouldUse($name);
}


// Illuminate\Contracts\Auth\AuthManager
<?php

namespace Illuminate\Auth;

use Closure;
use InvalidArgumentException;
use Illuminate\Contracts\Auth\Factory as FactoryContract;

class AuthManager implements FactoryContract
{
    use CreatesUserProviders;

    public function guard($name = null)
    {
        $name = $name ?: $this->getDefaultDriver();

        return $this->guards[$name] ?? $this->guards[$name] = $this->resolve($name);
    }
}

这里的guard方法就是工厂方法,AuthManager是工厂,它实现了guard方法,通过调用guard方法,可以得到不同的guard实例。当需要添加自定义的guard时,可以让AuthManager把自定义的产品类型(guard)添加进来,在调用时可以通过guard(“xxx”)取回自定义的guard实例。

1.2 建造者模式(Builder)

建造者模式将一个复杂的对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。(对象创造过程进行抽离,把对象的创建变成标准步骤)。

关键点就是把步骤进行抽离,然后在面对不同类型对象的构建时,通过组合不同的步骤进行构建对象。

3 单例模式(Singleton)
单例模式的作用就是保证在整个应用程序的生命周期中,任何一个时刻,单例类的实例都只存在一个,同时这个类还必须提供一个访问该类的全局访问点。常见使用实例:数据库连接器;日志记录器(如果有多种用途使用多例模式);锁定文件。

<?php

/**
 * Singleton类
 */
class Singleton
{
    /**
     * @var Singleton reference to singleton instance
     */
    private static $instance;
    
    /**
     * 通过延迟加载(用到时才加载)获取实例
     *
     * @return self
     */
    public static function getInstance()
    {
        if (null === static::$instance) {
            static::$instance = new static;
        }

        return static::$instance;
    }

    /**
     * 构造函数私有,不允许在外部实例化
     *
     */
    private function __construct()
    {
    }

    /**
     * 防止对象实例被克隆
     *
     * @return void
     */
    private function __clone()
    {
    }

    /**
     * 防止被反序列化
     *
     * @return void
     */
    private function __wakeup()
    {
    }
}

由于PHP的运行模式的关系,单例设计模式并没有真正发挥到它应该起到的作用(只能锁定到请求作用域,无法应用到整个应用)。

2.1 适配器(Adapter)
适配器的存在,就是为了将已存在的东西(接口)转换成适合我们需要、能被我们所利用的东西。在现实生活中,适配器更多的是作为一个中间层来实现这种转换作用。比如电源适配器,它是用于电流变换(整流)的设备。

2.3 装饰模式(Decorator)
装饰器模式能够从一个对象的外部动态地给对象添加功能。

通常给对象添加功能,要么直接修改对象添加相应的功能,要么派生对应的子类来扩展,抑或是使用对象组合的方式。显然,直接修改对应的类这种方式并不可取。在面向对象的设计中,我们也应该尽量使用对象组合,而不是对象继承来扩展和复用功能。装饰器模式就是基于对象组合的方式,可以很灵活的给对象添加所需要的功能。装饰器模式的本质就是动态组合。动态是手段,组合才是目的。

常见的使用示例:Web服务层 —— 为 REST 服务提供 JSON 和 XML 装饰器。

2.3 依赖注入模式(Dependency Injection)

2.4 门面模式(Facade)
门面模式(Facade)又称外观模式,用于为子系统中的一组接口提供一个一致的界面。门面模式定义了一个高层接口,这个接口使得子系统更加容易使用:引入门面角色之后,用户只需要直接与门面角色交互,用户与子系统之间的复杂关系由门面角色来实现,从而降低了系统的耦合度。

2.5 流接口模式(Fluent Interface)

2.6 代理模式(Proxy)

3.1 责任链模式(Chain Of Responsibilities)
3.2 观察者模式(Observer)
3.3 迭代器模式(Iterator)

Laravel 事件详解

https://github.com/illuminate/events

一个Laravel应用就是一个大容器,容器首先需要构建:

// bootstrap/app.php
$app = new Illuminate\Foundation\Application(
    realpath(__DIR__.'/../')
);

// Illuminate\Foundation\Application构造函数
// 注意$this->registerBaseServiceProviders()
class Application extends Container implements ApplicationContract, HttpKernelInterface
{
    public function __construct($basePath = null)
    {
        if ($basePath) {
            $this->setBasePath($basePath);
        }

        $this->registerBaseBindings();

        $this->registerBaseServiceProviders();

        $this->registerCoreContainerAliases();
    }

    protected function registerBaseServiceProviders()
    {
        $this->register(new EventServiceProvider($this));

        $this->register(new LogServiceProvider($this));

        $this->register(new RoutingServiceProvider($this));
    }
}

容器的register方法不过是调用具体的ServiceProvider的register方法,然后在调用具体的ServiceProvider的boot方法(如果没有boot过)。可见之类的Event,Log,Routing是核心服务提供者。

Illuminate\Events\EventServiceProvider:

class EventServiceProvider extends ServiceProvider
{
    public function register()
    {
        $this->app->singleton('events', function ($app) {
            return (new Dispatcher($app))->setQueueResolver(function () use ($app) {
                return $app->make(QueueFactoryContract::class);
            });
        });
    }
}

往容器中注入绑定,events对应Illuminate\Events\Dispatcher类实例。这里的Illuminate\Events\Dispatcher类仅仅几百行代码,就是事件机制实现的全部。

总体上,首先是定义事件(一个事件类,用于承载事件的数据,类名就是事件名称,另一类是没有对应类,仅一个字符串,表示事件名称),事件可以对应一个或多个监听器,监听器接收事件对象(事件载体就是事件类对象,如果非事件类,则是自定义数据),进行代码处理,然后是触发事件,那么对应的监听器就会被执行。

添加事件的监听器:

    public function listen($events, $listener)
    {
        foreach ((array) $events as $event) {
            if (Str::contains($event, '*')) {
                $this->setupWildcardListen($event, $listener);
            } else {
                $this->listeners[$event][] = $this->makeListener($listener);
            }
        }
    }
// 用法1
dispatch.listen("TestEvent", $listener)
dispatch.listen("TestEvent2", $listener)
$this->listeners["TestEvent"][] = $listener;
$this->listeners["TestEvent2"][] = $listener;
// 用法2
dispatch.listen("TestEvent*", $listener)
$this->wildcards["TestEvent*"][] = $listener;
// 用法3
dispatch.listen(["TestEvent", "TestEvent*"], $listener)
$this->listeners["TestEvent"][] = $listener;
$this->wildcards["TestEvent*"][] = $listener;

如果要设置一批事件对应同一个监听器,可以传递一个事件数组。另外,事件名称对应的监听器是一个数组,如果多次调用,就多次添加监听器,哪怕监听器是一样的。

分发器的hasListeners不过是判断事件名称是否在$this->listeners或$this->wildcards中

    public function hasListeners($eventName)
    {
        return isset($this->listeners[$eventName]) || isset($this->wildcards[$eventName]);
    }

添加了事件监听器,但是事件监听器实际被makeListener方法进行规范处理:

    public function makeListener($listener, $wildcard = false)
    {
        if (is_string($listener)) {
            return $this->createClassListener($listener, $wildcard);
        }

        // 监听器不是字符串,认为是闭包函数。
        // 注:如果针对的是通配符事件,自定义的监听器第一参数必须是事件,第二参数必须是事件载体
        return function ($event, $payload) use ($listener, $wildcard) {
            if ($wildcard) {
                return $listener($event, $payload);
            }

            return $listener(...array_values($payload));
        };
    }

    // 1 监听器是字符串的情况,也是返回闭包,在执行闭包时,也是根据事件类型($wildcard),调用方法有所差别
    public function createClassListener($listener, $wildcard = false)
    {
        return function ($event, $payload) use ($listener, $wildcard) {
            if ($wildcard) {
                return call_user_func($this->createClassCallable($listener), $event, $payload);
            }

            return call_user_func_array(
                $this->createClassCallable($listener), $payload
            );
        };
    }
    
    // 从字符串解析出类目和需要执行的方法
    // 通过类反射,判断类是否实现了Illuminate\Contracts\Queue\ShouldQueue接口(该接口未定义方法)
    // 如果实现了该接口,返回一个闭包函数,这个作为call_user_func或call_user_func_array的第一参数,事件错误时,监听器代码被推入队列
    // 根据类名生成对象,按照[类对象,类方法]返回,这个作为call_user_func或call_user_func_array的第一参数
    protected function createClassCallable($listener)
    {
        list($class, $method) = $this->parseClassCallable($listener);
    
        if ($this->handlerShouldBeQueued($class)) {
            return $this->createQueuedHandlerCallable($class, $method);
        }

        return [$this->container->make($class), $method];
    }

    // Test@fire => ["Test", "fire"],表示要调用fire方法
    // 如果把包含@,比如Test,那么返回["Test", "handle"],表示要调用handle方法 
    protected function parseClassCallable($listener)
    {
        return Str::parseCallback($listener, 'handle');
    }

如果传递进来的$listener是字符串,解析字符串构建监听器实例。否则就认为是一个函数(或是闭包函数)。注意这里返回的是一个统一格式的闭包,第一个是事件名称(或事件对象),第二个参数是事件载体(payload),如果事件是一个类名称,它对应的对象就是载体,如果已经是事件对象,那么这个事件对象即是事件,也是事件对应的载体(这个情况在触发事件时,直接触发事件对象即可)。

事件触发:

public function dispatch($event, $payload = [], $halt = false)
    {
        // 如果传递进来的$event是对象,那么$payload设置为等于该对象,然后把$event改为该对象的类名
        // 如果传递进来的$event不是对象,$payload就是对应的载体
        list($event, $payload) = $this->parseEventAndPayload(
            $event, $payload
        );
        // 通过载体,判断是否需要广播($payload是一个数组,第一个元素理论上对应事件对象,如果事件对象声明要广播)
        if ($this->shouldBroadcast($payload)) {
            $this->broadcastEvent($payload[0]);
        }

        $responses = [];
        // 根据事件取回监听器(按照预期格式生成,格式统一的闭包),循环执行监听器
        foreach ($this->getListeners($event) as $listener) {
            $response = $listener($event, $payload);

            // $halt用来判断一旦有输出,就返回
            if ($halt && ! is_null($response)) {
                return $response;
            }

            // 如果返回false,说明后续的监听器不在运行
            if ($response === false) {
                break;
            }

            $responses[] = $response;
        }
        return $halt ? null : $responses;
    }

事件触发都是直接或间接调用该方法。如果一个监听器是需要推入队列的,那么这个时候会生成一个job推入队列。如果一个事件声明要广播的,也会在这里处理。另外,框架中有一个全局方法对应这个方法,event(), 直接调用event(‘事件或事件对象’)。

事件触发时,需要取回监听器:

    public function getListeners($eventName)
    {
        $listeners = $this->listeners[$eventName] ?? [];

        $listeners = array_merge(
            $listeners, $this->getWildcardListeners($eventName)
        );

        return class_exists($eventName, false)
                    ? $this->addInterfaceListeners($eventName, $listeners)
                    : $listeners;
    }

这个就是数组合并。
注意:getWildcardListeners()这个方法,实际是根据给定的事件名称,检查是否和通配事件名称匹配,如果匹配,对应的监听器也返回。
注意:$this->addInterfaceListeners($eventName, $listeners),这个举例来说明,比如B实现了A接口,如果A接口指定了监听器,B也对应了监听器,取回B事件对应的监听器时,也包含A的监听器(这个就是基于接口的事件)。

以上是添加事件监听器,事件监听器的构建,触发事件,以及触发事件时如何取回监听器。总体上就是把找到事件对应的监听器,然后把事件对象传递给监听器,监听器根据事件对象取回数据进行逻辑处理。

用法:

// 1 触发事件对象时,可以直接传递事件对象(常用),事件对象会直接传入监听器方法
# 定义了事件对象
class TestEvent {}
$testEvent = new TestEvent();
# 用法
dispatch($testEvent); #实际会解析成dispatch("TestEvent", $testEvent)
dispatch("TestEvent", $testEvent);

// 2 触发的事件非事件对象(只是一个名字)$payload会传入自定义的方法中(监听器,可以是闭包函数),这个需要根据具体情况编写
dispatch("aaaaa", $payload)

还有特殊的搞法:

    public function push($event, $payload = [])
    {
        $this->listen($event.'_pushed', function () use ($event, $payload) {
            $this->dispatch($event, $payload);
        });
    }

    public function flush($event)
    {
        $this->dispatch($event.'_pushed');
    }

    public function forgetPushed()
    {
        foreach ($this->listeners as $key => $value) {
            if (Str::endsWith($key, '_pushed')) {
                $this->forget($key);
            }
        }
    }

先push,然后flush直接分发。

另外有一个订阅的概念:

    public function subscribe($subscriber)
    {
        $subscriber = $this->resolveSubscriber($subscriber);

        $subscriber->subscribe($this);
    }

取到订阅器对象,执行订阅器的subscribe()方法。实际不过是一个二次包装。但是需要有一个地方运行分发器的subscribe()方法。这个在框架中:

#文件: app/Providers/EventServiceProvider.php
<?php

namespace App\Providers;

use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;

class EventServiceProvider extends ServiceProvider
{
    /**
     * 应用的事件监听器映射.
     *
     * @var array
     */
    protected $listen = [
        //
    ];

    /**
     * 要注册的订阅者类.
     *
     * @var array
     */
    protected $subscribe = [
         'App\Listeners\UserEventSubscriber',
    ];
}

定义订阅器:

<?php

namespace App\Listeners;

class UserEventSubscriber
{
    public function onUserLogin($event) {}

    public function onUserLogout($event) {}

    /**
     * 为订阅者注册监听器.
     *
     * @param  Illuminate\Events\Dispatcher  $events
     */
    public function subscribe($events)
    {
        $events->listen(
            'Illuminate\Auth\Events\Login',
            'App\Listeners\UserEventSubscriber@onUserLogin'
        );

        $events->listen(
            'Illuminate\Auth\Events\Logout',
            'App\Listeners\UserEventSubscriber@onUserLogout'
        );
    }

}

这个不过是定义事件与监听器的另一种方式而已,完成可以把对应关系直接写入到$listen数组中。

其它用法(来自文档):

#文件: app/Providers/EventServiceProvider.php
<?php

namespace App\Providers;

use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;

class EventServiceProvider extends ServiceProvider
{
    protected $listen = [
        'App\Events\OrderShipped' => [
            'App\Listeners\SendShipmentNotification',
        ],
    ];
}
// 生成对应的事件对象和监听器对象
php artisan event:generate

对应生成的事件类,可以编写其它逻辑,让其承载数据,而生成的监听器的handle方法必须接收这个事件对象。

// 定义事件
<?php

namespace App\Events;

use App\Order;
use Illuminate\Queue\SerializesModels;

class OrderShipped
{
    use SerializesModels;

    public $order;

    /**
     * 创建一个新的事件实例.
     *
     * @param  Order  $order
     * @return void
     */
    public function __construct(Order $order)
    {
        $this->order = $order;
    }
}

// 监听器
<?php

namespace App\Listeners;

use App\Events\OrderShipped;

class SendShipmentNotification
{
    /**
     * 创建事件监听器.
     *
     * @return void
     */
    public function __construct()
    {
        //
    }

    /**
     * 处理事件.
     *
     * @param  OrderShipped  $event
     * @return void
     */
    public function handle(OrderShipped $event)
    {
        // 使用 $event->order 发访问订单...
    }
}

除了直接声明对应关系,也可以直接编写:

public function boot()
{
    parent::boot();

    Event::listen('event.name', function ($foo, $bar) {
        //
    });
}

通配符事件监听器:

// 闭包特殊,第一参数必须是事件名称,第二参数是承载的数据(事件对象)
$events->listen('event.*', function ($eventName, array $data) {
    //
});

如果希望事件往下传播,监听器的handle方法返回false即可。

如果希望监听器异步运行,只要监听器类实现ShouldQueue接口(空接口,不需要实现任何方法)即可:

<?php

namespace App\Listeners;

use App\Events\OrderShipped;
use Illuminate\Contracts\Queue\ShouldQueue;

class SendShipmentNotification implements ShouldQueue
{
    //
}

如果希望定义监听器进入的队列已经队列名称:

<?php

namespace App\Listeners;

use App\Events\OrderShipped;
use Illuminate\Contracts\Queue\ShouldQueue;

class SendShipmentNotification implements ShouldQueue
{
    /**
     * 任务将被推送到的连接名称.
     *
     * @var string|null
     */
    public $connection = 'sqs';

    /**
     * 任务将被推送到的连接名称.
     *
     * @var string|null
     */
    public $queue = 'listeners';
}

如果监听器还要和队列进行交互,需要使用Illuminate\Queue\InteractsWithQueue:

<?php

namespace App\Listeners;

use App\Events\OrderShipped;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;

class SendShipmentNotification implements ShouldQueue
{
    use InteractsWithQueue;

    public function handle(OrderShipped $event)
    {
        if (true) {
            $this->release(30);
        }
    }

    // 处理失败job
    public function failed(OrderShipped $event, $exception)
    {
        //
    }
}

Laravel 异常详解

流程启动:

#public/index.php

$app = require_once __DIR__.'/../bootstrap/app.php';
$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);
$response = $kernel->handle(
    $request = Illuminate\Http\Request::capture()
);

首先,在__DIR__.’/../bootstrap/app.php’中,向容器注册了错误处理类:

$app->singleton(
    Illuminate\Contracts\Debug\ExceptionHandler::class,
    App\Exceptions\Handler::class
);

然后,$kernel->handle()中,会调用kernel的bootstrap()方法,最终会运行\Illuminate\Foundation\Bootstrap\HandleExceptions::class类的bootstrap()方法:

class HandleExceptions
{
    public function bootstrap(Application $app)
    {
        $this->app = $app;

        error_reporting(-1);

        set_error_handler([$this, 'handleError']);

        set_exception_handler([$this, 'handleException']);

        register_shutdown_function([$this, 'handleShutdown']);

        if (! $app->environment('testing')) {
            ini_set('display_errors', 'Off');
        }
    }
    public function handleError($level, $message, $file = '', $line = 0, $context = [])
    {
        if (error_reporting() & $level) {
            throw new ErrorException($message, 0, $level, $file, $line);
        }
    }
    public function handleException($e)
    {
        if (! $e instanceof Exception) {
            $e = new FatalThrowableError($e);
        }

        try {
            $this->getExceptionHandler()->report($e);
        } catch (Exception $e) {
            //
        }

        if ($this->app->runningInConsole()) {
            $this->renderForConsole($e);
        } else {
            $this->renderHttpResponse($e);
        }
    }
}

这里挟持了PHP的错误和异常处理。分别对应handleError和handleException。一旦发生错误或异常,相应进入设置的回调。注意到handleError方法,如果$level是需要报告的,那么就抛出一个ErrorException异常,这里是把错误转换成了异常。

继续handleException()方法,首先如果接收的对象不是一个异常对象(Exception子类),就用FatalThrowableError类包装一下(变成异常对象)。然后调用异常处理类的report方法,接着判断是否运行在Console下,对应运行render方法。这里的$this->getExceptionHandler()方法从容器中取回绑定的异常处理对象,它就是前面绑定的App\Exceptions\Handler类对象,所以这个类应该实现了report和render方法。

最终,异常(包括错误),最终定位到App\Exceptions\Handler中的report和render方法(这两个方法都会运行):

<?php
// App\Exceptions\Handler

namespace App\Exceptions;

use Exception;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;

class Handler extends ExceptionHandler
{
    protected $dontReport = [
        //
    ];

    protected $dontFlash = [
        'password',
        'password_confirmation',
    ];

    public function report(Exception $exception)
    {
        parent::report($exception);
    }

    public function render($request, Exception $exception)
    {
        return parent::render($request, $exception);
    }
}

App\Exceptions\Handler中的report和render都是直接调用父类方法(这里可以注入自定义逻辑)。

App\Exceptions\Handler继承自Illuminate\Foundation\Exceptions\Handler,Illuminate\Foundation\Exceptions\Handler实现了Illuminate\Contracts\Debug\ExceptionHandler接口。

// Illuminate\Contracts\Debug\ExceptionHandler 接口
namespace Illuminate\Contracts\Debug;

use Exception;

interface ExceptionHandler
{
    public function report(Exception $e);

    public function render($request, Exception $e);

    public function renderForConsole($output, Exception $e);
}

注,report方法就是把异常记录到外部,这个过程是由日志处理器来完成的,在Laravel中是通过Monolog来处理的(可以设置多个处理器,以让日志发送到多个地方)。

以下就是Illuminate\Foundation\Exceptions\Handler中的report实现:

//Illuminate\Foundation\Exceptions\Handler
    public function report(Exception $e)
    {
        if ($this->shouldntReport($e)) {
            return;
        }

        if (method_exists($e, 'report')) {
            return $e->report();
        }

        try {
            $logger = $this->container->make(LoggerInterface::class);
        } catch (Exception $ex) {
            throw $e; // throw the original exception
        }

        $logger->error(
            $e->getMessage(),
            array_merge($this->context(), ['exception' => $e]
        ));
    }

首先$this->shouldntReport()实际就是判断异常是否在$this->dontReport和$this->internalDontReport数组中,在就是不需要报告。

protected $internalDontReport = [
        AuthenticationException::class,
        AuthorizationException::class,
        HttpException::class,
        HttpResponseException::class,
        ModelNotFoundException::class,
        TokenMismatchException::class,
        ValidationException::class,
    ];

而$this->dontReport是空的,可以在子类App\Exceptions\Handler的dontReport中添加不需要报告的异常类型。

接下来,如果一个异常包含了report方法,那么直接调用它的report方法。然后从容器中取回日志处理器(框架初始化时已经注册),然后调用日志处理器的error记录日志。 这里需要注意,report方法还是可能继续抛出异常。

对于render方法就比较复杂一点,因为需要渲染输出:

//Illuminate\Foundation\Exceptions\Handler
    public function render($request, Exception $e)
    {
        if (method_exists($e, 'render') && $response = $e->render($request)) {
            return Router::toResponse($request, $response);
        } elseif ($e instanceof Responsable) {
            return $e->toResponse($request);
        }

        $e = $this->prepareException($e);

        if ($e instanceof HttpResponseException) {
            return $e->getResponse();
        } elseif ($e instanceof AuthenticationException) {
            return $this->unauthenticated($request, $e);
        } elseif ($e instanceof ValidationException) {
            return $this->convertValidationExceptionToResponse($e, $request);
        }

        return $request->expectsJson()
                        ? $this->prepareJsonResponse($request, $e)
                        : $this->prepareResponse($request, $e);
    }

看看AuthenticationException异常的处理(认证异常):

    protected function unauthenticated($request, AuthenticationException $exception)
    {
        return $request->expectsJson()
                    ? response()->json(['message' => $exception->getMessage()], 401)
                    : redirect()->guest(route('login'));
    }

认证不通过,可能返回route(‘login’)路由。这里有点硬编码了。如果有两个后台,那么这里只要认证不通过就永远跳转到route(‘login’)。所以需要根据guard来判断跳转到哪个路由:

//App\Exceptions\Handler
    public function render($request, Exception $exception)
    {
        if ($exception instanceof \Illuminate\Auth\AuthenticationException) {
            $guards = $exception->guards();
            if (is_array($guards) && in_array('admin', $guards)) {
                return $request->expectsJson()
                    ? response()->json(['message' => $exception->getMessage()], 401)
                    : redirect()->guest(route('backend.login'));
            }
        }
        return parent::render($request, $exception);
    }

当使用默认auth中间件时,如果抛异常,$exception->guards()是空的(使用了默认的guard);当指定了guard后(比如auth:admin),这时$exception->guards()就包含admin,通过这个信息可以控制怎么渲染输出(特别是控制怎么跳转)。

Laravel 管道详解

函数http://php.net/array_reduce用法(http://php.net/array_reduce
):

// array_reduce() 将回调函数 callback 迭代地作用到 array 数组中的每一个单元中,从而将数组简化为单一的值。
mixed array_reduce ( array $array , callable $callback [, mixed $initial = NULL ] )

第二参数的$callback调用格式:

//$carry携带上次迭代里的值; 如果本次迭代是第一次,那么这个值是 initial
//$item携带了本次迭代的值
mixed callback ( mixed $carry , mixed $item )

1 如果$array为空,直接返回$initial
2 如果$array不为空,那么$initial作为第一次迭代时的$carry
3 每次迭代调用的$callback的结果作为下次迭代调用$callback的$carry

这里主要是第三点,经过特殊处理,可以产生多层嵌套(C包装了B,B包装了A)。

列子:

$arr = [3, 2, 1];
$arr = [];
$fn = function ($carry, $hold) {
    return function() use ($carry, $hold) {
        echo "$hold -- Start\n";
        $carry();
        echo "$hold -- End \n";
    };
};
$init = function () {
    echo "init\n";
};
$callback = array_reduce($arr, $fn, $init);
$callback();

输出:

// $arr等于空时
init

// $arr = [3,2,1]时
1 -- Start
2 -- Start
3 -- Start
init
3 -- End 
2 -- End 
1 -- End

各种情况分析:
1 当$arr为空时,直接返回$init,这里返回一个闭包函数,执行后直接输出”init”字符串
2 当$arr等于[3]时,第一个次迭代,相当于如下代码:

// 相当于
$fn($init, 3);
// 相当于
$fn(function () {
    echo "init\n";
}, 3);
// 相当于
function (function () {
    echo "init\n";
}, 3);
// 相当于
function() use(function () {
    echo "init\n";
}, 3) {
    echo "$hold -- Start\n";
    (function () {
        echo "init\n";
    })();
    echo "$hold -- End \n";
}
// 最后运行这个闭包,输出
3 -- Start
init
3 -- End

3 对于$arr大于1的情况也是类似,最终会得到类似这样的结构

(function() use($carray, 1) {
    echo "1 -- Start\n";
    (function () use ($array, 2) {
        echo "2 -- Start\n";
        (function () use($carray, 1) {
            echo "3 -- Start\n";
            (function () {
                echo "init\n";
            })();
            echo "3 -- End\n";
        })();
        echo "2 -- End\n";
    })();
    echo "1 -- End\n";
})();

使用闭包,把每次迭代过程中的变量锁定,执行时,从外层逐层剥离。这个就是管道的实现方式。

在Laravel中,包Illuminate\Pipeline的管道实现原理与以上的描述类似。而管道的应用,最经典的就是中间件的设计。请求在分发到具体的路由前(或之后),可以设置其经过一些列的中间,要经过的中间件,就是一个个管道,在任何时刻,都可以非常无痛的插入和删除某个中间件,以下是模拟过程:

class VerifyCsrfToken
{
    public static function handle(Closure $next)
    {
        echo "VerifyCsrfToken\n";
        $next();
    }
}

class ShareErrorsFromSession
{
    public static function handle(Closure $next)
    {
        echo "ShareErrorsFromSession\n";
        $next();
    }
}

class StartSession
{
    public static function handle(Closure $next)
    {
        echo "StartSession -- Start\n";
        $next();
        echo "StartSession -- End\n";
    }
}

class AddQueuedCookiesToResponse
{
    public static function handle(Closure $next)
    {
        $next();
        echo "AddQueuedCookiesToResponse\n";
    }
}

class EncryptCookies
{
    public static function handle(Closure $next)
    {
        echo "EncryptCookies -- Start\n";
        $next();
        echo "EncryptCookies -- End\n";
    }
}

class CheckForMaintenanceMode
{
    public static function handle(Closure $next)
    {
        echo "CheckForMaintenanceMode\n";
        $next();
    }
}

class pipeline
{
    protected $hold = null;

    protected $pipes = [];

    public function send($hold)
    {
        $this->hold = $hold;
        return $this;
    }

    public function though(array $pipes)
    {
        $this->pipes = $pipes;
        return $this;
    }

    public function then(Closure $to)
    {
        $fn = function ($stack, $pipe) {
            return function() use ($stack, $pipe) {
                return $pipe::handle($stack);
            };
        };
        $callBack = array_reduce($this->pipes, $fn, $to);

        return $callBack();
    }
}

//////////////////////////////////////
$hold = [];
$pipes = array_reverse([
//    "CheckForMaintenanceMode",
//    "EncryptCookies",
//    "AddQueuedCookiesToResponse",
//    "StartSession",
//    "ShareErrorsFromSession",
//    "VerifyCsrfToken"
]);
$result = (new Pipeline())->though($pipes)->then(function () {
    echo "--------Request to Route------------\n";
});

这样,我们自定义的中间件(数组顺序),如果希望在路由前运行,就在$next()前执行,如果希望路由后执行,就在$next()后运行。当然,在执行某个中间件时(管道),也可以直接return,这样后面的代码就不会执行。

以上的演示代码已经把管道的实现基本说清楚,以下是Illuminate\Pipeline的用法:

<?php

require __DIR__.'/../vendor/autoload.php';

class MyPipe
{
    public function handle($request, $next, $param1 = 1, $param2 = 2)
    {
        echo "MyPipe Handle -- request is $request, param1 is $param1, param2 is $param2 \n";
        $request++;
        $next($request);
    }

    public function fire($request, $next, $param1 = 1, $param2 = 2)
    {
        echo "MyPipe Fire -- request is $request, param1 is $param1, param2 is $param2 \n";
        $request++;
        $next($request);
    }
}
$container = new Illuminate\Container\Container();

$request = 10;
// 三种使用方式
$pipes = [
    // 直接传递一个闭包
    function($request, $next) {
        $request++;
        $next($request);
    },
    // 直接生成一个包含"handle方法的对象",无法传递额外参数
    new MyPipe(),
    // 管道对象生成时需要把容器对象传入,因为需要依靠容器根据字符串类make对象
    // 直接指定一个包含"handle"方法的类名称
    // handle方法如果有超过2个参数,超过的参数可以在冒号后给出,并以逗号分隔多个参数
    "MyPipe:11,22"
];

// 更换默认调用的方法
$pipeline = new Illuminate\Pipeline\Pipeline($container);
$pipeline->via("fire")->send($request)->through($pipes)->then(function ($request) {
    echo "request is $request\n";
    echo "Finish ---\n";
});

// 使用默认的handle方法
$pipeline = new Illuminate\Pipeline\Pipeline($container);
$pipeline->send($request)->through($pipes)->then(function ($request) {
    echo "request is $request\n";
    echo "Finish ---\n";
});

// 输出
MyPipe Fire -- request is 11, param1 is 1, param2 is 2 
MyPipe Fire -- request is 12, param1 is 11, param2 is 22 
request is 13
Finish ---

MyPipe Handle -- request is 11, param1 is 1, param2 is 2 
MyPipe Handle -- request is 12, param1 is 11, param2 is 22 
request is 13
Finish ---

闭包函数应用:

$destination = function () {};

$fn = function ($passable) use ($destination) {
    return $destination($passable);
};
$passable = 1;
// $destination() 和$destination($passable)等同
// $destination($passable) 和 $fn($passable)等同

当要把一个闭包以一个统一的格式执行时(总是带一个或多个参数),通常需要做这样的转换。$destination()这个闭包函数定义时不需要传递参数,但是在调用时传递传输参数过去也不会报错。

demo($destination) 
{
    return function ($passable) use ($destination) {
        return $destination($passable);
    }
}
//
demo($destination) === function ($passable) use ($destination) {
    return $destination($passable);
};
// 调用时就必须是fn(param)格式,外部参数注入到了闭包,闭包中就可以使用到外部传递进来的参数。

传递一个闭包函数给一个函数,这个函数返回了一个包装了这个闭包函数的闭包函数,这样外部的闭包函数执行,就可以把外部的变量传递给内部的闭包函数。

其它管道实现参考:
https://github.com/thephpleague/pipeline

Laravel 认证详解


管理器
管理多个拦截器,通过给定拦截器的名称,可以返回或生产拦截器并返回,那么管理器必定是一个工厂(生产拦截器)。管理器管理多个拦截器,那么必定就有一个默认拦截器的概念。

拦截器
拦截器要认证用户(用户登录),首先客户端需要提供凭证(比如客户端提供的token或对应的用户session中的用户id),如果没有凭证,就直接跳转到登录页(或返回认证失败信息),然后是拿到凭证后(如果是非一次性认证,这个时候还会到持久介质中检查是否已经登录,登录就直接通过 – 持久介质一般就是指session了),还需要知道到什么地方进行比对,这时需要有一个数据源(比如用户表)。这个数据源就是提供者,它向拦截器按照一致的方法提供用户实例。拦截器对于已经通过认证的用户,可能还需要保存认证凭证(下次请求直接通过),有保存认证凭证,当然还要包括注销凭证(用户登出)。

提供者
拦截器面对提供者,通过向提供者获取用户实例,那么提供者必须按照一致的方式编写(这样不同的拦截器就可以调用不同的提供者)。提供者是数据源的抽象,对外提供统一接口,比如根据某个一致的方法返回用户实例。

用户实例
提供者提供用户实例,拦截器使用这个用户实例来比对,很明显,如果用户实例没有一定的规范,也是不行的,比如要比对什么字段不能定死,但是可以定死方法,这个就是接口规范。

总结:
管理器是一个工厂,可以管理和生成拦截器。拦截器需要一个提供者,依赖提供者提供的用户来认证用户。提供者被拦截器调用,它需要提供一致的接口,以使得它可以插入到不同的拦截器中。提供者提供的用户实例也是面向拦截器的,为了能适应不同的拦截器,用户实例也需要按照一致的接口来实现。

由于有一致的接口,所以一个拦截器,可以更换提供者(比如使用不同的数据)。一个提供者,可以插入到不同的拦截器(比如拦截器都使用同一个提供者)。

可扩展:
对于一些常用的拦截器,比如token和session,可以预先实现,同理,对于提供者,也可以预先实现。如果需要插入自定义的拦截器或提供者,只需要按照预定实现相关方法就可以了。

以上就是用户认证的理论知识。以下就是针对这些理论知识进行展开。

管理器 – Illuminate\Auth\AuthManager:

这个类提供了怎么创建默认的Guard的方法(createSessionDriver、createTokenDriver),和怎么创建Provider的方法(createDatabaseProvider),还提供了如何插入自定义的Guard的方法(extend()方法)和插入自定义Provider的方法(provider()方法)。Guard实例的生成,对应的Provider实例必须先生成。

AuthManager中管理不同的Guard,Guard实例的生成需要知道Driver(就是怎么生成Guard实例,比如session,api),还需要知道Provider(数据源),Provider内置支持Database和Eloquent这两种类型,根据类型产生数据源对象(需要实现对应接口)。另外,还有一个默认Guard的概念。有了Guard实例后,需要从Guard实例中取回用户实例,AuthManager中记录默认Guard的用户实例(userResolver)

用户登录认证,首先需要拦截所有的请求,判断是否登录,如果没有登录,跳转到登录页,登录成功后,记录登录信息到会话(下次就知道已登录);如果登录了,跳转到首页, 还可以取到用户实例。

这里的拦截器就是Guard, 登录成功后保存信息,取回用户实例,是Guard需要实现的,另外,如何认证用户需要依赖传递给Guard的Provider,Guard收集认证信息,到Provider中查找比对做出判断。

提供者 – 用户提供者
考虑这样的应用场景:有多个系统公用一套管理员信息,即管理员A用同一套信息可以登录系统1,也可以登录系统2。

这里的管理员需要单独出来,并对外提供API,系统1和系统2以统一的方式调用这些API进行用户认证。

首先需要定义一个UserProriver,需要实现Illuminate\Contracts\Auth\UserProvider接口:

// 通过唯一标示符获取认证模型
public function retrieveById($identifier);
// 通过唯一标示符remember token获取模型
public function retrieveByToken($identifier, $token);
// 通过给定的认证模型更新remember token
public function updateRememberToken(Authenticatable $user, $token);
// 通过给定的凭证获取用户,比如 email 或用户名等等
public function retrieveByCredentials(array $credentials);
// 认证给定的用户和给定的凭证是否符合
public function validateCredentials(Authenticatable $user, array $credentials);

自定义的UserProriver需要实现这些方法(在其中调用外部API),从这个Provider返回的用户实例必须实现了Illuminate\Contracts\Auth\Authenticatable接口的实例(可以认证的):

<?php

namespace Illuminate\Contracts\Auth;

interface Authenticatable
{
    public function getAuthIdentifierName();
    public function getAuthIdentifier();
    public function getAuthPassword();
    public function getRememberToken();
    public function setRememberToken($value);
    public function getRememberTokenName();
}

注:App\User实现了该接口(可认证),实际是使用了Illuminate\Auth\Authenticatable特性。

Illuminate\Auth\GenericUser也实现了这个接口,可以直接套用(DatabaseUserProvider就用它来包装用户实例),否则需要自己实现(比如字段名称不一样),具体实现可以参考Illuminate\Auth\DatabaseUserProvider。

图中MyUser是一个自定义实现,除非需要自定义各种字段,否则没有必要,直接使用GenericUser来对应用户实例即可。

接下来是调用AuthManager的provider()方法来定义新的Provider(数据源):

// App\Providers\AuthServiceProvider
public function boot()
    {
        $this->registerPolicies();

        \Auth::provider('my-user', function() {
            return new MyUserProvider();    // 返回自定义的 user provider
        });
    }

然后修改配置,让Guard使用my-user作为Provider:

// app/config/auth.php
'guards' => [
        'web2' => [
            'driver' => 'session',
            'provider' => 'my-user'  //名字可以随意,只要在providers中找到
        ],
],
'providers' => [
        'my-user' => [
            'driver' => 'my-user'  //名字必须和在\Auth::provider()定义时的一样
        ]
],

这里的’providers’中的’my-user’是否需要其它的参数(driver参数必须,并且和定义时的值一样),是由自定义的Provider决定的,可以根据情况添加。

设置 config\auth.php 的 defaults.guard 为web2,就可以使用自定义的Provider进行认证了。

拦截器 – Guard
考虑这样的应用场景:如果需要修改登录认证逻辑,比如对认证做一些额外的处理,默认提供的Guard不能满足的情况。

Illuminate\Contracts\Auth\StatefulGuard接口在Illuminate\Contracts\Auth\Guard
上提供了额外方法,比如:login() attempt() logout()等(Session认证必须)。

每个Guard实际都注入了Illuminate\Auth\GuardHelpers, 这是一些辅助方法,比如:check() 、guest()通用的方式实现 。Guard中进行拦截是实际是跟对应的UserProvider进行交互,取回用户实例(必须实现Illuminate\Contracts\Auth\Authenticatable接口),

Illuminate\Contracts\Auth\Guard接口:

// 判断当前用户是否登录
public function check();
// 判断当前用户是否是游客(未登录)
public function guest();
// 获取当前认证的用户
public function user();
// 获取当前认证用户的 id,严格来说不一定是 id,应该是上个模型中定义的唯一的字段名
public function id();
// 根据提供的消息认证用户
public function validate(array $credentials = []);
// 设置当前用户
public function setUser(Authenticatable $user);

Illuminate\Contracts\Auth\StatefulGuard继承Guard接口,还提供了:

// 尝试根据提供的凭证验证用户是否合法
public function attempt(array $credentials = [], $remember = false);
// 一次性登录,不记录session or cookie
public function once(array $credentials = []);
// 登录用户,通常在验证成功后记录 session 和 cookie 
public function login(Authenticatable $user, $remember = false);
// 使用用户 id 登录
public function loginUsingId($id, $remember = false);
// 使用用户 ID 登录,但是不记录 session 和 cookie
public function onceUsingId($id);
// 通过 cookie 中的 remember token 自动登录
public function viaRemember();
// 登出
public function logout();

预定义的拦截器:
1 RequestGuard
Illuminate\Auth\RequestGuard
RequestGuard是一个非常简单的Guard。RequestGuard 是通过传入一个闭包来认证的。可以通过调用 Auth::viaRequest 添加一个自定义的 RequestGuard.(每个请求,都执行这个闭包,这个闭包负责怎么认证用户,当然,数据来源,怎么比对数据,都需要在这个闭包中完成)

2 SessionGuard
Illuminate\Auth\SessionGuard
SessionGuard 是 Laravel web 认证默认的 guard(用户认证后,记录到session,第二次请求就不会再次认证).

3 TokenGuard
Illuminate\Auth\TokenGuard
TokenGuard 适用于无状态 api 认证,通过 token 认证(请求中携带token,用token认证用户,每次请求都会进行认证).

要实现自定义的Guard,可以参考以上的实现,然后调用AuthManager的extend()方法来定义新的Guard(Driver):

// App\Providers\AuthServiceProvider
public function boot()
    {
        $this->registerPolicies();

        \Auth::extend('web2', function(){
              return new MyGuard();
        });
    }

Guard需要使用到Provider(构造函数中注入),完成验证,检查等操作。

另外
Laravel支持在认证过程中触发多种事件,可以在的EventServiceProvider中监听这些事件:

protected $listen = [
    'Illuminate\Auth\Events\Registered' => [
        'App\Listeners\LogRegisteredUser',
    ],


    'Illuminate\Auth\Events\Attempting' => [
        'App\Listeners\LogAuthenticationAttempt',
    ],

    'Illuminate\Auth\Events\Authenticated' => [
        'App\Listeners\LogAuthenticated',
    ],

    'Illuminate\Auth\Events\Login' => [
        'App\Listeners\LogSuccessfulLogin',
    ],

    'Illuminate\Auth\Events\Failed' => [
    'App\Listeners\LogFailedLogin',
],

    'Illuminate\Auth\Events\Logout' => [
        'App\Listeners\LogSuccessfulLogout',
    ],

    'Illuminate\Auth\Events\Lockout' => [
        'App\Listeners\LogLockout',
    ],

    'Illuminate\Auth\Events\PasswordReset' => [
        'App\Listeners\LogPasswordReset',
    ],
];

Composer中replace属性的作用

原始解释:
“Lists packages that are replaced by this package. This allows you to fork a package, publish it under a different name with its own version numbers, while packages requiring the original package continue to work with your fork because it replaces the original package.”

列出被当前包替换的包。这允许你fork一个包,并以不同的名称和它自己的版本号进行发布,由于替换了原始的包,当包依赖原始包时可以使用fork包继续工作。

实际上,第一句话已经解释清楚了,就是当前包替换哪个(或哪些)包。比如:

{
“name": "b/b",
"replace": {
    "a/a": "x.y.z"
}
}

就是b/b这个包替换了a/a这个对应了x.y.z版本的包。如果b/b包中的replace列出了多个包,那么它将替换多个包。在Composer解决依赖的过程中,如果遇到了replace,那么原始的包会被移除,所以它的应用场景非常清晰,就是原始包停止维护了,但是你的项目或包直接或间接依赖一个原始的包,为了可以修复原始包的bug,你可以fork这个包,并在这个包中声明要替换原始包,然后在你的应用或包中依赖这个你fork的包,这样就可以全局替换原始包(原始包被移除)。

由于是包替换,很明显也可能引入安全问题,比如某个包被替换,替换的包中包含恶意代码。

关于replace的第二段解释:
”This is also useful for packages that contain sub-packages, for example the main symfony/symfony package contains all the Symfony Components which are also available as individual packages. If you require the main package it will automatically fulfill any requirement of one of the individual components, since it replaces them.“

这里描述的就是现代框架的组织方式。一个框架(或一个大组件),是很多子包组成的,每个子包都可以单独使用,通常每个子包都会以只读的方式fork到另一个包名,当单独使用时,可以依赖这个只读的包,当依赖整个框架时,框架中声明了替换,则可以移除单独依赖时下载的包。

比如laravel/framework是由很多包组成的,每个包都以只读的方式fork到另一个包名称:

// https://github.com/laravel/framework
"replace": {
        "illuminate/auth": "self.version",
        "illuminate/broadcasting": "self.version",
        "illuminate/bus": "self.version",
        "illuminate/cache": "self.version",
        "illuminate/config": "self.version",
        "illuminate/console": "self.version",
        "illuminate/container": "self.version"
}

这个包包括了子包illuminate/container,而它被只读方式fork了一份到https://github.com/illuminate/container,所以当要仅用这个组件时,可以composer require illuminate/container即可。在依赖了illuminate/container后,如何由依赖laravel/framework,这个时候会引起命名冲突,但是laravel/framework中声明了illuminate/container这个包被laravel/framework替换,所以在依赖laravel/framework后,illuminate/container独立的下载将被移除,从而解决了名称冲突。

第一个用法,提供了一个全局包替换的机会。第二个用法,提供了一个现代化的框架组织方式。

PHP 反射

PHP对反射提供了完整的支持。http://php.net/manual/zh/intro.reflection.php

类图简单结构:

命令行工具:

php —help
  --rf <name>      Show information about function <name>.
  --rc <name>      Show information about class <name>.
  --re <name>      Show information about extension <name>.
  --rz <name>      Show information about Zend extension <name>.
  --ri <name>      Show configuration for extension <name>.

查看一个实例:

class B {
    public function test()
    {
        echo date("Y-m-d H:i:s") . "\n";
    }
}

class A {
    public $b = null;

    public $try = 0;

    public function __construct(B $b, $try = 111)
    {
        $this->b = $b;
        $this->try = $try;
    }

    public function test()
    {
        echo "B-------\n";
        $this->b->test();
        echo "A--------\n";
        echo date("Y-m-d H:i:s") . "\n";
        var_dump($this->try);
        echo "\n";
    }
}

// 反射类
$reflector = new ReflectionClass(A::class);
// 是否可实例化
$instantiable = $reflector->isInstantiable();
if ($instantiable) {
    // 取到构造函数
    $constructor = $reflector->getConstructor();
    // 取到构造函数的参数
    $dependencies = $constructor->getParameters();
    print_r($constructor);
    print_r($dependencies);
    $instances = [];
    // 根据构造函数的参数,确定依赖
    foreach ($dependencies as $idx => $dependency) {
        // $dependency是一个ReflectionParameter对象实例
        // getClass()取回参数对应的类原型定义(如果不是类则返回null)
        $class = $dependency->getClass();
        if (is_null($class)) {
           echo $dependency->getName() . " - Class is Null \n";
           // 确定参数是否可选,如果可选则使用默认值
           if ($dependency->isOptional()) {
               $instances[$idx] = $dependency->getDefaultValue();
           } else {
               throw new Exception();
           }
        } else {
            echo $dependency->getName() . " - Class is $class->name \n";
            try {
                // 构建对象
                $className = $class->name;
                $instances[$idx] = new $className();
            } catch (Exception $e) {
                if ($dependency->isOptional()) {
                    $instances[$idx] = $dependency->getDefaultValue();
                }
                throw $e;
            }
        }
    }
    // 把取回的依赖注入构造函数
    $a = $reflector->newInstanceArgs($instances);
    $a->test();
}

这个实例中,A类的构造函数需要注入B类对象。利用反射的支持,先取回A类的构造函数,然后分析构造函数参数,根据参数自动实例化依赖,最后生成对象。这里有一个问题,如果B类实例化时需要依赖其它类对象注入该怎么办? 实际这个就是一个递归过程而已。如果需要构建一个对象,对象的依赖自动解决(依赖对象自动生成),这个只有利用反射能实现。

反射也是实现依赖注入容器的基础。

事件循环:JavaScript、Node.js、PHP-Swoole、PHP-workerman

在浏览器端,JavaScript的主要是与用户互动,以及操作DOM。为了避免复杂性,JavaScript被设计为只能运行在单进程单线程中。H5提出了Web Worker标准,允许JavaScript创建多个线程,但子线程受主线程控制,且不得操作DOM。

在JavaScript中,任务分为同步任务和异步任务(异步任务不堵塞当前线程),所有同步任务都在主线程上执行,形成一个执行栈,另外存在一个“任务队列”,如果异步任务有了结果,就在“任务队列”中添加一个事件(对应回调),一旦“执行栈”中的所有同步任务执行完毕,就会读取“任务队列”,取出事件对应的回调,然后进入执行栈执行回调(注:任务队列中对应的是事件,事件可以对应多个回调,回调的执行也依赖事件对象记录的原始参数)。

事件的产生包括用户比如用户点击,异步调用后触发的事件,取出事件是一个循环过程(执行栈空),所以这个过程叫事件循环(event loop)。

Node.js中的事件循环原理上和JavaScript中的(应该是浏览器)并没有很大不同。Node.js中为了对付大量的链接,使用了epoll,对于异步任务,使用了一个线程池来模拟,回调的执行全部落在主线程中,所以它是单进程单线程的(这个也是为何面对CPU密集运算时的场景不合适,尽管还有其它的CPU是空闲的,因为执行回调的,仅一个线程)。

PHP中的一个扩展Swoole,理念上和Node.js是差不多的,但是架构上有很大不同。Swoole启动后,首先启动一个主进程,在这个进程内其它若干个React线程,这些线程专门负责监听,接收数据和响应,然后启动一个manager进程和一组Worker进程和若干task进程,manager进程主要用来监控worker进程和task进程(比如退出重启等),Worker可以把耗时的任务投递给task进程,task执行时同步堵塞的,执行完毕后通过进程间通信的方式通知Worker进程。每个Worker进程维护一个事件循环,并在Worker进程内执行回调(可以应用到多核CPU)。

PHP-workerman相对Swoole来说,架构上就比较简单。它相当于只有Swoole的Worker进程这部分。每个Worker都相互独立的监听端口,执行回调,响应数据等。

Mac 开发环境搭建

进入Mac的默认Shell终端,安装Homebrew工具:

Homebrew是一个包管理器,用于在Mac上安装一些OS X没有的UNIX工具(比如著名的wget)。 Homebrew将这些工具统统安装到了/usr/local/Cellar目录并在/usr/local/bin中创建符号链接。

官方网站:
http://brew.sh/index_zh-cn.html

安装:

/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

#检查安装:
brew -v

Homebrew 常用命令

brew install git

brew uninstall git

brew search git

brew list

brew update

#更新某具体软件
brew upgrade git

#查看软件信息
brew [info | home] [FORMULA...]

#和upgrade一样,单个软件删除 和 所有程序删除。清理所有已安装软件包的历史老版本
brew cleanup git 
brew cleanup

#查看哪些已安装的程序需要更新
brew outdated

Homebrew 卸载

    cd `brew --prefix`
    rm -rf Cellar
    brew prune 
    rm `git ls-files` 
    rm -rf Library .git .gitignore bin/brew
    rm -rf README.md share/man/man1/brew
    rm -rf Library/Homebrew Library/Aliases 
    rm -rf Library/Formula Library/Contributions
    rm -rf ~/Library/Caches/Homebrew

一 安装PHP

#搜索,会出现几个分之,比如PHP56 PHP71
brew search php
#过滤,只要71分支,提供了非常多扩展包
brew search php71
#安装(选择需要的扩展包)
brew install homebrew/php/php71 homebrew/php/php71-apcu homebrew/php/php71-redis homebrew/php/php71-mongodb homebrew/php/php71-opcache omebrew/php/php71-swoole

大部分PHP的模块,都包含在了homebrew/php/php71中,是编译到内核的(非动态模块),上面的apcu,redis,mogondb,swoole是动态模块,模块安装位置:/usr/local/opt/,比如:/usr/local/opt/php71-apcu/apcu.so。配置文件自然是/usr/local/etc/php/7.1/php.ini,扩展的配置放在/usr/local/etc/php/7.1/conf.d/*.ini。

php -v
php -m

编译到内核的模块确实是大而全,然后还需要调整一下php.ini的配置(才能符合开发环境要求):

#设置时区
date.timezone = Asia/Shanghai
 
#CGI相关参数,实际上建议修改的是force_redirect,其它均保留默认值
cgi.force_redirect = 0   #默认为1,改为0
cgi.fix_pathinfo = 1     #默认是1,保留
fastcgi.impersonate = 1  #默认是1,保留
cgi.rfc2616_headers = 0  #默认是0,保留

#其它参数调整,根据实际情况调整
upload_max_filesize = 64M
max_execution_time = 1200
max_input_time = 600
max_input_nesting_level = 128
max_input_vars = 2048
memory_limit = 1024M
 
post_max_size = 64M

如果要启动PHPFPM,FPM主配置文件/usr/local/etc/php/7.1/php-fpm.conf,池配置在/usr/local/etc/php/7.1/php-fpm.d中,需要注意的是,池配置中,默认的运行用户是和用户组均为_www,所以需要检查文件的权限,保证对_www具有读和执行(默认是符合的),如果要写入,那么还需要保证对应的文件夹有被写入的权限。

启动PHPFPM,由于php-fpm这个命令放入到了/usr/local/sbin中,默认shell并不搜索这个路径,所以要想添加环境变量:

#设置环境变量
export PATH="/usr/local/sbin:$PATH"  
echo 'export PATH="/usr/local/sbin:$PATH"' >> ~/.bash_profile

#确认命令能找到
which php-fpm
which php71-fpm

#手动启动,PHPFPM可以不使用root身份启动(user和group指令无用),会使用当前用户运行
sudo php71-fpm start
sudo php71-fpm stop

对于开发环境,PHPFPM可以不用启动,直接使用PHP内置的HTTP服务器也可以。

二 安装Nginx

brew install --with-http2 nginx  

如果要绑定到80端口,那么Nginx就必须以root身份运行。默认的server配置位于(可改):/usr/local/etc/nginx/servers。可以往里面方式配置:

server {
    listen 80;
    #listen 443 ssl http2;
    server_name test.app;
    root "/Users/xx/www/test/public";
 
    index index.html index.htm index.php;
 
    charset utf-8;
 
    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }
 
    location = /favicon.ico { access_log off; log_not_found off; }
    location = /robots.txt  { access_log off; log_not_found off; }
 
    access_log off;
 
    sendfile off;
 
    location ~ \.php$ {
        client_max_body_size 64M;
        fastcgi_intercept_errors off;
        fastcgi_connect_timeout 300;
        fastcgi_send_timeout 300;
        fastcgi_read_timeout 300;
        fastcgi_buffer_size 32k;
        fastcgi_buffers 64 32k;
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_pass 127.0.0.1:9000;
        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include fastcgi_params;
    }
 
    location ~ /\.ht {
        deny all;
    }
 
    #ssl_certificate     /etc/nginx/ssl/test.app.crt;
    #ssl_certificate_key /etc/nginx/ssl/test.app.key;
}

Nginx的主配置文件设置的启动user一般应该和PHPFPM相同,或者需要保证Nginx对文件具备读和执行的权限。如果是文件上传,还需要确保Nginx对临时中间文件夹具备写入权限。

启动关闭等:

sudo nginx -t
sudo nginx -s start
sudo nginx -s stop

三 安装MySQL

#安装最新版本(5.7.xx)
brew install mysql

#确定搜索路径:
which mysqld
mysqld —verbose —help | grep -A 1 ‘Default options’

/etc/my.cnf  /etc/mysql/my.cnf  /usr/local/etc/my.cnf  ~/.my.cnf

#
mysql.server start
mysql_secure_installation

# 停止
mysql.server stop

MySQL不需要以root身份启动。

四、安装Tomcat

brew search tomcat
==> Searching local taps...
tomcat ✔            tomcat-native       tomcat@6            tomcat@7            tomcat@8
==> Searching taps on GitHub...
==> Searching blacklisted, migrated and deleted formulae...

# 安装最新版本
brew install tomcat

#安装指定版本
brew install tomcat@8

启动关闭:

catalina --help
Using CATALINA_BASE:   /usr/local/Cellar/tomcat/9.0.6/libexec
Using CATALINA_HOME:   /usr/local/Cellar/tomcat/9.0.6/libexec
Using CATALINA_TMPDIR: /usr/local/Cellar/tomcat/9.0.6/libexec/temp
Using JRE_HOME:        /Library/Java/JavaVirtualMachines/jdk1.8.0_144.jdk/Contents/Home
Using CLASSPATH:       /usr/local/Cellar/tomcat/9.0.6/libexec/bin/bootstrap.jar:/usr/local/Cellar/tomcat/9.0.6/libexec/bin/tomcat-juli.jar
Usage: catalina.sh ( commands ... )
commands:
  debug             Start Catalina in a debugger
  debug -security   Debug Catalina with a security manager
  jpda start        Start Catalina under JPDA debugger
  run               Start Catalina in the current window
  run -security     Start in the current window with security manager
  start             Start Catalina in a separate window
  start -security   Start in a separate window with security manager
  stop              Stop Catalina, waiting up to 5 seconds for the process to end
  stop n            Stop Catalina, waiting up to n seconds for the process to end
  stop -force       Stop Catalina, wait up to 5 seconds and then use kill -KILL if still running
  stop n -force     Stop Catalina, wait up to n seconds and then use kill -KILL if still running
  configtest        Run a basic syntax check on server.xml - check exit code for result
  version           What version of tomcat are you running?
Note: Waiting for the process to end and use of the -force option require that $CATALINA_PID is defined

关于启动问题:
在Mac下,如果要开机启动,可以参考如下配置(一般不需要):

#Nginx
cp /usr/local/opt/nginx/homebrew.mxcl.nginx.plist ~/Library/LaunchAgents/
launchctl load ~/Library/LaunchAgents/homebrew.mxcl.nginx.plist  

# PHP-FPM
cp /usr/local/opt/php70/homebrew.mxcl.php71.plist ~/Library/LaunchAgents/  
launchctl load ~/Library/LaunchAgents/homebrew.mxcl.php71.plist  

# MySQL
cp /usr/local/opt/mysql/homebrew.mxcl.mysql.plist ~/Library/LaunchAgents/  
launchctl load ~/Library/LaunchAgents/homebrew.mxcl.mysql.plist

## 卸载
launchctl unload ~/Library/LaunchAgents/homebrew.mxcl.nginx.plist  
launchctl unload ~/Library/LaunchAgents/homebrew.mxcl.php71.plist  
launchctl unload ~/Library/LaunchAgents/homebrew.mxcl.mysql.plist  
rm ~/Library/LaunchAgents/homebrew.mxcl.nginx.plist  
rm ~/Library/LaunchAgents/homebrew.mxcl.php71.plist  
rm ~/Library/LaunchAgents/homebrew.mxcl.mysql.plist

更加实际的方法:进入操作系统后,启动Nginx和PHPFPM(因为要sudo,需要输入密码),MySQL则在需要时启动,比如本地一般链接远程数据库。所以可以这个别名:(往.bash_profile中写入)

alias servers.start='sudo nginx && php-fpm --fpm-config /usr/local/etc/php/7.1/php-fpm.conf -D'
alias servers.stop='sudo bash -c "killall -9 php-fpm && nginx -s stop"'                       
alias nginx.logs='tail -f /usr/local/opt/nginx/access.log'
alias nginx.errors='tail -f /usr/local/opt/nginx/error.log'

遇到问题:
1 Nginx启动提示

nginx: [emerg] getgrnam("

提示大体就是找不到用户组的意思。在Nginx配置中,user如果只指定了用户名,默认会去寻找同名的用户组,在Mac中,用户不一定对应一个同名的用户组,所以出现这种情况就是需要明确指定存在的用户组,可以通过如下方式来确定用户和用户组:

#当前登录的用户名
whoami
www

#确认用户组(可见www的uid是502,对应的组id是20,名称是staff)
id
uid=502(www) gid=20(staff) groups=20(staff),12(everyone)

把www和staff对应填入,错误提示消失。

Hack PHP: 黑你没商量

PHP7和它之前的版本比较,有了巨大的性能提升。对于一门具有20多年历史的语言,还能有如此大的性能提升,确实不容易。不过这也间接说明PHP7之前的版本有点烂吧。PHP7之后的PHP7.1和PHP7.2,都没有加入JIT,说好的JIT在PHP7中被跳票了。PHP的历史包袱是很重的,需要多方面兼顾。说PHP7接近HHVM运行PHP的性能,这其实是需要打问号的。从原理上来说,一个没有JIT的运行引擎会比一个具备JIT的运行引擎更快应该不可能,否则PHP还搞什么JIT。

PHP是弱类型的,这个对JIT来说不太友好。在运行时,需要类型推断,而且需要推断正确才能发挥JIT的作用。对于强类型语言,JIT就好做的多,于是出现了Hack(Hack PHP一把的意思),它引入了类型系统,用Hack写的PHP,HHVM的JIT可以充分发挥,从这个角度来说,HHVM是向JVM看齐的(它的多线程架构也和JAVA类似)。

所以,当前的PHP7和HHVM下的PHP相比,差的何止一点点。PHP7.0引入了类型系统(默认关闭),PHP7.1引入了类型推断优化opcode,PHP7.2还是看不到JIT的影子。这个大概就是PHP自身坚持弱类型,但是又要打造一个实用的JIT之间的矛盾。如果没有HHVM的出现,估计PHP压根没有打造自己的JIT的打算,这事本身就极具悲剧色彩,说HHVM拯救了PHP不为过。

从纯计算的角度,C/C++比Java快,Java比Node.js快,Node.js比PHP快。Java比PHP快一个数量级,不奇怪。Node.js携带的V8引擎自带JIT,可能已经到达极限,瓶颈在弱类型。目前Node.js在吞噬PHP的市场,它的生态虽然很火爆,但是工程化比PHP还是差很多。Java虽然运行很快,但是由于其臃肿的体积,在Web领域,无法撼动PHP的市场。多语言并存,相互协作已是常态。

PHP引入JIT还是非常值得期待的,从现有思路来看,官方希望在opcode上进行透明操作,或者提供一个开关也是一个不错的做法。比如对应新的项目,开启类型系统,开启JIT。