分类目录归档:laravel

Laravel Envoy 详解

Envoy: 使节,外交官;全权公使;谈判代表

简单描述这个包的作用:以一种更加友好的方式,在本地操作远程服务器。

Laravel Envoy是一个标准的PHP组件,被定义为一个命令行工具,可以使用composer全局安装,也可以安装到具体项目中。

地址:
https://packagist.org/packages/laravel/envoy

检查composer.json文件:

"autoload": {
    "classmap": ["src"]
},
"require": {
    "illuminate/support": "~4.1 || ~5.0",
    "nategood/httpful": "~0.2",
    "symfony/console": "~3.0 || ~4.0",
    "symfony/process": "~3.0 || ~4.0"
}, 
"bin": [
    "envoy"
],

首先,这个包的类是以classmap方式直接映射的,另外它依赖4个包,其中symfony/console用来建立命令行工具的,symfony/process用来操作进程。

另外,bin指定了envoy,表明这个包下有一个叫envoy的文件,告诉Composer在vendor/bin中建立对应的符号链接(认为是执行文件)。

安装:

// 全局安装
composer global require laravel/envoy

// 在具体项目中安装
composer require laravel/envoy

不管以哪种方式安装,最终都需要在命令行中可以找到envoy这个工具,所以,可以把~/.composer/vendor/bin加入到PATH中:

vi .bash_profile
export PATH="/Users/vft/.composer/vendor/bin:/usr/local/sbin:$PATH"

用法(参考官方文档):

// 安装包中的Envoy.blade.php就是定义任务的地方(默认存在)
// 编写任务
@servers(['web' => 'user@192.168.1.1'])

@task('foo', ['on' => 'web'])
    ls -la
@endtask

任务foo是在web这台机器上运行(意思是登录到这个机器运行指定的任务)。

在运行任务前先执行一段PHP代码:

@setup
    $now = new DateTime();
    $environment = isset($env) ? $env : "testing";
@endsetup

引入其它文件:

@include('vendor/autoload.php')

@task('foo')
    # ...
@endtask

传递变量到命令:

envoy run deploy --branch=master
@servers(['web' => '192.168.1.1'])

@task('deploy', ['on' => 'web'])
    cd site
    @if ($branch)
        git pull origin {{ $branch }}
    @endif
    php artisan migrate
@endtask

任务分组:

@servers(['web' => '192.168.1.1'])

@story('deploy')
    git
    composer
@endstory

@task('git')
    git pull origin master
@endtask

@task('composer')
    composer install
@endtask

// 运行
envoy run deploy

任务分组用story指令来完成,所谓story,其实也可以看做是一个任务(执行时就看做是一个任务),不过它会依次运行分组里面的任务而已。

使用多个服务器:

@servers(['web-1' => '192.168.1.1', 'web-2' => '192.168.1.2'])

@task('deploy', ['on' => ['web-1', 'web-2']])
    cd site
    git pull origin {{ $branch }}
    php artisan migrate
@endtask

// 并行运行
@servers(['web-1' => '192.168.1.1', 'web-2' => '192.168.1.2'])

@task('deploy', ['on' => ['web-1', 'web-2'], 'parallel' => true])
    cd site
    git pull origin {{ $branch }}
    php artisan migrate
@endtask

在使用多个服务器的情况下,默认是一个服务器上的任务运行完成后,开启另一台服务器任务的运行,如果需要同时运行,那么就需要在定义任务时添加parallel为true。

运行任务:

./envoy run task

任务运行前确认:

@task('deploy', ['on' => 'web', 'confirm' => true])
    cd site
    git pull origin {{ $branch }}
    php artisan migrate
@endtask

命令执行后发送通知:

@after
    @slack('webhook-url', '#bots')
@endafter

发送通知这个不过是调用外部程序预留的钩子,达到通知外部的目的。

以下跟踪以下具体实现:
对应的文件:vendor/laravel/envoy/envoy是入口文件,vendor/laravel/envoy/ Envoy.blade.php就是定义任务的地方。

首先查看vendor/laravel/envoy/envoy:

#!/usr/bin/env php
<?php

if (file_exists(__DIR__.'/vendor/autoload.php')) {
    require __DIR__.'/vendor/autoload.php';
} else {
    require __DIR__.'/../../autoload.php';
}

$app = new Symfony\Component\Console\Application('Laravel Envoy', '1.4.1');

$app->add(new Laravel\Envoy\Console\RunCommand);
$app->add(new Laravel\Envoy\Console\SshCommand);
$app->add(new Laravel\Envoy\Console\InitCommand);
$app->add(new Laravel\Envoy\Console\TasksCommand);

$app->run();

第一行文件:#!/usr/bin/env php,找到PHP,如果环境变量中找不到PHP,就需要指定运行该脚本。

接下来是把autoload.php包含进来,命令可能是vendor/bin中的envoy命令,也可能是组件中的envoy命令,所以包含autoload.php需要适配这两种情况。

然后就是一个典型symfony/console命令行工具:先建立一个Application,然后往Application中添加命令,然后运行命令。

关于symfony/console包的使用,参考:
http://symfonychina.com/doc/current/console.html

运行envoy:

./envoy 
Laravel Envoy 1.4.1

Usage:
  command [options] [arguments]

Options:
  -h, --help            Display this help message
  -q, --quiet           Do not output any message
  -V, --version         Display this application version
      --ansi            Force ANSI output
      --no-ansi         Disable ANSI output
  -n, --no-interaction  Do not ask any interactive question
  -v|vv|vvv, --verbose  Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug

Available commands:
  help   Displays help for a command
  init   Create a new Envoy file in the current directory.
  list   Lists commands
  run    Run an Envoy task.
  ssh    Connect to an Envoy server.
  tasks  Lists all Envoy tasks and macros.

命令envoy的用法是:envoy [options] [arguments], 这里的optins列表是默认添加的,在可用命令部分有help和list都是默认提供的(直接输入envoy时,默认就是调用list命令),主动添加的只有:init, tasks, ssh, run。

以下看看具体的命令实现,首先,添加一个命令,命令周边的操作都一样,而命令只需要实现configure()和fire()方法,所以命令周边的操作提取到了一个trait中,具体的命令引用这个trait即可,这个trait对应src/Console/Command.php。

1 命令:init

./envoy init 192.168.1.111

源码:

<?php

namespace Laravel\Envoy\Console;

use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Command\Command as SymfonyCommand;

class InitCommand extends SymfonyCommand
{
    use Command;

    /**
     * Configure the command options.
     *
     * @return void
     */
    protected function configure()
    {
        $this
            ->setName('init')
            ->setDescription('Create a new Envoy file in the current directory.')
            ->addArgument('host', InputArgument::REQUIRED, 'The host server to initialize with.');
    }

    /**
     * Execute the command.
     *
     * @return void
     */
    protected function fire()
    {
        if (file_exists(getcwd().'/Envoy.blade.php')) {
            $this->output->writeln('<error>Envoy file already exists!</error>');

            return;
        }

        file_put_contents(getcwd().'/Envoy.blade.php', "@servers(['web' => '".$this->input->getArgument('host')."'])

@task('deploy')
    cd /path/to/site
    git pull origin master
@endtask
");

        $this->output->writeln('<info>Envoy file created!</info>');
    }
}

方法configure()配置了命令名,需要什么参数,什么选项,命令注释等。fire()方法是具体的实现,命令的输入可用$this->input,命令的输出可用$this->output。

这里的envoy init只是往envoy所在的目录写入一个模板文件而已。

2 命令:tasks

./envoy tasks
Available tasks:
  foo
  deploy
  git
  composer

Available stories:
  story

实现:

protected function fire()
{
    $container = $this->loadTaskContainer();

    $this->listTasks($container);

    $this->output->writeln('');

    $this->listMacros($container);
}

protected function loadTaskContainer()
{
    if (! file_exists($envoyFile = getcwd().'/Envoy.blade.php')) {
        echo "Envoy.blade.php not found.\n";

        exit(1);
    }

    with($container = new TaskContainer)->load($envoyFile, new Compiler);

    return $container;
}

把Envoy.blade.php加载到taskContainer中,并指定了编译器。编译器对应src/Compiler.php,它把Envoy.blade.php这种Blade语法转换为PHP语法:

@servers(['web' => ['127.0.0.1']])

@setup
$now = new DateTime();
$environment = isset($env) ? $env : "testing";
@endsetup

@include('../../../vendor/autoload.php')

@task('foo', ['on' => 'web'])
    ls -la
@endtask

@task('deploy', ['on' => 'web'])
    cd site

    @if ($branch)
        git pull origin {{ $branch }}
    @endif

    php artisan migrate
@endtask

@story('story')
    git
    composer
@endstory

@task('git')
    git pull origin master
@endtask

@task('composer')
    composer install
@endtask

经过编译后得到:

<?php $branch = isset($branch) ? $branch : null; ?>
<?php $env = isset($env) ? $env : null; ?>
<?php $environment = isset($environment) ? $environment : null; ?>
<?php $now = isset($now) ? $now : null; ?>
<?php $__container->servers(['web' => ['127.0.0.1']]); ?>

<?php
$now = new DateTime();
$environment = isset($env) ? $env : "testing";
?>

