月度归档:2017年02月

使用Vagrant构建本地开发环境

首先安装VirtualBox(https://www.virtualbox.org) 和 Vagrant(https://www.vagrantup.com)。

Vagrant是一个基于Ruby的工具,用于创建和部署虚拟化开发环境。简单来说,Vagrant基于一个虚拟机的(虚拟机运行环境可以是VirtualBox,也可是VMWare),并在它之上,根据自定义配置,构建开发环境,比如:宿主机和虚拟机的文件共享,站点配置,建立数据库等。一般是先安装一个虚拟操作系统,上面安装好必须的应用软件(如Nginx、MySQL、PHP等),然后按照格式打包成一个Box(Vagrant的称呼),Vagrant与VituralBox或VMWare进行交互,根据配置配置把Box还原为一个具体的虚拟机,这些是这个工具主要的工作内容。

操作Vagrant有如下几个步骤:
1 添加Box
Vagrant安装之后,会在~目录中生成.vagrant.d文件夹,里面的boxes是已经添加到Vagrant的box,tmp中是下下载box时的临时文件,如果下载box失败,可以到这里删除临时文件。

#https://atlas.hashicorp.com/laravel/boxes/homestead
vagrant box add laravel/homestead

vagrant-box-search

vagrant-box-laravel-homestead
由于已知原因,这样可以成功的几率很低。所以,我们需要离线下载。比如上面的laravel/homestead跳出的URL是:

https://atlas.hashicorp.com/laravel/boxes/homestead/versions/1.1.0/providers/virtualbox.box

然后用代理下载。下载完毕后得到文件,比如叫homestead-virtualbox.box,由于box是有版本的,而离线下载的情况,box的元数据丢失了,所以需要在homestead-virtualbox.box同类目下建立一个叫metadata.json的文件:

{
    "name": "laravel/homestead",
    "versions": [{
        "version": "1.1.0",
        "providers": [{
            "name": "virtualbox",
            "url": "file://homestead-virtualbox.box"
        }]
    }]
}

然后运行vagrant add metadata.json,这样就跟在线添加box是一样的了。如果直接运行vagrant box add homestead-virtualbox.box,在vagrant中是无法启动的,进入~/.vagrant.d\boxes\laravel-VAGRANTSLASH-homestead查看就很明白,它是根据版本号来存放的。

#查看已经添加的box
vagrant box list

laravel/homestead (virtualbox, 1.1.0)

后面的virtualbox表明这个box是使用VirtualBox来运行的,1.1.0表示这个盒子的版本。

2 在工作目录下生成启动文件
进入到具体的工作目录,运行vagrant init laravel/homestead,就会在当前目录下生成Vagrantfile文件,它需要启动的就是之前添加的laravel/homestead这个box,Vagrantfile是一个简单的Ruby脚本,是很容易看懂的。Vagrantfile文件可以读入一个配置文件,也可以把配置直接全部写入Vagrantfile

3 启动
启动文件Vagrantfile建立好后,在当前工作目录下运行vagrant up就可以启动。这个命令会搜索当前目录下的Vagrantfile,如果找不到则会到~目录寻找Vagrantfile。Box启动时,会在和Vagrantfile相同的目录下生成一个.vagrant目录,里面保存了启动后相关的文件,box消毁后会自动删除相关文件。

如果把所有文件都硬编码到Vagrantfile,明显不是最佳实践,所以一般都会把配置提取到一个外部的配置文件,配置文件格式和位置就完全取决Vagrantfile能不能以及如何处理。

只要找到Vagrantfile,并且指定的box存在,box就可以启动。但是Vagrantfile可以配置的参数实际是很多的,所以最好是在Vagrantfile实现编程,把外部的文件读取进来,然后传递给Vagrant让其启动Box。Laravel为其提供的laravel/homestead盒子对应了一个项目()https://github.com/laravel/homestead),说实在的,这个项目仅仅提供了一段Ruby脚本而已(提供Vagrantfile,核心是scripts/homestead.rb),直接克隆下来:

#也可以直接下在Zip包
git clone https://github.com/laravel/homestead.git Homestead

#然后运行init脚本
init 

这个脚本实际就仅仅拷贝几个文件(作为模子),怪胎就在于,它跑到~目录下生成.homestead目录,拷贝进去的文件是after.sh, aliases, Homestead.yaml。主要就是这个Homestead.yaml文件,在其提供的Vagrantfile的文件中(它实现了编程),会读入这个配置文件,所以只需要修改这个配置文件即可:

---
ip: "192.168.10.10"
memory: 2048
cpus: 1
provider: virtualbox

authorize: ~/.ssh/id_rsa.pub

name: homestead

keys:
    - ~/.ssh/id_rsa

folders:
    - map: E:/var/www
      to: /home/vagrant/www

sites:
    - map: ebt.app
      to: /home/vagrant/www/ebt/public
    - map: elc.app
      to: /home/vagrant/www/elc/public

databases:
    - ebt

更多的参数,参考scripts/homestead.rb就可以很容易提取出来。不过这里提一下SSH的配置,它把~/.ssh/id_rsa.pub这个公钥塞进到Linux的vagrant用于的home目录中,换句话说就是,本机需要配置了RSA秘钥对:

#
ssh-keygen -t rsa -C "you@homestead"

所以,box启动后可以直接使用本地私钥ssh到其中(ip:192.168.10.10, port:22, user:vagrant)。另外,box启动后也会动态的产生一个RSA秘钥对,并且这个公钥也会塞进vagrant用户,所以也可以使用这个动态产生的RSA秘钥对登录,只是机器销毁后这个秘钥对也会被删除,重启后又产生新的,多少有点不方便。

另外关于端口映射,比如8000转发到 80,实际就是把本机(127.0.0.1)的8000端口转发到客户机(192.168.10.10)的80端口上,看起来是虚拟机层次提供的实现。所以域名可以直接绑定到127.0.0.1,只是在端口号上要处理一下。

在虚拟机中共享宿主机文件

在VMware中,可以使用虚拟机本身提供的文件共享功能实现文件共享:
vmware-share
这里是选定宿主机需要曝光的文件夹。客户机要能读取到这个文件夹,还需要借助VMware提供的VMware Tool,进入客户机后,点击VMware的虚拟机 – 安装VMware Tool:
vmtool-install
1 如果是图形界面,一般就可以自动完成安装
2 如果非图形界面,可能需要手动安装

mount /dev/cdrom2 /mnt
cd /mnt
cp VMwareTools-xxxx.tar.gz ~
cd ~
tar zxvf VMwareTools-xxxx.tar.gz
cd vmware-tools-distrib
# 最终运行,根据提示完成安装
./vmware-install.pl

工具安装完毕。运行:

/usr/bin/vmhgfs-fuse .host:/  /mnt

这样共享文件夹就mount到了/mnt,进入/mnt应该就可以看到宿主机的共享文件了。不过如果重启,这个mount点就消失了,所以还需要如下操作:

#在CentOS 7中执行
chmod +x /etc/rc.d/rc.local

#添加
vi /etc/rc.local

/usr/bin/vmhgfs-fuse .host:/  /mnt

不过这视乎仅能做文件共享,当配置Nginx指向到共享文件夹时,提示权限问题而无法使用。

在VirtualBox中,其提供的文件共享功能就比较实用:
vbox
为了让共享文件可以工作,需要在客户机中安装增强工具。

除了利用虚拟软件提供的支持,如果宿主机是Windows,SMB文件共享方案是最好的选择,SMB在Windows下是内置的,客户机可以是Windows或Linux, 如果是Linux,一般需要安装cifs软件包:

yum search cifs
cifs-utils-6.2-9.el7.x86_64

#CentOS 
yum install cifs-utils
#Ubuntu
sudo apt-get install cifs-utils

#mount共享文件夹
mount -t cifs //192.168.1.121/www /var/www -o username=administrator,password='xxx',uid=1000,gid=1000

如果宿主机是Linux,客户机也是Linux,最好的共享方式就是使用NFS(NFS不支持Windows)。

Laravel容器与服务提供者

Laravel容器继承图:
laravel-container
这里的Illuminate\Container类实现了Illuminate\Contracts\Container\Container和ArrayAccess接口提供的方法,Illuminate\Foundation\Application实现了Illuminate\Contracts\Foundation\Application接口提供的方法。注:Illuminate\Contracts\Foundation\Application接口继承自Illuminate\Contracts\Container\Container,大体上从语义上,Application也必须是一个容器,要实现容器提供的方法。

在一个服务提供者中,可以通过$this->app变量来访问到应用容器。也可以在任何地方使用app()全局方法获取到。应用容器提供了很多方法,主要分两类:一类是基本容器方法 、一类是服务提供者容器方法。

Laravel中对于容器中的对象叫服务,一般可以认为是一个类名(或者是接口名,用接口名对应具体的类),由于类名可能比较长,所以引入了别名,别名就是一个映射关系。

一般情况,一个服务要可用,首先需要绑定到容器,容器根据绑定生成实例:

$app = app();

$app->bind(‘Ifeeline\Test’, function($app){
	return new Ifeeline\TestInstance();
});

当调用make()时就会取出绑定,然后生成实例。具体实现上就稍微复杂一些:

public function bind($abstract, $concrete = null, $shared = false)
{
    // If the given types are actually an array, we will assume an alias is being
    // defined and will grab this "real" abstract class name and register this
    // alias with the container so that it can be used as a shortcut for it.
    if (is_array($abstract)) {
        list($abstract, $alias) = $this->extractAlias($abstract);

        $this->alias($abstract, $alias);
    }

    // If no concrete type was given, we will simply set the concrete type to the
    // abstract type. This will allow concrete type to be registered as shared
    // without being forced to state their classes in both of the parameter.
    $this->dropStaleInstances($abstract);

    if (is_null($concrete)) {
        $concrete = $abstract;
    }

    // If the factory is not a Closure, it means it is just a class name which is
    // bound into this container to the abstract type and we will just wrap it
    // up inside its own Closure to give us more convenience when extending.
    if (! $concrete instanceof Closure) {
        $concrete = $this->getClosure($abstract, $concrete);
    }

    $this->bindings[$abstract] = compact('concrete', 'shared');

    // If the abstract type was already resolved in this container we'll fire the
    // rebound listener so that any objects which have already gotten resolved
    // can have their copy of the object updated via the listener callbacks.
    if ($this->resolved($abstract)) {
        $this->rebound($abstract);
    }
}

第一个参数可以是一个数组,用来指定类型的同时指定别名,举例:
Ifeeline\ExampleClass 对应别名为 example-class:

// 别名类似
// use XXX\YY as XX
$aliases[‘example-class’] = ‘Ifeeline\ExampleClass’;

$app->bind([‘Ifeeline\ExampleClass’ => ‘example-class’], function($app) {});

第二参数可以是空,也可以是字符串,或者是闭包、第三参数表明这个绑定生成的对象是否是共享的。

如果第二参数未提供,就是直接实例化类。如果提供了字符串,就是实例化这个类型的对象。如果是闭包,对象的实例化由该闭包完成。

对于希望用一个接口对应某个实现的情况:

$this->app->bind('接口', '接口实现类');

从实现上可以看到,一个bind()调用实际是产生如下内容:

$this->bindings[‘类名’] = [
‘concrete’ => ,
‘shared’ =>
];

其中concrete必定对应一个闭包函数,这个闭包函数第一个参数是容器实例,第二参数是生成实例时需要用到的参数,所以,如果自己传递一个闭包进去,可以这样:

$app->bind(‘类名’, function($app, $params) {}); 

如果调用bind()时第二参数不是闭包则会自动构建一个闭包函数,只是这个闭包函数第二参数为空而已。自动产生的闭包已经指定了如何产生对象,比如如果bind()未提供第二参数,就是直接调用$app->build()生成对象而已,如果提供了则使用$app->make()来生成对象。

容器的build和make方法是不同的,build()方法是根据反射来生成对象,具体来说是取出构造方法,然后解析构造方法的依赖,自动注入然后实例化对象。而make()方法直接从已有实例中返回实例,如果没有则从binding中取回concrete,由它来决定接下来是执行build方法还是make方法(递归)。总体来说,最终产生对象的,还是会调用到build方法。make方法最后还会根据binding中的shared来决定这个实例是否放入$this->instances,一旦放入这个数组,那么以后都用它。

如果需要产生一个单例绑定,可以使用$app->singlton(),它实际是bind()方法的封装而已。

如果希望把一个实例直接放入$instances数组,可以用$app->instance(‘类目’, new Object())。

如果要直接产生一个类的实例,不需要首先绑定,直接调用make即可。换句话说,绑定是在需要把接口对应到实现类,或者需要对生成的实例做配置时(传递闭包)才需要这样做。

不管信不信,对于一个容器,就只有这些内容。其它的都是一些扩展。比如绑定可以做所谓的上下文绑定,绑定分标签,容器事件等。

接下来就是需要知道在框架初始化的时候,绑定是怎么写入进来的,这个就是ServiceProvider的概念。

框架初始化流程:
实例化Application后,建设两个单例Kernel绑定到容器,然后调用Kernel的handle方法,这个方法最终会调用到Kernel的bootstrap()方法,这个方法最终把它自己的bootstrap数组(bootstrappers方法返回)传递到容器的bootstrapWith
方法,这个方法循环实例化从Kernel传递过来的bootstrap数组,然后调用其bootstrap方法,Kernel传递过来的bootstrap数组在其父类中(子类可覆盖):

//Illuminate\Foundation\Http\Kernel
protected $bootstrappers = [
    'Illuminate\Foundation\Bootstrap\DetectEnvironment',
    'Illuminate\Foundation\Bootstrap\LoadConfiguration',
    'Illuminate\Foundation\Bootstrap\ConfigureLogging',
    'Illuminate\Foundation\Bootstrap\HandleExceptions',
    'Illuminate\Foundation\Bootstrap\RegisterFacades',
    'Illuminate\Foundation\Bootstrap\RegisterProviders',
    'Illuminate\Foundation\Bootstrap\BootProviders',
];

//Illuminate\Foundation\Console\Kernel
protected $bootstrappers = [
    'Illuminate\Foundation\Bootstrap\DetectEnvironment',
    'Illuminate\Foundation\Bootstrap\LoadConfiguration',
    'Illuminate\Foundation\Bootstrap\ConfigureLogging',
    'Illuminate\Foundation\Bootstrap\HandleExceptions',
    'Illuminate\Foundation\Bootstrap\RegisterFacades',
    'Illuminate\Foundation\Bootstrap\SetRequestForConsole',
    'Illuminate\Foundation\Bootstrap\RegisterProviders',
    'Illuminate\Foundation\Bootstrap\BootProviders',
];

Console的Kernel多了一个SetRequestForConsole。这里主要关注Illuminate\Foundation\Bootstrap\RegisterProviders类:

<?php

namespace Illuminate\Foundation\Bootstrap;

use Illuminate\Contracts\Foundation\Application;

class RegisterProviders
{
    /**
     * Bootstrap the given application.
     *
     * @param  \Illuminate\Contracts\Foundation\Application  $app
     * @return void
     */
    public function bootstrap(Application $app)
    {
        $app->registerConfiguredProviders();
    }
}

仅仅调用了容器的registerConfiguredProviders()方法,继续跟踪这个方法:

// Illuminate\Foundation\Application
public function registerConfiguredProviders()
{
    $manifestPath = $this->getCachedServicesPath();

    (new ProviderRepository($this, new Filesystem, $manifestPath))
                ->load($this->config['app.providers']);
}

这里的$this->getCachedServicesPath()返回bootstrap/cache/services.json路径,然后由Illuminate\Foundation\ProviderRepository类实例来加载ServiceProvider。

首先去检测bootstrap/cache/services.json文件是否存在,如果存在并且与$this->config[‘app.providers’]比较无变化则直接使用这个结果,否则就循环$this->config[‘app.providers’]:

foreach ($providers as $provider) {
//创建实例
    $instance = $this->createProvider($provider);

    // When recompiling the service manifest, we will spin through each of the
    // providers and check if it's a deferred provider or not. If so we'll
    // add it's provided services to the manifest and note the provider.
    if ($instance->isDeferred()) {
        foreach ($instance->provides() as $service) {
            $manifest['deferred'][$service] = $provider;
        }

        $manifest['when'][$provider] = $instance->when();
    }

    // If the service providers are not deferred, we will simply add it to an
    // array of eagerly loaded providers that will get registered on every
    // request to this application instead of "lazy" loading every time.
    else {
        $manifest['eager'][] = $provider;
    }
}

这里是判断服务提供者是否是延时的(就是用到是才实例化服务),如果是就调用provides()来获取延时服务,添加到$manifest[‘deferred’][$service]中,其值是服务提供者的名称,表示这个服务由它提供。并且调用when()方法,用来实现当某事件触发时,实例化这个服务提供者。最后把非延时的服务提供者放入$manifest[‘eager’]。

然后把$manifest数组保存到bootstrap/cache/services.json,下次就继续使用这个数组,看起来是这个样子:

{
    "providers": [
        "Illuminate\\Foundation\\Providers\\ArtisanServiceProvider",
    ],
    "eager": [
        "Illuminate\\Auth\\AuthServiceProvider",
    ],
    "deferred": {
        "command.app.name": "Illuminate\\Foundation\\Providers\\ArtisanServiceProvider",
    },
    "when": {
        "Illuminate\\Foundation\\Providers\\ArtisanServiceProvider": [],
    }
}

取回这个ServiceProvider数组后,会执行如下代码:

//Illuminate\Foundation\ProviderRepository 的load()方法
foreach ($manifest['when'] as $provider => $events) {
    $this->registerLoadEvents($provider, $events);
}

// We will go ahead and register all of the eagerly loaded providers with the
// application so their services can be registered with the application as
// a provided service. Then we will set the deferred service list on it.
foreach ($manifest['eager'] as $provider) {
    $this->app->register($this->createProvider($provider));
}

$this->app->addDeferredServices($manifest['deferred']);

循环when,注册when中注册的事件,实现当某时间触发时实例化此服务提供者并调用其register方法。然后循环调用eager中的服务提供者的register方法。最后把延时服务合并到容器的deferredServices中。

从这个过程可见,服务提供者的provides方法暴露了其提供的服务,对一个延时服务提供者来说是必须的。同样,为了实现某些事件触发时对应的延时服务可用,可以定义when方法。另外,如果一个服务提供者是延时的,那么它提供的所有服务都是延时的,实例化一个延时服务时,在同一个延时服务提供者提供的服务都会实例化,因为实例化一个延时服务实际调用服务提供者的register方法。

最后,服务提供者的boot方法被调用(延时服务提供者则在register后被调用)

关于延时服务的生成:

public function make($abstract, array $parameters = [])
{
    $abstract = $this->getAlias($abstract);

    if (isset($this->deferredServices[$abstract])) {
        $this->loadDeferredProvider($abstract);
    }

    return parent::make($abstract, $parameters);
}

当make一个延时服务时,首先把服务提供者实例调用register方法,然后就跟一般的make无异。

服务提供者的register方法主要提供服务,往容器中注入绑定(不完全是)。举例如下:

protected function registerManager()
{
    $this->app->singleton('queue', function ($app) {
        // Once we have an instance of the queue manager, we will register the various
        // resolvers for the queue connectors. These connectors are responsible for
        // creating the classes that accept queue configs and instantiate queues.
        $manager = new QueueManager($app);

        $this->registerConnectors($manager);

        return $manager;
    });

    $this->app->singleton('queue.connection', function ($app) {
        return $app['queue']->connection();
    });
}

这样,如果需要queue实例,只需要$app->make(‘queue’)即可。

对应非延时服务,注册到容器中的仅仅是绑定,只有真需要的时候才会产生实例,避免了用不到也实例化。另外,对应延时服务,则是在用到时才实例化服务提供者,然后才开始做绑定,最后才实例化,所有对于使用概率较低的服务,可以把其变为延时服务。