 <?php require_once('../../../vendor/autoload.php'); ?>

<?php $__container->startTask('foo', ['on' => 'web']); ?>
    ls -la
<?php $__container->endTask(); ?>

<?php $__container->startTask('deploy', ['on' => 'web']); ?>
    cd site

    <?php if ($branch): ?>
        git pull origin <?php echo $branch; ?>

    <?php endif; ?>

    php artisan migrate
<?php $__container->endTask(); ?>

<?php $__container->startMacro('story'); ?>
    git
    composer
<?php $__container->endMacro(); ?>

<?php $__container->startTask('git'); ?>
    git pull origin master
<?php $__container->endTask(); ?>

<?php $__container->startTask('composer'); ?>
    composer install
<?php $__container->endTask(); ?>

然后taskContainer再把这个编译后的文件include进来, 实际执行容器里面的方法, 然后相关的数据就已经解析进入容器了,然后就调用相关的容器方法,比如取回任务等。

这里的tasks命令,实际就是调用容器的getTasks()方法。

3 命令:run

./envoy run task –continue –pretend –path= --conf=

选项—continue用来控制当执行一个任务失败后是否继续,这种情况发生在使用story指令来包装一组任务时,运行这个story实际是运行一组任务,默认不需要指定,表示不继续。
选项—pretend是伪装的意思,即把任务要运行的命令输出,并返回1(未执行成功)
选项—path用来指定任务定义的目录
选项—conf用来指定任务定义的文件

命令run的流程:
1 初始化容器
2 从容器中取回任务(脚本)
3 SSH登录服务器运行
4 运行任务执行后的回调

protected function fire()
{
    $container = $this->loadTaskContainer();

    $exitCode = 0;

    foreach ($this->getTasks($container) as $task) {
        $thisCode = $this->runTask($container, $task);

        if (0 !== $thisCode) {
            $exitCode = $thisCode;
        }

        if ($thisCode > 0 && ! $this->input->getOption('continue')) {
            $this->output->writeln('[<fg=red>✗</>] <fg=red>This task did not complete successfully on one of your servers.</>');

            break;
        }
    }

    foreach ($container->getFinishedCallbacks() as $callback) {
        call_user_func($callback);
    }

    return $exitCode;
}

这里的foreach是针对任务组(多个任务)的情况,然后调用runTask():

protected function runTask($container, $task)
{
    $macroOptions = $container->getMacroOptions($this->argument('task'));

    $confirm = $container->getTask($task, $macroOptions)->confirm;

    if ($confirm && ! $this->confirmTaskWithUser($task, $confirm)) {
        return;
    }

    if (($exitCode = $this->runTaskOverSSH($container->getTask($task, $macroOptions))) > 0) {
        foreach ($container->getErrorCallbacks() as $callback) {
            call_user_func($callback, $task);
        }

        return $exitCode;
    }

    foreach ($container->getAfterCallbacks() as $callback) {
        call_user_func($callback, $task);
    }
}

这里会返回一个Envoy/Task的对象实例传递到runTaskOverSSH()方法,Envoy/Task的对象是一个任务的简单封装,比如规定了任务在什么服务器上运行,是否并行运行等。

然后是runTaskOverSSH()方法:

protected function runTaskOverSSH(Task $task)
{
    // If the pretending option has been set, we'll simply dump the script out to the command
    // line so the developer can inspect it which is useful for just inspecting the script
    // before it is actually run against these servers. Allows checking for errors, etc.
    if ($this->pretending()) {
        echo $task->script.PHP_EOL;

        return 1;
    } else {
        return $this->passToRemoteProcessor($task);
    }
}

protected function passToRemoteProcessor(Task $task)
{
    return $this->getRemoteProcessor($task)->run($task, function ($type, $host, $line) {
        if (starts_with($line, 'Warning: Permanently added ')) {
            return;
        }

        $this->displayOutput($type, $host, $line);
    });
}
protected function getRemoteProcessor(Task $task)
{
    return $task->parallel ? new ParallelSSH : new SSH;
}

一个任务最终会放入到一个Envoy\SSH或ParallelSSH进程中执行(调用进程类的run()方法)。

进程类中,需要完成登录远程服务器,然后在远程服务器上执行脚本。登录服务器这个步骤,除非是本地的服务器(127.0.0.1),那么就需要认证,密码或私钥,这个是ssh外部命令完成。不过这里需要知道的是,SSH登录可以利用~/.ssh/config中定义的别名,比如针对这个别名定义了非22端口,那么就要解析这个文件,找到别名,然后ssh 别名。Laravel\Envoy\SSHConfigFile就解析这个config文件。

4 命令:ssh
这个命令基本是Bash中的ssh的包装,不过主机必须配置中定义过的主机:

@servers(['web' => 'root@192.168.1.111'])

那么就可以使用如下命令来登录:

./envoy ssh web

这个命令不过是找到web对应的机器root@192.168.1.111,然后ssh root@192.168.1.111而已:

protected function fire()
    {
        $host = $this->getServer($container = $this->loadTaskContainer());

        passthru('ssh '.($this->getConfiguredServer($host) ?: $host));
    }

这里的$host就是root@192.168.1.111,当然,这个机器可能在~/.ssh/config中定义了其它参数(比如非22端口),那么需要通过它找到别名,然后ssh别名。

Laravel Envoy最后一个功能就是命令执行完毕后,可以把结果发送给第三方,实际就是通知功能。

另外,一个任务可以异步发送到多台机器运行,但是不能多个任务同时运行。任务运行时可以指定continue参数,指定在任务失败时,是否继续,如果是并行运行的,这个参数似乎是无效的。另外,任务如果在多台机器上运行,只有全部成功才认为成功,可能发生前一个任务成功,后一个任务失败,这个时候需要设置回调做善后工作。

Laravel 容器源码研究

<?php
//error_reporting(0);

include "vendor/autoload.php";

class A
{
    public $b = null;

    public $p = 0;

    public function __construct($b, $p)
    {
        $this->b = $b;
        $this->p = $p;
    }

    public function test()
    {
        echo "A-------\n";
        var_dump($this->p);
        $this->b->test();
    }
}

class B
{
    public $c = null;

    public $o = 0;

    public function __construct(C $c, $o = 0)
    {
        $this->c = $c;
        $this->o = $o;
    }

    public function test()
    {
        echo "B-------\n";
        if ($this->o) {
            echo 'Great 0-----';
        }
        $this->c->test();
    }
}

class C
{
    public function test()
    {
        echo "C-------\n";
    }
}

$container = new \Illuminate\Container\Container();
$p = 10101000;

// A类构造函数依赖形参$p(没有默认值),需要传递: 通过when()方法指定
$b = $container->make(B::class, ['o' => 1]);
$container->when(A::class)->needs('$p')->give($p);
$container->when(A::class)->needs('$b')->give($b);
//$container->when(A::class)->needs(B::class)->give(function () use ($b) {
//    return $b;
//});
$a = $container->make(A::class);

// A类构造函数依赖形参$p(没有默认值),需要传递:通过make()方法的第二参数传递
//$a = $container->make(A::class, ['p' => $p]);
$a->test();

dd((array)$container);

一、对象构建:

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

方法make只是resolve()方法的包装而已(注意:make方法是公共的,而resolve是受保护的):

    protected function resolve($abstract, $parameters = [])
    {
	// 如果是别名,取回原来的名称:比如aa是a的别名,传递aa进来,返回a
	// 传递a进来,直接返回a
        $abstract = $this->getAlias($abstract);

        $needsContextualBuild = ! empty($parameters) || ! is_null(
            $this->getContextualConcrete($abstract)
        );

        // 在instances中,并且没有上下文对象,直接返回
        if (isset($this->instances[$abstract]) && ! $needsContextualBuild) {
            return $this->instances[$abstract];
        }

	// 把传递进来的参数进行临时保存(入栈)
        $this->with[] = $parameters;

        $concrete = $this->getConcrete($abstract);

        // 递归构建对象
        if ($this->isBuildable($concrete, $abstract)) {
            $object = $this->build($concrete);
        } else {
            $object = $this->make($concrete);
        }

        // 应用对象装饰
        foreach ($this->getExtenders($abstract) as $extender) {
            $object = $extender($object, $this);
        }

        // 如果是share类型,并且没有上下文依赖,就存入instances:并不是设置为share就一定进入instances
        if ($this->isShared($abstract) && ! $needsContextualBuild) {
            $this->instances[$abstract] = $object;
        }

	// 回调
        $this->fireResolvingCallbacks($abstract, $object);

        // 标记已经取回
        $this->resolved[$abstract] = true;

	// 出栈
        array_pop($this->with);

        return $object;
    }

类目传递了A,这时$abstract是A, $parameters是[‘p’ => $p],$abstract经过getAlias后没有发送变化(不是别名),由于参数不是空,$needsContextualBuild为true; $this->getConcrete($abstract)后得到的$concrete是$abstract本身(都是A),然后判断是否可以构建:

    protected function isBuildable($concrete, $abstract)
    {
        return $concrete === $abstract || $concrete instanceof Closure;
    }

是否可以构建,条件是$concrete和$abstract相等,或者$concrete是一个闭包函数。所以这里会进入build()方法:

    public function build($concrete)
    {
        // 如果是闭包,调用闭包函数:第一参数是容器,第二参数是外部传递进来的参数
        if ($concrete instanceof Closure) {
            return $concrete($this, $this->getLastParameterOverride());
        }
	// 不是闭包就利用反射:$concrete这里应该是类名(或类对象)
        $reflector = new ReflectionClass($concrete);

        // 如果不可实例化,抛异常
        if (! $reflector->isInstantiable()) {
            return $this->notInstantiable($concrete);
        }

	// 由于可能会依赖其它对象,这里先把当前类目入栈
        $this->buildStack[] = $concrete;
	// 取回构造函数
        $constructor = $reflector->getConstructor();

        // 构造函数为空,直接实例化
        if (is_null($constructor)) {
            array_pop($this->buildStack);

            return new $concrete;
        }
	// 否则就是指定了构造函数,那么解析这个构造函数,取回参数
        $dependencies = $constructor->getParameters();

        // 根据构造函数的参数,实例化依赖,这里可能会产生递归
	// 另外,如果外部有传递参数进来,会优先使用
        $instances = $this->resolveDependencies(
            $dependencies
        );
	// 出栈
        array_pop($this->buildStack);

	// 把实例对象传递构造函数生成对象
        return $reflector->newInstanceArgs($instances);
    }

这个构建的过程,利用反射,如果依赖其它对象,则递归构建依赖。 这里面用到了两个栈,with是参数堆栈,每次构建从其中取出参数,填充到构造函数,另一个是buildStack。这里可以看到A传递进来,它不是闭包,那么就开始了下面的反射构建过程,A是可以实例化的,有构造函数,那么会进入resolveDependencies()递归解决依赖:

    protected function resolveDependencies(array $dependencies)
    {
        $results = [];

        foreach ($dependencies as $dependency) {
            if ($this->hasParameterOverride($dependency)) {
                $results[] = $this->getParameterOverride($dependency);

                continue;
            }

            $results[] = is_null($dependency->getClass())
                            ? $this->resolvePrimitive($dependency)
                            : $this->resolveClass($dependency);
        }

        return $results;
    }

这里看明白如下几个函数:

    protected function hasParameterOverride($dependency)
    {
        return array_key_exists(
            $dependency->name, $this->getLastParameterOverride()
        );
    }

    protected function getParameterOverride($dependency)
    {
        return $this->getLastParameterOverride()[$dependency->name];
    }

    protected function getLastParameterOverride()
    {
        return count($this->with) ? end($this->with) : [];
    }

方法getLastParameterOverride取回with堆栈的头元素,就是传递进来的参数,方法hasParameterOverride就是判断传递进来的参数有没有对应名称的变量,方法getParameterOverride就是取到传递进来的值。在这,传递进来的参数是[‘p’ => $p], 那么在构建A时,取到形参p,p出现在了[‘p’ => $p]中,所以A构造函数的$p就等于传递进来的变量$p。如果没有对应的参数,就检查是否有指定上下文参数,有则使用,没有,如果是类就去实例类(可能递归),如果是原始变量,就使用默认值。

由于A构造函数需要B对象,而B对象又需要C对象,所以会进入递归,最终返回的对象传递到A构造函数。从这个过程可见,容器的make方法,可以自动构建对象。如果没有构造函数,或构造函数没有依赖其它对象,直接new即可。到这里,还有问题问解决,当构建A时,会去构建B,如果希望使用一个现成的B对象,这个需要依靠上下文绑定来解决。

二、上下文绑定:

//当解析A::class,如果需要B::class类型对象,那么对应的对象是$b。
$container->when(A::class)->needs(B::class)->give(function () use ($b) {
    return $b;
});
//当解析A::class,如果需要形参是$b时,那么对应的值是$b。
$container->when(A::class)->needs('$b')->give($b);

注意语法,当需要的是类类型时,需要给一个闭包函数,否则直接指定值。另外,如果构造函数依赖的是B $b,这个时候直接给值的语法可能是无作用的,因为如果有类型限定,并且类型是类类型,那么总是根据类来实例化对象,这个过程会取回闭包执行返回对象。

这个用法实际产生如下数组:

$this->contextual['A']['B'] = function () use ($b) {
    return $b;
};
$this->contextual['A']['$b'] = $b;

当A实例化是,遇到了需要实例化B,那么首先判断是否存在$this->contextual[‘A’][‘B’],如果存在直接用,否则就去构造。当遇到形参$b时,就去判断$this->contextual[‘A’][‘$b’]是否存在,存在直接用,否则利用反射API去使用默认值。

容器的when()方法不过是一个语法糖,最终会调用addContextualBinding()方法来设置上下绑定:

    public function addContextualBinding($concrete, $abstract, $implementation)
    {
        $this->contextual[$concrete][$this->getAlias($abstract)] = $implementation;
    }

可以这样用:

$container->addContextualBinding(A::class, B::class, function () use ($b) {
    return $b;
});
$container->addContextualBinding(A::class, '$b', $b);

这个上下文绑定,跟make()时传递的第二参数作用非常类似,不过make时传递的参数需要和构造函数的形参名称相同,并且总是优先使用的,比如:

$container->addContextualBinding(A::class, '$b', $b);

$container->make(A::class, ['$b' => $b]);

这里的设置了上下文,也传递了第二参数,那么make时传递的总是优先被使用。一旦设置了上下文,或make时传递了参数,那么生成的对象都是独立(不共享,不会压入$this->instances数组),哪怕设置的绑定是共享的,因为跟上下文相关的对象,是特定的。

上下文设置或构建时的参数传递严格意义上说并不是容器需要实现的内容。这里显然是为了能便利的构建依赖而引入的便利方法。

三、对象构建时的回调:
对象构建之后,可以设置回调,可以全局设置回调(每构建一个对象都调用),或者设置在构建某个类型的对象时才执行回调:

$container = new \Illuminate\Container\Container();
$callBack = function () {
    echo "Callback -------\n";
};

$globalAfterCallBack = function () {
    echo "Global After Callback -------\n";
};

$afterCallBack = function () {
    echo "After Callback -------\n";
};
$container->resolving($globalCallBack);
$container->resolving('a', $callBack);
$container->afterResolving($globalAfterCallBack);
$container->afterResolving('a', $afterCallBack);

四、对象装饰器:
对象构建后,需要执行对象的某个方法,或者运行某个逻辑来扩展一下这个对象,这个就是对象的装饰。在对象生成后,可以使用extend来装饰:

$container->extend(Service::class, function($service) {
    return new DecoratedService($service);
});

五、绑定
在理解了以上基本内容后,再来看看绑定,应该就很好理解了。

// 直接绑定一个类,可以这样做,除了类实例化会延后外,没有额外好处,一般应该避免这个用法
// 在需要是直接make即可
$container->bind(A::class);

// 设置服务,当make(‘a’)时,会实时生成一个A对象
$container->bind(‘a’, A::class);

// 绑定接口,AI实现了I的接口
$container->bind(‘I’, AI::class);

// 设置服务,指定闭包函数,多见
$container->bind(‘events’, function () {});

主要到以上用法尽管都是合法的,但是绑定闭包这种用法比较多见。另外,bing的第三参数控制这个bind是否是共享的,也就是生成实例是否是单例的,bind方法默认是不共享的,有一个快捷方法singleton()可以直接设置共享绑定。

如果绑定是共享的,生成的实例会压入instances数组,表示这些是共享对象,所以有一个方法,可以往这个数组中直接压入内容进行全局共享:instance($key, $instance)。

接下来看看具体过程:

    public function bind($abstract, $concrete = null, $shared = false)
    {
        $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);
        }
    }

简单来说就是对一个类型设置绑定,第二参数是要绑定的内容,第三参数设置是否共享,它决定了对象是否是单例对象。首先调用的$this->dropStaleInstances($abstract),对应的方法实际就是清空该类型对应的绑定和对应的实例:

    protected function dropStaleInstances($abstract)
    {
        unset($this->instances[$abstract], $this->aliases[$abstract]);
    }

如果第二参数是null(没有传递需要绑定的内容),那么绑定的内容就是类型本身。如果绑定的内容如果不是一个闭包函数,那么就调用getClosure()把内容变成一个闭包函数:

    protected function getClosure($abstract, $concrete)
    {
        return function ($container, $parameters = []) use ($abstract, $concrete) {
            if ($abstract == $concrete) {
                return $container->build($concrete);
            }

            return $container->make($concrete, $parameters);
        };
    }

这个getClosure()返回的是一个闭包函数,返回的这个闭包函数接受两个参数($container, $parameters = []),如果类型和内容一样,就直接build,否则就是make一个对象,总之现在知道其返回的是function ($container, $parameters = [])即可。

接下来按照一个统一的格式设置绑定:

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

if ($this->resolved($abstract)) {
    $this->rebound($abstract);
}

// 举例来说
$this->bindings[‘a’] = [
    ‘concrete’ => function ($container, $parameters = []) {},
    ‘shared’ => false
];

那么在a实例被生成时,concrete对应的闭包函数会被执行,其中会根据shared来决定是否设置共享实例。resolved()方法用来判断绑定是否已经生成了实例,如果生成了实例,调用rebound()方法重新生成一次(实际是调用make方法,不过,在重写生成对象时,可以对特定类型设置回调,这个方法会执行回调)。

绑定的其它情况分析:
第二参数只要不是闭包函数,那么总会调用getClosure()方法封装为一个闭包函数:
1 当第二参数为空时,把第一参数赋值给第二参数,调用getClosure()方法

class A {}

$container->bind(A::class);
// $abstract = $concrete = A::class
$container->bind['A'] = function ($container, $parameters = []) use ($abstract, $concrete) {
     if ($abstract == $concrete) {
         return $container->build($concrete);
     }

     return $container->make($concrete, $parameters);
};
// 此时获取该服务跟直接new基本一样
// 不一样的是实例生成时,如果构造函数依赖容器对象,可以自动注入

2 当是一个对象时,调用getClosure()方法

class A {}

$container->bind('aa', new A());
// $abstract = 'aa'; $concrete = new A();
$container->bind['aa'] = function ($container, $parameters = []) use ($abstract, $concrete) {
     if ($abstract == $concrete) {
         return $container->build($concrete);
     }

     return $container->make($concrete, $parameters);
};
// 在实例化时会运行$container->make('aa', []), ‘aa’对应的闭包函数被运行,该函数内再次运行$container->make($concrete, $parameters);
// 此时的$concrete是一个对象,PHP的isset($this->aliases[$concrete])会报警告,但不会影响程序运行
// 第二次运行的make最终会到达build函数,这是的build不过是根据$concrete(此时是一个对象)并应用反射,重新生成一个对象而已

所以这两种用法都应该避免。相反,在使用bind时,应该总是使用闭包。

另外,第二参数是闭包时,这个闭包函数在定义参数时注意:

$container->bind('aa', function ($a, $b, $c) {});

如果调用make时,将会报错。因为闭包调用时总是如此格式:

return $concrete($this, $this->getLastParameterOverride());

闭包可以不定义参数,以上的用法不会发生错误。如果定义了参数,第一个参数总是代表容器实例本身,第二个参数是一个数组(可不定义)用来从外传入数据到闭包中。如果多于2个参数就发生错误。如果定义了第二参数,那么在make对象时,就可以传递数据进去(注:如果是单例实例,一旦生成后不会再改变,除非强制rebund,所以不能期望通过传递参数改变单例对象的状态)

最终提炼一下公共方法:

public function when($concrete)

// 服务是否已经设置
public function bound($abstract)

// 方法bound的别名
public function has($id)

// 服务是否已经实例化
public function resolved($abstract)

// 服务是否是共享的(单例)
public function isShared($abstract)

// 名称是否是别名
public function isAlias($name)

// 服务绑定
public function bind($abstract, $concrete = null, $shared = false)

public function hasMethodBinding($method)
public function bindMethod($method, $callback)
public function callMethodBinding($method, $instance)

// 上下文绑定
public function addContextualBinding($concrete, $abstract, $implementation)

// 条件绑定,bind方法的再封装
public function bindIf($abstract, $concrete = null, $shared = false)

// 单例绑定
public function singleton($abstract, $concrete = null)

// 服务器装饰器
public function extend($abstract, Closure $closure)

// 直接注入内容到容器
public function instance($abstract, $instance)

public function tag($abstracts, $tags)
public function tagged($tag)

// 服务别名设置
public function alias($abstract, $alias)

// 绑定回调设置
public function rebinding($abstract, Closure $callback)

public function refresh($abstract, $target, $method)
public function wrap(Closure $callback, array $parameters = [])
public function call($callback, array $parameters = [], $defaultMethod = null)

// 用一个闭包封装make
public function factory($abstract)

// 方法make的另一种形式
public function makeWith($abstract, array $parameters = [])

// 构建服务
public function make($abstract, array $parameters = [])
// 
public function get($id)

// 构建服务
public function build($concrete)

public function resolving($abstract, Closure $callback = null)
public function afterResolving($abstract, Closure $callback = null)

public function getBindings()
public function getAlias($abstract)
public function forgetExtenders($abstract)
public function forgetInstance($abstract)
public function forgetInstances()
public function flush()

实际常用的就是几个方法。

Laravel Scout使用实例

Laravel Scout是从Laravel 5.3引入的一个实现全文搜索的扩展包。

以下实例构建过程:

1 建立目录,安装框架
mkdir scout
cd scout
composer create-project --prefer-dist laravel/laravel .

vi app/Providers/AppServiceProvider.php
#对应添加
use Illuminate\Support\Facades\Schema;

class AppServiceProvider extends ServiceProvider
{
    public function boot()
    {
        Schema::defaultStringLength(191);
    }
}

#
vi config/database.php
'mysql' => [
    'strict' => false, // true改为false
],

2 安装扩展包
composer require laravel/scout
composer require algolia/algoliasearch-client-php

3 配置laravel/scout
vi config/app.php
#添加服务提供者
'providers' => [
    ....
    Laravel\Scout\ScoutServiceProvider::class,
]
#产生配置文件config/scout.php
php artisan vendor:publish --provider="Laravel\Scout\ScoutServiceProvider"

4 配置algolia驱动
到https://www.algolia.com/api-keys获取秘钥(先注册一个账户,选择香港节点),获取Application ID对应ALGOLIA_APP_ID, Admin API Key对应ALGOLIA_SECRET,然后按照如下格式写入.env文件:
ALGOLIA_APP_ID=Application ID
ALGOLIA_SECRET=ALGOLIA_SECRET

5 建表和模型
php artisan make:migration create_items_table

#编辑database/migrations的迁移文件:
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateItemsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('items', function (Blueprint $table) {
           $table->increments('id');
           $table->string('title');
           $table->timestamps();
       });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::drop("items");
    }
}
#编辑.env,填写数据库相关信息

#运行迁移命令,开始建表
php artisan migrate

#建立模型
vi app/Item.php
<?php
namespace App;

use Illuminate\Database\Eloquent\Model;
use Laravel\Scout\Searchable;

class Item extends Model
{

    use Searchable;

    public $fillable = ['title'];

    /**
     * 获取模型的索引名称
     *
     * @return string
     */
    public function searchableAs()
    {
        return 'items_index';
    }
}

6 添加路由
vi routes/web.php
Route::get('items-lists', ['as'=>'items-lists','uses'=>'ItemSearchController@index']);
Route::post('create-item', ['as'=>'create-item','uses'=>'ItemSearchController@create']);

7 添加控制器
vi app/Http/Controllers/ItemSearchController.php

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Http\Requests;
use App\Item;

class ItemSearchController extends Controller
{

    /**
     * items列表
     */
    public function index(Request $request)
    {
        if($request->has('titlesearch')){
            $items = Item::search($request->titlesearch)
                     ->paginate(6);
        }else{
            $items = Item::paginate(6);
        }
        return view('item-search',compact('items'));
    }


    /**
     * 创建新的item
     */
    public function create(Request $request)
    {
        $this->validate($request,['title'=>'required']);

        $items = Item::create($request->all());
        return back();
    }
}

8 添加视图
vi resources/views/item-search.blade.php

<!DOCTYPE html>
<html>
    <head>
        <title>Laravel 5.3 - laravel scout algolia search example</title>
        <link rel="stylesheet" type="text/css" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
    </head>
    <body>
        <div class="container">
            <h2>Laravel Full Text Search using Scout and algolia</h2><br/>
            <form method="POST" action="{{ route('create-item') }}" autocomplete="off">
            @if(count($errors))
            <div class="alert alert-danger">
                <strong>Whoops!</strong> There were some problems with your input.
                <br/>
                <ul>
                @foreach($errors->all() as $error)
                    <li>{{ $error }}</li>
                @endforeach
                </ul>
            </div>
            @endif

            <input type="hidden" name="_token" value="{{ csrf_token() }}">

            <div class="row">
                <div class="col-md-6">
                    <div class="form-group {{ $errors->has('title') ? 'has-error' : '' }}">
                        <input type="text" id="title" name="title" class="form-control" placeholder="Enter Title" value="{{ old('title') }}">
                        <span class="text-danger">{{ $errors->first('title') }}</span>
                    </div>
                </div>
                <div class="col-md-6">
                    <div class="form-group">
                        <button class="btn btn-success">Create New Item</button>
                    </div>
                </div>
            </div>
        </form>

        <div class="panel panel-primary">
            <div class="panel-heading">Item management</div>
                <div class="panel-body">
                    <form method="GET" action="{{ route('items-lists') }}">
                    <div class="row">
                        <div class="col-md-6">
                            <div class="form-group">
                                <input type="text" name="titlesearch" class="form-control" placeholder="Enter Title For Search" value="{{ old('titlesearch') }}">
                            </div>
                        </div>
                        <div class="col-md-6">
                            <div class="form-group">
                                <button class="btn btn-success">Search</button>
                            </div>
                        </div>
                    </div>
                    </form>

                    <table class="table table-bordered">
                        <thead>
                             <th>Id</th>
                             <th>Title</th>
                             <th>Creation Date</th>
                             <th>Updated Date</th>
                        </thead>
                        <tbody>
                        @if($items->count())
                            @foreach($items as $key => $item)
                            <tr>
                                <td>{{ ++$key }}</td>
                                <td>{{ $item->title }}</td>
                                <td>{{ $item->created_at }}</td>
                                <td>{{ $item->updated_at }}</td>
                            </tr>
                            @endforeach
                        @else
                            <tr>
                                 <td colspan="4">There are no data.</td>
                            </tr>
                        @endif
                        </tbody>
                    </table>
                    {{ $items->links() }}
                </div>
            </div>
        </div>
    </body>
</html>

启动HTTP服务器(可以直接使用php artisan serve),访问http://localhost:8000/items-lists:

如果item表已经存在数据,可以:php artisan scout:import “App\Item”,这样就会把数据推送到algolia,尤其里建索引。可以开启队列的方式让其进行异步推送。具体可参考文档。

这里的全文搜索方式的实现是利用了离线搜索服务商-algolia,由于没有在中国大陆部署节点(在香港有节点),所以访问会比较慢(涉及数据推送与查询)。这种离线搜索服务大多是收费的,不过相比自己搭建维护服务器集群,成本还是低很多的。

国内的用户可以试试阿里云的OpenSearch,也是离线搜索服务。

Laravel 路线图

Laravel的发布路线图可以参考官方文档:https://laravel-news.com/laravel-release-process,最新的发货周期变更可参考:https://laravel-news.com/release-cycle-changes。

Laravel的版本发布是6个月发布一个版本,原来是6月和12月发布,最新变更为1月和7月(比原来推迟一个月)。

Laravel有两种发布类型:
LTS 版本 – 长期支持版本,英文 Long Term Support 的缩写,此类版本是 Laravel 能提供的最长时间维护版本。
一般发行版 – 只提供 6 个月的 Bug 修复支持,一年的安全修复支持。

解释(来自维基百科):
“长期支持 (英语:Long-term support,缩写:LTS)是一种软件的产品生命周期政策,特别是开源软件,它增加了软件开发过程及软件版本周期的可靠度。长期支持延长了软件维护的周期;它也改变了软件更新(补丁)的类型及频率以降低风险、费用及软件部署的中断时间,同时提升了软件的可靠性。但这并不必然包含技术支持。

在长期支持周期的开始,软件设计师会将软件特性冻结:他们制作补丁来修复程序错误及计算机安全隐患,但不会加入新的,可能会造成软件破坏的功能。软件维护者可能会单独发布补丁,或是将其置于维护版本、小数点版本或是服务包中发布。支持周期结束后,其称之为产品的生命周期结束。

“长期支持”这个术语通常是保留给特殊的软件版本,其他版本会有更短的生命周期。通常来说,长期支持版本至少会被维护两年。”

以下是一份版本计划,内容来自 – https://laravel-news.com/laravel-release-process
Laravel 5.1 LTS – 2015 年 6 月份
LTS 长久支持版本,Bug 修复直到 2017 年 6 月份,安全修复直到 2018 年 6 月份。

Laravel 5.2
– 2015 年 12 月份
一般发行版,提供 6 个月的 Bug 修复支持,一年的安全修复支持。

Laravel 5.3 – 2016 年 8 月份
一般发行版,提供 6 个月的 Bug 修复支持,一年的安全修复支持。

Laravel 5.4
– 2017 年 1 月份
一般发行版,提供 6 个月的 Bug 修复支持,一年的安全修复支持。

Laravel 5.5 – 2017 年 7 月份
下一个版本的 LTS 版本,会从这一刻开始停止 Laravel 5.1 的 Bug 修复,安全修复直到 2018 年 7 月份。

Laravel 5.x 核心概念

基于Laravel 5.4官方文档,重新梳理这部分内容。

一、请求的生命周期
1、简介

2、生命周期概览
第一件事
Laravel 应用的所有请求入口都是 public/index.php 文件,所有请求都会被 web 服务器(Apache/Nginx)导向这个文件。 index.php 文件包含的代码并不多,但是,这里是加载框架其它部分的起点。

index.php 文件载入 Composer 生成的自动加载设置,然后从 bootstrap/app.php 脚本获取 Laravel 应用实例,Laravel 的第一个动作就是创建服务容器实例。

HTTP/Console内核
接下来,请求被发送到 HTTP 内核或 Console 内核,这取决于进入应用的请求类型。这两个内核是所有请求都要经过的中央处理器,现在,就让我们聚焦在位于 app/Http/Kernel.php 的 HTTP 内核。

HTTP 内核继承自 Illuminate\Foundation\Http\Kernel 类,该类定义了一个 bootstrappers 数组,这个数组中的类在请求被执行前运行,这些 bootstrappers 配置了错误处理、日志、检测应用环境以及其它在请求被处理前需要执行的任务。

HTTP 内核还定义了一系列所有请求在处理前需要经过的 HTTP 中间件,这些中间件处理 HTTP 会话的读写、判断应用是否处于维护模式、验证 CSRF 令牌等等。

HTTP 内核的标志性方法 handle 处理的逻辑相当简单:获取一个 Request,返回一个 Response,把该内核想象作一个代表整个应用的大黑盒子,输入 HTTP 请求,返回 HTTP 响应。

服务提供者
内核启动过程中最重要的动作之一就是为应用载入服务提供者,应用的所有服务提供者都被配置在 config/app.php 配置文件的 providers 数组中。首先,所有提供者的 register 方法被调用,然后,所有提供者被注册之后,boot 方法被调用。

服务提供者负责启动框架的所有各种各样的组件,比如数据库、队列、验证器,以及路由组件等,正是因为他们启动并配置了框架提供的所有特性,服务提供者是整个 Laravel 启动过程中最重要的部分。

分发请求
一旦应用被启动并且所有的服务提供者被注册,Request 将会被交给路由器进行分发,路由器将会分发请求到路由或控制器,同时运行所有路由指定的中间件。

3、聚焦服务提供者
服务提供者是启动 Laravel 应用中最关键的部分,应用实例被创建后,服务提供者被注册,请求被交给启动后的应用进行处理,整个过程就是这么简单!

对 Laravel 应用如何通过服务提供者构建和启动有一个牢固的掌握非常有价值,当然,应用默认的服务提供者存放在 app/Providers 目录下。

默认情况下,AppServiceProvider 是空的,这里是添加自定义启动和服务容器绑定的最佳位置,当然,对大型应用,你可能希望创建多个服务提供者,每一个都有着更加细粒度的启动。

框架的启动流程参考:http://blog.ifeeline.com/2047.html

二、服务容器
三、服务提供者
服务容器与服务器提供者参考:http://blog.ifeeline.com/2507.html

四、Facades
Laravel 的门面作为服务容器中底层类的“静态代理”。Laravel 的所有门面都定义在 Illuminate\Support\Facades 命名空间。

在 Laravel 应用中,门面就是一个为容器中对象提供访问方式的类。该机制原理由 Facade 类实现。Laravel 自带的门面,以及我们创建的自定义门面,都会继承自 Illuminate\Support\Facades\Facade 基类。

门面类只需要实现一个方法:getFacadeAccessor。正是 getFacadeAccessor 方法定义了从容器中解析什么,然后 Facade 基类使用魔术方法 __callStatic() 从你的门面中调用解析对象。

门面 服务容器绑定
App Illuminate\Foundation\Application app
Artisan Illuminate\Contracts\Console\Kernel artisan
Auth Illuminate\Auth\AuthManager auth
Blade Illuminate\View\Compilers\BladeCompiler blade.compiler
Bus Illuminate\Contracts\Bus\Dispatcher
Cache Illuminate\Cache\Repository cache
Config Illuminate\Config\Repository config
Cookie Illuminate\Cookie\CookieJar cookie
Crypt Illuminate\Encryption\Encrypter encrypter
DB Illuminate\Database\DatabaseManager db
DB(Instance) Illuminate\Database\Connection
Event Illuminate\Events\Dispatcher events
File Illuminate\Filesystem\Filesystem files
Gate Illuminate\Contracts\Auth\Access\Gate
Hash Illuminate\Contracts\Hashing\Hasher hash
Lang Illuminate\Translation\Translator translator
Log Illuminate\Log\Writer log
Mail Illuminate\Mail\Mailer mailer
Notification Illuminate\Notifications\ChannelManager
Password Illuminate\Auth\Passwords\PasswordBrokerManager auth.password
Queue Illuminate\Queue\QueueManager queue
Queue(Instance) Illuminate\Contracts\Queue\Queue queue
Queue(Base Class) Illuminate\Queue\Queue
Redirect Illuminate\Routing\Redirector redirect
Redis Illuminate\Redis\Database redis
Request Illuminate\Http\Request request
Response Illuminate\Contracts\Routing\ResponseFactory
Route Illuminate\Routing\Router router
Schema Illuminate\Database\Schema\Blueprint
Session Illuminate\Session\SessionManager session
Session(Instance) Illuminate\Session\Store
Storage Illuminate\Contracts\Filesystem\Factory filesystem
URL Illuminate\Routing\UrlGenerator url
Validator Illuminate\Validation\Factory validator
Validator(Instance) Illuminate\Validation\Validator
View Illuminate\View\Factory view
View(Instance) Illuminate\View\View

五、Contracts

Contract References Facade
Illuminate\Contracts\Auth\Factory Auth
Illuminate\Contracts\Auth\PasswordBroker Password
Illuminate\Contracts\Bus\Dispatcher Bus
Illuminate\Contracts\Broadcasting\Broadcaster
Illuminate\Contracts\Cache\Repository Cache
Illuminate\Contracts\Cache\Factory Cache::driver()
Illuminate\Contracts\Config\Repository Config
Illuminate\Contracts\Container\Container App
Illuminate\Contracts\Cookie\Factory Cookie
Illuminate\Contracts\Cookie\QueueingFactory Cookie::queue()
Illuminate\Contracts\Encryption\Encrypter Crypt
Illuminate\Contracts\Events\Dispatcher Event
Illuminate\Contracts\Filesystem\Cloud
Illuminate\Contracts\Filesystem\Factory File
Illuminate\Contracts\Filesystem\Filesystem File
Illuminate\Contracts\Foundation\Application App
Illuminate\Contracts\Hashing\Hasher Hash
Illuminate\Contracts\Logging\Log Log
Illuminate\Contracts\Mail\MailQueue Mail::queue()
Illuminate\Contracts\Mail\Mailer Mail
Illuminate\Contracts\Queue\Factory Queue::driver()
Illuminate\Contracts\Queue\Queue Queue
Illuminate\Contracts\Redis\Database Redis
Illuminate\Contracts\Routing\Registrar Route
Illuminate\Contracts\Routing\ResponseFactory Response
Illuminate\Contracts\Routing\UrlGenerator URL
Illuminate\Contracts\Support\Arrayable
Illuminate\Contracts\Support\Jsonable
Illuminate\Contracts\Support\Renderable
Illuminate\Contracts\Validation\Factory Validator::make()
Illuminate\Contracts\Validation\Validator
Illuminate\Contracts\View\Factory View::make()
Illuminate\Contracts\View\View

跟踪框架的整个流程可以看到,容器中的很多绑定都是针对接口的,然后针对这些接口的绑定设置一个别名,Facade就根据这个别名取回绑定。

Laravel 5.x – 起步

一 安装
1 服务器要求
官方文档推荐使用Laravel Homestead作为本地开发环境(Mac推荐使用Valet作为本地开发环境)。Laravel Homestead是一个虚拟机镜像文件,基于Ubuntu,里面安装了所有必须软件(比如Nginx/PHP/MySQL等)。否则,开发环境需要满足:
PHP版本 >= 5.6.4
PHP扩展 OpenSSL,加密用到,并且只支持使用OpenSSL
PHP扩展 PDO,数据库链接只通过PDO进行链接,不支持其它方案,比如专门针对MySQL的mysqli驱动
PHP扩展 Mbstring,Multi-Byte String,PHP中的多字节字符串处理
PHP扩展 Tokenizer,这个扩展一般默认都会安装,详细可参看PHP文档
PHP扩展 XML

注:或者已经注意到,Laravel并不要求PHP的Session扩展,对的,它确实不需要,Laravel自己实现来一套Session管理。

2 安装Laravel
PHP中的生态系统的繁荣,composer功不可没,现代的PHP应用几乎都使用composer来管理依赖。简单来说,composer就是一个命令,这个命令本身就是使用PHP来编写的,它是一个phar压缩包,它会去分析composer.json中的配置,把所有依赖下载到vendor目录,并在vendor目录下提供了autoload.php,只要包含它就可以自动装载类(更多内容需要参看composer官方网站:getcomposer.org)。

可以直接到https://getcomposer.org/download/下载composer,比如当期最新版本为:https://getcomposer.org/download/1.4.1/composer.phar,对于Window,可以下载Windows安装包,它会帮我们设置环境变量,设置代理等,建议使用。

在Linux下,一般建议把composer.phar安装到/usr/local/bin中,并把composer.phar改名为composer,赋予可执行权限,这样composer就是全局可用的了(/user/local/bin默认在path变量中)。

由于直接链接都国外地址下载软件包比较慢,所以要愉快使用Composer,还需要做一些配置,有两种方式可选:
1 设置代理
2 设置composer使用国内镜像
可以在C:\Users\Administrator\AppData\Roaming\Composer中放入config.json(Linux:~/.config/composer/config.json),也可以在具体的项目的composer.json中手动添加:

{
    "config": {},
    "repositories": {
        "packagist": {
            "type": "composer",
            "url": "https://packagist.phpcomposer.com"
        }
    }
}

这里的配置指向了国内镜像,优先使用本地缓存。以上的内容也可以通过命令行完成:

#添加全局的config.json
composer config -g repo.packagist composer https://packagist.phpcomposer.com

#在具体的项目下运行(自动在composer.json文件中添加内容
composer config repo.packagist composer https://packagist.phpcomposer.com

全局的config.json会和具体项目中的composer.json文件合并(具体的会覆盖全局的)。

现在composer已经安装好。接下来安装Laravel:
1)通过Laravel安装器安装

composer global require "laravel/installer"

Laravel提供了一个安装器(实际就是一个程序包),通过composer安装到全局,然后调用这个安装器提供的命令行工具安装Laravel。

在CentOS 7.x中,全局包安装到了$home/.config/composer/verdor,所以为了全局可以使用verdor/bin里面的命令行工具,需要把这个路径添加到path中:

echo $PATH
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/root/binll]

#临时生效
export /home/www/.config/composer/vendor/bin:$PATH

#永久生效
vi .bashrc
export /home/www/.config/composer/vendor/bin:$PATH
source .bashrc

然后运行laravel new安装Laravel应用:

laravel new blog

2)通过Composer直接安装
相比通过Laravel安装器安装(链接上laravel的一个服务,类似通过代理链接composer官方代码仓库),直接使用Composer来安装就相对简单直接一些:

composer create-project --prefer-dist laravel/laravel blog

#指定安装版本
composer create-project --prefer-dist laravel/laravel blog 5.3.*

注:可以使用PHP内置的开发环境服务器来运行应用,Laravel提供了支持:

php artisan serve
Laravel development server started: <http://127.0.0.1:8000>

不过,与其间接这样整,不如直接运行php命令:

php -S 0.0.0.0:8000 -t /home/www/blog/public

如果不是使用PHP内置的应用服务器来运行Laravel,就需要配置HTTP服务器执行public目录,其中的index.php作为前端控制器,是所有请求的入口。

需要注意的是:storage 和 bootstrap/cache 目录应该是可写的;如果通过安装器或composer直接安装,那么根目录下.env文件已经自动生成,并且其中的APP_KEY已经产生,否则就需要手动产生,拷贝.env.example为.env,运行php artisan key:generate自动生成。

以下是一个Nginx配置例子(来自Homestead的默认配置):

server {
    listen 80;
    #listen 443 ssl http2;
    server_name blog.app;
    root "/var/www/blog/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;
    error_log  /var/log/nginx/blog.app-error.log error;

    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/blog.app.crt;
    #ssl_certificate_key /etc/nginx/ssl/blog.app.key;
}

二、配置
Larave的配置放在config目录下。

1 环境配置
Laravel 使用 Vance Lucas 开发的 PHP 库 DotEnv 来实现这一机制,在新安装的 Laravel 中,根目录下有一个 .env.example 文件,如果 Laravel 是通过 Composer 安装的,那么该文件已经被重命名为 .env,否则的话你要自己手动重命名该文件。(还可以创建一个 .env.testing 文件,该文件会在运行 PHPUnit 测试或执行带有 –env=testing 选项的 Artisan 命令时覆盖从 .env 文件读取的值。)

在应用每次接受请求时,.env 中列出的所有配置及其值都会被载入到 PHP 超全局变量 $_ENV 中,然后你就可以在应用中通过辅助函数 env 来获取这些配置值。

'debug' => env('APP_DEBUG', false),

注:.env不能提交给版本库;获取环境配置的值,应该总是在配置文件中进行(调用env()函数),因为配置可缓存;.env.example是一个模板文件,所有可用配置应该都写入模板文件。

当前应用环境由 .env 文件中的 APP_ENV 变量决定,可以通过App门面的environment方法来访问其值:

$environment = App::environment();

#也可以向 environment 方法中传递参数来判断当前环境是否匹配给定值
if (App::environment('local')) {
    // The environment is local
}

if (App::environment('local', 'staging')) {
    // The environment is either local OR staging...
}

配置值的访问可以使用config函数在任意位置访问配置值:

$value = config('app.timezone');

#动态改变配置值
config(['app.timezone' => 'America/Chicago']);

可以对config中的配置文件合并为一个文件:

#本地开发环境就不需要这样用
php artisan config:cache

生成环境中,尽管已经把配置缓存了(生成了一个文件),但每个请求都要读一次这个根本不改变的配置文件还是比较耗费资源的,完全可以写入共享内存,具体可以参考:http://blog.ifeeline.com/2157.html

维护模式:

php artisan down

php artisan up

返回503。当处于维护模式中时,所有的队列任务都不会执行

三、应用目录结构
https://laravel.com/docs/5.4/structure(http://laravelacademy.org/post/6681.html)

四、Homestead
这部分内容参考:http://blog.ifeeline.com/2524.html

虽然实际上并不使用Homestead作为开发环境,但是需要提一下的是,Laravel本身是一个框架,除了框架,还提供了之外的一系列工具(Homestead是一个),而且这些工具都不错,这个是其它框架无法达到的。

五、Valet

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’)即可。

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

MySQL 插入数据之replace和ignore

MySQL中处理常规的insert into之外,也提供了replace insert和insert ignore into语句。

当需要插入不重复的值(有唯一索引),常规的插入就必须先判断是否存在,然后再进行插入,这个在需要批量插入数据时,需要循环查询判断,如果使用replace insert和insert ignore into语句就不需要这样做,insert ignore into很好理解,对比唯一索引,如果存在直接跳过,而replace insert是指存在时,对存在的数据行进行更新,准确来说应该是对存在的数据行进行删除,然后插入新的。

所以使用replace insert要特别小心,它是先删除,再插入,比如插入一个已经存在的行,它会返回受影响的行是2,如果新的行没有包含原来的全部数据,那么这部分数据将丢失,如果设置了id为自动增长的,就可以看到,id将不会连续(先删除后插入的缘故)。

以下是一个trait,用来扩展Laravel ORM模型以支持insertReplace和insertIgnore这样的语法:

<?php

namespace Ebt\ModelExtend;

trait InsertReplaceable 
{
    public static function insertReplace(array $attributes = []) 
    {
        return static::executeQuery ( 'replace', $attributes );
    }
    
    public static function insertIgnore(array $attributes = []) 
    {
        return static::executeQuery ( 'insert ignore', $attributes );
    }
    
    protected static function executeQuery($command, array $attributes) 
    {
        $prefix = \DB::connection()->getTablePrefix();
        if (! count ( $attributes )) {
            return true;
        }
        $model = new static ();
        if ($model->fireModelEvent ( 'saving' ) === false) {
            return false;
        }
        $attributes = collect ( $attributes );
        $first = $attributes->first ();
        if (! is_array ( $first )) {
            $attributes = collect ( [ 
                $attributes->toArray () 
            ] );
        }
        $keys = collect ( $attributes->first () )->keys ()->transform ( function ($key) {
            return "`" . $key . "`";
        } );
        $bindings = [ ];
        $query = $command . " into " .  $prefix . $model->getTable () . " (" . $keys->implode ( "," ) . ") values ";
        $inserts = [ ];
        foreach ( $attributes as $data ) {
            $qs = [ ];
            foreach ( $data as $value ) {
                $qs [] = '?';
                $bindings [] = $value;
            }
            $inserts [] = '(' . implode ( ",", $qs ) . ')';
        }
        $query .= implode ( ",", $inserts );
        \DB::connection ( $model->getConnectionName () )->insert ( $query, $bindings );
        $model->fireModelEvent ( 'saved', false );
    }
}

用法:

$data = [
    [
        "name" => 'ifeeline',
        "note" => "xx"
    ],
    [
        "name" => 'ifeeline2',
        "note" => "yy"
    ],
];
//\App\TestTest::insertReplace($data);
\App\TestTest::insertIgnore($data);

Laravel 数据统计实例

以下程序大概对几百万数据进行汇总统计,依赖关系数据库而不是在程序中进行计算,所以需要先导入数据。

<?php
/* 统计订单重复购买率等
 * 
 * 用法:
 * 1 数据导入(定位文件:storage/app/orders/amazon/default/2015_01.txt)
 * php artisan statistic:order:purchase:rate --platform=amazon --account=default --year=2016 --month=01
 *  
 * 2 数据统计 (1月份)
 * php artisan statistic:order:purchase:rate --platform=amazon --account=default --year=2016 --month=01 --fire
 * 
 * 3 数据统计 (全年,--month=all表示全年)
 * php artisan statistic:order:purchase:rate --platform=amazon --account=default --year=2016 --month=all --fire
 * 
 */

namespace App\Console\Commands;

use Illuminate\Console\Command;

use DB,Storage,Validator;

class StatisticOrderPurchaseRate extends Command
{
    protected $signature = 'statistic:order:purchase:rate {--platform=} {--account=} {--year=} {--month=} {--fire}';
    protected $description = '';
    
    public function __construct()
    {       
        parent::__construct();
    }

    public function handle()
    {   
        $prefix = \DB::connection()->getTablePrefix();
        
        $platform = strtolower(trim($this->option('platform')));
        if(!in_array($platform, ['amazon', "ebay"])) {
            echo "平台必须是:amazon 和 ebay";
            return;
        }
        
        $account = strtolower(trim($this->option('account')));
        if(empty($account)) {
            $account = "default";
        }
        
        $year = strtolower(trim($this->option('year')));
        $month = strtolower(trim($this->option('month')));
        
        if(empty($year) || empty($month)) {
            echo "年 月必须指定,用来定位文件";
            return;
        }
        
        $fire = $this->option('fire');
        if(empty($fire)) {
            $file = "orders/".$platform."/".$account."/".$year."_".$month.".txt";
            
            if(!Storage::exists($file)){
                echo "数据文件无法定位:$file";
                return;
            }
            
            $filePath = storage_path("app/".$file);
            
            // 读取数据文件  一次性读完,然后批量插入数据库
            if($platform === "amazon") {
                $fp = fopen($filePath,"r");
                $data = array();
                while($line = fgetcsv ($fp, 0, "\t", ' ')) {
                    if(!isset($line[0])) { continue; }
                    
                    $order_id = $line[0];
                    if($order_id === 'amazon-order-id') {
                        continue;
                    }
                    $email = $line[10];
                    $total = round($line[17],2);
                    
                    if(isset($data[$order_id])) {
                        $data[$order_id]["total"] += $total;
                    } else {
                        $data[$order_id] = [
                            "platform" => $platform,
                            "account" => $account,
                            "year" => $year,
                            "month" => $month,
                            "order_id" => $order_id,
                            "email" => $email,
                            "total" => $total
                        ];
                    }
                }
                fclose($fp);
                
                // 数据分块批量插入数据库
                if(\DB::statement("DELETE FROM ".$prefix."order_summary WHERE platform='".$platform."' and account='".$account."' and year='".$year."' and month='".$month."'")) {
                    
                    // 分块插入
                    $chunks = array_chunk($data, 1000);
                    foreach($chunks as $chunk) {
                        \DB::table('order_summary')->insert($chunk);
                    }
                    
                }            
            } else {
                // 
            }
        
        } else {
            
            if($month === 'all') {
                // 客单价
                $total_order = DB::select("SELECT sum(total) as total_sale, count(order_id) as total_order, (sum(total)/count(order_id)) as avg_price,
                count(distinct email) as total_customer
                FROM ".$prefix."order_summary
                WHERE platform='".$platform."' and account='".$account."' and year='".$year."' and email != ''");
            } else {
                // 客单价
                $total_order = DB::select("SELECT sum(total) as total_sale, count(order_id) as total_order, (sum(total)/count(order_id)) as avg_price,
                count(distinct email) as total_customer
                FROM ".$prefix."order_summary
                WHERE platform='".$platform."' and account='".$account."' and year='".$year."' and month='".$month."' and email != ''"); 
            }
            
            $has = DB::table("order_lookup")->where("platform", $platform)->where("account", $account)
            ->where("year", $year)->where("month", $month)->first();
            
            if(isset($has->id)) {
                \DB::table("order_lookup")->where("platform", $platform)->where("account", $account)
                ->where("year", $year)->where("month", $month)->update([
                    "total_sale" => $total_order[0]->total_sale,
                    "total_order" => $total_order[0]->total_order,
                    "avg_price" => $total_order[0]->avg_price,
                    "total_customer" => $total_order[0]->total_customer
                ]);
            } else {
                \DB::table("order_lookup")->insert([
                    "platform" => $platform,
                    "account" => $account,
                    "year" => $year,
                    "month" => $month,
                    "total_sale" => $total_order[0]->total_sale,
                    "total_order" => $total_order[0]->total_order,
                    "avg_price" => $total_order[0]->avg_price,
                    "total_customer" => $total_order[0]->total_customer
                ]);
            }
            
            if($month === 'all') {
                // 获取重复购买
                $repeate = DB::table("order_summary")->where("platform", $platform)->where("account", $account)
                ->where("year", $year)->where("email", "!=", "")->groupBy("email")->havingRaw("count(order_id) > 1")->get(["email"]); 
            } else {
                // 获取重复购买
                $repeate = DB::table("order_summary")->where("platform", $platform)->where("account", $account)
                ->where("year", $year)->where("month", $month)->where("email", "!=", "")->groupBy("email")->havingRaw("count(order_id) > 1")->get(["email"]);
            }
            $repeate_total = count($repeate);
            
            if($total_order[0]->total_customer > 0) {
                \DB::table("order_lookup")->where("platform", $platform)->where("account", $account)
                ->where("year", $year)->where("month", $month)->update([
                    "total_customer_repeat" => $repeate_total,
                    "total_customer_uniq" => (int)($total_order[0]->total_customer - $repeate_total),
                    "total_customer_repeat_rate" => round($repeate_total/$total_order[0]->total_customer,5)
                ]);
            }
        }
    }
}
/*
SET FOREIGN_KEY_CHECKS=0;

-- ----------------------------
-- Table structure for order_summary
-- ----------------------------
DROP TABLE IF EXISTS `order_summary`;
CREATE TABLE `order_summary` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `platform` varchar(32) COLLATE utf8_unicode_ci DEFAULT NULL,
  `account` varchar(64) COLLATE utf8_unicode_ci DEFAULT NULL,
  `year` varchar(16) COLLATE utf8_unicode_ci DEFAULT NULL,
  `month` varchar(16) COLLATE utf8_unicode_ci DEFAULT NULL,
  `order_id` varchar(32) COLLATE utf8_unicode_ci DEFAULT NULL,
  `email` varchar(128) COLLATE utf8_unicode_ci DEFAULT NULL,
  `total` decimal(10,2) DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uniq_order` (`platform`,`account`,`year`,`month`,`order_id`),
  KEY `account` (`account`) USING BTREE,
  KEY `year` (`year`) USING BTREE,
  KEY `month` (`month`) USING BTREE,
  KEY `email` (`email`) USING BTREE,
  KEY `platform` (`platform`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1108415 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;

-- ----------------------------
-- Table structure for order_lookup
-- ----------------------------
DROP TABLE IF EXISTS `order_lookup`;
CREATE TABLE `order_lookup` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `platform` varchar(32) COLLATE utf8_unicode_ci DEFAULT NULL,
  `account` varchar(64) COLLATE utf8_unicode_ci DEFAULT NULL,
  `year` varchar(16) COLLATE utf8_unicode_ci DEFAULT NULL,
  `month` varchar(16) COLLATE utf8_unicode_ci DEFAULT NULL,
  `total_sale` decimal(10,2) DEFAULT NULL,
  `total_order` int(11) DEFAULT NULL,
  `avg_price` decimal(10,2) DEFAULT NULL COMMENT '客单价(总销售/总订单)',
  `total_customer` int(11) DEFAULT NULL,
  `total_customer_uniq` int(11) DEFAULT NULL COMMENT '没有重复购买',
  `total_customer_repeat_rate` decimal(10,4) DEFAULT NULL,
  `total_customer_repeat` int(11) DEFAULT NULL COMMENT '有重复购买',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=14 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
*/

PHP Laravel Excel

PHP Laravel Excel官方网址:http://www.maatwebsite.nl/laravel-excel/docs;

GitHub: https://github.com/Maatwebsite/Laravel-Excel;
Packagist: https://packagist.org/packages/maatwebsite/excel

这个程序包依赖PHPOffice/PHPExcel,实际就是在这个工具包上做了一些易于用户操作的封装。需要说明的是,目录依赖的PHPOffice/PHPExcel版本是1.8.*(也是当前的稳定版),在1.9.*和2.0.*中开始引入PHP的命名空间,意味着PHP版本至少要5.3以上,这两个分支还在开发中,看起来这个包的作者非常的保守。

安装就按照Laravel套路来就好:

#往composer.json中添加"maatwebsite/excel": "~2.1.0",然后update

#添加ServiceProvider
vi config/app.php
'Maatwebsite\Excel\ExcelServiceProvider',

#添加Facade(可选)
'Excel' => 'Maatwebsite\Excel\Facades\Excel',

#配置(会添加excel.php配置文件)
php artisan vendor:publish --provider="Maatwebsite\Excel\ExcelServiceProvider"

#获取excel实例
$excel = App::make('excel');

Maatwebsite/excel本身有一个默认的配置文件,如果应用级别有配置文件,那么应用配置将覆盖默认配置配置,具体实现是在ExcelServiceProvider中:

    public function boot()
    {
        $this->publishes([
            __DIR__ . '/../../config/excel.php' => config_path('excel.php'),
        ]);

        $this->mergeConfigFrom(
            __DIR__ . '/../../config/excel.php', 'excel'
        );

        //Set the autosizing settings
        $this->setAutoSizingSettings();
    }

表格操作,主要涉及输入和输出。

Excel::load('file.xls', function($reader) {

    // Getting all results
    $results = $reader->get();

    // ->all() is a wrapper for ->get() and will work the same
    $results = $reader->all();

});

使用get和all方法获取结果,默认,如果表格只有一个sheet,那么直接返回行集合,如果有多个sheet,那么返回sheet的集合,每个sheet又是行的集合。为了统一操作,可以设置配置文件中的force_sheets_collection设置为true,这样都会返回sheet的集合。

表格sheet的第一行是头部,默认会被转换成slug(别名),可以设置import.heading为false表示不使用文件头,可用值true|false|slugged|slugged_with_count|ascii|numeric|hashed|trans|original,设置为original表示使用字面值作为key,这个比较常见。

Sheet/行/单元格都是集合,使用了get()之后,就可以使用集合的方法。

#
$reader->get()->groupBy('firstname');

#依赖force_sheets_collection,可能返回第一个sheet或第一个行
$reader->first();
// Get workbook title
$workbookTitle = $reader->getTitle();

foreach($reader as $sheet)
{
    // get sheet title
    $sheetTitle = $sheet->getTitle();
}

// You can either use ->take()
$reader->take(10);

// Or ->limit()
$reader->limit(10);

// Skip 10 results
$reader->skip(10);

// Skip 10 results with limit, but return all other rows
$reader->limit(false, 10);

// Skip and take
$reader->skip(10)->take(10);

// Limit with skip and take
$reader->($skip, $take);

$reader->toArray();

$reader->toObject();

// Dump the results
$reader->dump();

// Dump results and die
$reader->dd();

#也可以使用foreach
// Loop through all sheets
$reader->each(function($sheet) {

    // Loop through all rows
    $sheet->each(function($row) {

    });

});

选择Sheet和列

#仅加载sheet1
Excel::selectSheets('sheet1')->load("xx.xls", function($reader) {});
Excel::selectSheets('sheet1', 'sheet2')->load();

#通过下标选择比较靠谱
// First sheet
Excel::selectSheetsByIndex(0)->load();

// First and second sheet
Excel::selectSheetsByIndex(0, 1)->load();

#选择列,很熟悉的用法?
#All get methods (like all(), first(), dump(), toArray(), ...) accept an array of columns.
// Select
$reader->select(array('firstname', 'lastname'))->get();

// Or
$reader->get(array('firstname', 'lastname'));

日期:
By default the dates will be parsed as a Carbon object.

分批导入:

Excel::filter('chunk')->load('file.csv')->chunk(250, function($results)
{
        foreach($results as $row)
        {
            // do stuff
        }
});

每次读入250行,处理完毕在导入250行??

批量导入:

Excel::batch('app/storage/uploads', function($rows, $file) {

    // Explain the reader how it should interpret each row,
    // for every file inside the batch
    $rows->each(function($row) {

        // Example: dump the firstname
        dd($row->firstname);

    });

});

$files = array(
    'file1.xls',
    'file2.xls'
);

Excel::batch($files, function($rows, $file) {

});

Excel::batch('app/storage/uploads', function($sheets, $file) {

    $sheets->each(function($sheet) {

    });

});

导出也有很多定制化的操作,参考:http://www.maatwebsite.nl/laravel-excel/docs/export

例子:

// 关联数组,输出表头
$excel_array = [
    [
        "表头1" => "xxxx",
        "表头2" => "yyyy"
    ],
    [
        "表头1" => "xxxx2",
        "表头2" => "yyyy3"
    ]
];
// 直接数据输入
$excel_array2 = [
    [
        "表头1", "表头2"
    ],
    [
        "xxxx", "yyyy"
    ],
    [
        "xxxx2", "yyyy3"
    ]
];

        \Excel::create("test1", function ($excel) use($excel_array) {
            $excel->sheet('sheet1', function ($sheet) use($excel_array) {
                $sheet->fromArray($excel_array);
            });
        })->store("xls","d:/");
        
        \Excel::create("test2", function ($excel) use($excel_array2) {
            $excel->sheet('sheet1', function ($sheet) use($excel_array2) {
                $sheet->fromArray($excel_array2, null, 'A1', false, false);
            });
        })->save("xls");

默认,如果不指定导入的路径,会保存到storage_path(‘exports’),即app/storage/exports。可以修改配置文件export.store.path的值。

导出的sheet,默认第一行总是头部,这个可以修改配置文件的export.generate_heading_by_indices为false取消这个默认值。也可以指定fromArray的第5参数为false达到同样目的。

store()的第一参数是导入文件的类型,第二参数是路径(不需要包含文件名),第三参数控制是否返回保存文件的信息(比如保存的路径,扩展名等)。