标签归档:laravel

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

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

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

Laravel 任务调度

Laravel中提供了一个任务调度实现,基本实现原理大体并不复杂,依赖操作系统的Crontab服务,每分钟运行一次schedule:run命令,然后判断到期任务,需要注意的是,如果schedule:run命令不是每分钟执行一次,那么调度就有可能有问题,比如:

0-59/3 * * * * 		php /mnt/www/ebt/artisan schedule:run 1>> /dev/null 2>&1

如下调度:

    protected function schedule(Schedule $schedule)
    {
        $schedule->command('test:test2')
                 ->cron('0-59/2 * * * *')
		 ->sendOutputTo("/home/www/schedule.txt");
    }

这个调度本意是每两分钟执行一次,而调度命令是3分钟执行一次,所以无法符合预期。从输出的情况来看,30分和36分执行了一次test:test2命令,那么就基本可以肯定,所谓的调度,实际就是根据当前时间,比对分钟数字,比如设置每两分钟执行一次,那么就比对调度命令被运行时的分钟数是不是2的倍数,其它的情况也是类似的。这个是最简单的实现方案了。所以,调度程序运行需要设置为每分钟执行一次。

以下示例:

<?php

namespace App\Console;

use DB;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;

class Kernel extends ConsoleKernel{
    /**
     * 应用提供的Artisan命令
     *
     * @var array
     */
    protected $commands = [
        'App\Console\Commands\Inspire',
    ];

    /**
     * 定义应用的命令调度
     *
     * @param  \Illuminate\Console\Scheduling\Schedule  $schedule
     * @return void
     */
    protected function schedule(Schedule $schedule)
    {
        // 闭包
        $schedule->call(function () {
            DB::table('recent_users')->delete();
        })->daily();

        // Artisan命令
        $schedule->command('emails:send --force')->daily();

        // 系统命令
        $schedule->exec('node /home/forge/script.js')->daily();
    }
}

可用方法列表:

->cron('* * * * *');
->everyMinute();
->everyFiveMinutes();
->everyTenMinutes();
->everyThirtyMinutes();
->hourly();
->daily();
->dailyAt('13:00');
->twiceDaily(1, 13);
->weekly();
->monthly();

额外的调度约束列表:
->weekdays();
->sundays();
->mondays();
->tuesdays();
->wednesdays();
->thursdays();
->fridays();
->saturdays();
->when(Closure);

大体上,对应Crontab的设置。这里的when()方法接收一个闭包函数,当这个函数返回true时,才执行。如果使用Crontab调度,就需要在运行脚本里面做控制。

由于这个任务调度器实际就是一个启动其它命令的命令,所以对于判断重复运行是很容易做到的,比如上一次运行没有结束,那么当前虽然也满足了运行的条件,也不去运行它,这个在Crontab中也是做不到的(需要在实际运行命令中控制):

$schedule->command('emails:send')->everyMinute()->withoutOverlapping();

方法withoutOverlapping()控制重复运行。

运行结果输出(结果输出到指定文件):

$schedule->command('emails:send')
         ->daily()
         ->sendOutputTo($filePath);

// 发邮件,需要先调用sendOutputTo
// emailOutputTo和sendOutputTo方法只对command方法有效,不支持call方法
$schedule->command('foo')
         ->daily()
         ->sendOutputTo($filePath)
         ->emailOutputTo('foo@example.com');

预留钩子:

$schedule->command('emails:send')
         ->daily()
         ->before(function () {
             // Task is about to start...
         })
         ->after(function () {
             // Task is complete...
         });

Laravel 邮件

Laravel中的发送邮件部分是swiftmailer的封装。它封装的意义在于使用Laravel并且更加容易的使用。目前存在的邮件相关的程序包很多,没有必要自己实现一个。swiftmailer大概是一个不错的邮件包。

关于邮件发送的内容,本身涉及到的内容是非常多的,但是我们可能要使用的仅仅是发送邮件,大部分情况下,我们需要的仅仅一个模子程序:

$mes = '你好';
        
$image = Image::make(storage_path('app/files/tt.jpg'));
$imgBase64String = $image->encode('data-url');
        
\Mail::send("emails.test", [
    'mes' => $mes,
    // 二进制数据流
    'image1'=>Storage::get('app/files/tt.jpg'),
    'image2'=>$imgBase64String         
], function($message) {
    $message->to('ifeeline@qq.com')->subject('test');
            
    //在邮件中上传附件,附加的名称乱码处理
    $attachment = storage_path('app/files/test.doc');
    $message->attach($attachment,['as'=>"=?UTF-8?B?".base64_encode('队列')."?=.doc"]);
});

## 模板
<span>Hello {{$mes}}</span>
<img src="{{$message->embedData($image1,'tt.jpg')}}">
<img src="{!!$image2!!}">

要在邮件主体中嵌入图片,最简便的方法就是直接生成data-url字符串。另外,附件中文名称的处理也算奇葩了。

Laravel 中使用 Redis

Laravel中对Redis的访问是通过predis/predis实现的,这个包是纯PHP实现(注意:PHP中有一个redis扩展实现,如果追求高效,可以使用它)。

安装predis/predis

php composer.phar require predis/predis

Laravel本身提供了一个叫Redis的Facade:

Redis      redis       Illuminate\Redis\Database

Illuminate\Redis\Database是一层简单的封装,内部使用Predis\Client(predis/predis包提供)。大概看看serviceProvider:

<?php

namespace Illuminate\Redis;

use Illuminate\Support\ServiceProvider;

class RedisServiceProvider extends ServiceProvider
{
    /**
     * Indicates if loading of the provider is deferred.
     *
     * @var bool
     */
    protected $defer = true;

    /**
     * Register the service provider.
     *
     * @return void
     */
    public function register()
    {
        $this->app->singleton('redis', function ($app) {
            return new Database($app['config']['database.redis']);
        });
    }

    /**
     * Get the services provided by the provider.
     *
     * @return array
     */
    public function provides()
    {
        return ['redis'];
    }
}

首先,这是一个延时服务(defer为true)。说明实际使用到时才实例化。redis对应一个Illuminate\Redis\Database实例。可以看到,它单独放入了Redis目录,可见这个东西确实是一个比较特殊的东西,不太好跟其它的东西并列。

针对Redis的配置,在config/database.php中用redis键配置:

    'redis' => [

        'cluster' => false,

        'default' => [
            'host'     => '127.0.0.1',
            'port'     => 6379,
            'database' => 0,
        ],
        'cache' => [
            'host'     => '127.0.0.1',
            'port'     => 6380,
            'database' => 0,
        ],
        'session' => [
            'host'     => '127.0.0.1',
            'port'     => 6381,
            'database' => 0,
        ],
    ]

很明显,可以设置多个配置。如果需要使用Redis作为缓存,需要修改config/cache.php:

'default' => env('CACHE_DRIVER', 'redis'),

'stores' => [
        'redis' => [
            'driver' => 'redis',
            'connection' => 'default',
        ],
],

其中的connection对应的default,就是database.php中redis的default,可以为cache专门指定一个链接(Redis实例)。

如果要把Session数据存储到Redis中,可以修改session.php文件:

'driver' => env('SESSION_DRIVER', 'redis'),
'connection' => 'session', // 对应Redis的配置

使用上,很简单,通过Redis进行:

Redis::get($key);
Redis::keys("*");
Redis::set($key,$value);
Redis::exists($key);

基本上,取决于你对Redis的熟悉程度了。

另外,PHP中的redis扩展会在全局空间中产生一个叫redis的类,这个名称和这里的Redis冲突,所以,如果安装redis扩展,你需要到app.php中将Redis别名改为其它的。

Laravel 配置详解

框架初始化流程:
实例化$app容器(Illuminate\Foundation\Application),然后调用$app->make(Illuminate\Contracts\Http\Kernel::class)产生一个$kernel,捕获请求信息生成$request对象($request = Illuminate\Http\Request::capture()),调用$kernel的handle()方法并把$request注入这个方法中。handle()方法在Illuminate\Foundation\Http\Kernel中,它的核心代码是$response = $this->sendRequestThroughRouter($request),sendRequestThroughRouter方法是进入点:

    protected function sendRequestThroughRouter($request)
    {
        $this->app->instance('request', $request);

        Facade::clearResolvedInstance('request');

        $this->bootstrap();

        return (new Pipeline($this->app))
                    ->send($request)
                    ->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware)
                    ->then($this->dispatchToRouter());
    }

首先把请求对象注入容器,接下来就是bootstrap(),最后是请求经过中间件,分发到路由器。这里关注点在bootstrap()方法:

    public function bootstrap()
    {
        if (! $this->app->hasBeenBootstrapped()) {
            $this->app->bootstrapWith($this->bootstrappers());
        }
    }

这里的$this->bootstrappers()返回的是$kernel对象的$bootstrappers数组(可以在最终类覆盖),然后调用容器的bootstrapWith()方法,类文件是Illuminate\Foundation\Application:

    public function bootstrapWith(array $bootstrappers)
    {
        $this->hasBeenBootstrapped = true;

        foreach ($bootstrappers as $bootstrapper) {
            $this['events']->fire('bootstrapping: '.$bootstrapper, [$this]);

            $this->make($bootstrapper)->bootstrap($this);

            $this['events']->fire('bootstrapped: '.$bootstrapper, [$this]);
        }
    }

可见是根据传递进来的$bootstrappers,然后循环实例化,调用实例的bootstrap()方法而已,所以我们知道,$kernel的$bootstrappers数组的每个元素是一个字符串类名,这些类都必须有bootstrap()方法:

    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',
    ];

在bootstrap前,容器已经初步初始化,经过bootstrap后的容器,才是真正初始化后的容器。 这个启动流程仅为热身。

##Illuminate\Foundation\Bootstrap\LoadConfiguration
<?php
namespace Illuminate\Foundation\Bootstrap;

class LoadConfiguration
{
    public function bootstrap(Application $app)
    {
        $items = [];

        if (file_exists($cached = $app->getCachedConfigPath())) {
            $items = require $cached;

            $loadedFromCache = true;
        }

        $app->instance('config', $config = new Repository($items));

        if (! isset($loadedFromCache)) {
            $this->loadConfigurationFiles($app, $config);
        }

        date_default_timezone_set($config['app.timezone']);

        mb_internal_encoding('UTF-8');
    }
    // ...
}

首先判断配置文件的缓存是否存在,如果存在就直接使用这个缓存。 $app->getCachedConfigPath():

public function getCachedConfigPath()
    {
        return $this->basePath().'/bootstrap/cache/config.php';
    }

就是一个文件路径,想必必定是所有配置文件的合并。如果这个文件不存在的,就遍历conf下的所有php结尾的文件,然后把配置文件require进$config对象,它是一个Illuminate\Config\Illuminate\Config实例(这个过程由loadConfigurationFiles实现),这个对象简单来说就是组装成一个如下的关联数组:

$config[
    'app' => [
        'timezone' => 'UTC',
    ],
    'auth' => [],
    'cache' => [],
]

如果要获取值,可以$config->get(‘app.timezone’),可以看到,可以很便利地使用点语法获取配置。以上提到,配置文件可以合并缓存,缓存起来的实际就是一个类似如上组织的大文件。这个文件可以使用如下命令生成:

php artisan config:cache

这个命令将会生成bootstrap/cache/config.php文件,它是所有文件的合并。一旦这个文件存在,那么框架在载入配置时就使用这个文件,同时也意味者如果修改了配置,需要重新生成这个文件。

所以,如果需要添加自己的配置,完全可以往config目录里面简单放入一个php文件,只要return一个关联数组即可。

获取配置值:

$app = config('app');
$timezone = $app->get('timezone');

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

$config = app('config');
$timezone = $config->get('app.timezone');

虽然可以把所有文件合并为一个大的文件缓存起来,但是每次请求都会解析一次配置,然后把配置装入一个对象,而配置几乎是不变的,并不需要每次请求都重复一次这个过程,可以使用apcu扩展来保存这个配置对象。apcu是一个共享内存方案,保存在其中的变量在多个进程中共享。

配置文件的加载是由Illuminate\Foundation\Bootstrap\LoadConfiguration来完成的,这个类是Illuminate\Foundation\Http\Kernel的中的$bootstrapper数组指定的:

    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',
    ];

而我们可以配置的App\Http\Kernel类继承了Illuminate\Foundation\Http\Kernel类,所以只需要在App\Http\Kernel中覆盖这个变量就可以用自定义的类来载入配置:

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

自定义的类继承Illuminate\Foundation\Bootstrap\LoadConfiguration,覆盖bootstrap()方法即可:

<?php

namespace App\Http\Bootstrap;

use Illuminate\Config\Repository;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Foundation\Bootstrap\LoadConfiguration as SystemLoadConfiguration;

class LoadConfiguration extends SystemLoadConfiguration
{
    public function bootstrap(Application $app)
    {
        $configKey = 'nal_config';
        $cacheConfig = env('CACHE_CONFIG', false);
        if ($cacheConfig) {
            if (extension_loaded('apcu')) {
                if (apcu_exists($configKey)) {
                    $this->load($app, apcu_fetch($configKey));
                } else {
                    apcu_store($configKey, $this->load($app, null, true));
                }
            } else {
                $this->load($app);
            }
        } else {
            if (extension_loaded('apcu') && apcu_exists($configKey)) {
                apcu_delete($configKey);
            }
            $this->load($app);
        }
    }

    public function load($app, $config = null)
    {
        if (!empty($config)) {
            $app->instance('config', $config);
        } else {
            $items = [];

            // First we will see if we have a cache configuration file. If we do, we'll load
            // the configuration items from that file so that it is very quick. Otherwise
            // we will need to spin through every configuration file and load them all.
            if (file_exists($cached = $app->getCachedConfigPath())) {
                $items = require $cached;

                $loadedFromCache = true;
            }

            $app->instance('config', $config = new Repository($items));

            // Next we will spin through all of the configuration files in the configuration
            // directory and load each one into the repository. This will make all of the
            // options available to the developer for use in various parts of this app.
            if (!isset($loadedFromCache)) {
                $this->loadConfigurationFiles($app, $config);
            }
        }

        date_default_timezone_set($config['app.timezone']);

        mb_internal_encoding('UTF-8');

        return $config;
    }
}

这样就可以实现配置共享了。一旦这么干,如果修改了配置,就需要重启或清空缓存才能让配置生效。

Laravel – Redis实例

<?php
 
namespace eBay\Console\Commands;
 
use Illuminate\Console\Command;
use DB;
use Schema;
 
class TitleRepeat extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'title:repeat {--list=}';
 
    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = '';
 
    /**
     * Create a new command instance.
     *
     * @return void
     */
    public function __construct()
    {
        parent::__construct();
    }
 
    /**
     * Execute the console command.
     *
     * @return mixed
     */
    public function handle()
    {
        $config = app('config');
        
        // Redis扩展不存在
        if (!extension_loaded('redis')) {
            echo "Redis do not exists";
            return;
        }
        
        // 链接Redis
        $redis = new \Redis();
        $redisServer = $config->get('app.redisServer');
        $redisPort = $config->get('app.redisPort');
        
        if(!$redis->connect($redisServer, $redisPort)) {
            echo 'Connect Redis Host:'.$redisServer." Port:".$redisPort." Fail";
            return;
        }
        
        // 单个  或  全量
        $pids = $this->option('list');
        if(!empty($pids)) {
            $pids = explode(',', preg_replace('/\s+/', '', $pids));
            $mainSkus = DB::table("ebay_item_master")->whereIn('id',$pids)->get();
        } else {
            $mainSkus = DB::table('ebay_item_master')->where('if_primary','>',0)
                ->select(['sku','title1','title2','title3','title4','title5','title6'])->get();
        }
        if(count($mainSkus) < 1){
            echo 'No Products...';
            return;
        }
        
        foreach($mainSkus as $mainSku){
            $sku = trim($mainSku->sku);
             
            for($i = 1; $i <= 6; $i++) {
                $title = 'title'.$i;
                // 不理会空的
                if(empty($mainSku->$title)){ continue; }
                 
                // 计算哈希
                $titleValueHash = $this->getStringHash($mainSku->$title);
                $hashHold = $sku.'_'.$title;
                
                $key = strtolower($sku."_".$title);
                if($redis->exists($key)) {
                    $keyValue = $redis->get($key);

                    // 不相等 换了标题
                    if($keyValue !== $titleValueHash) {
                        $oldKey = 'sku_hash_'.$keyValue;
                        $newKey = 'sku_hash_'.$titleValueHash;
                         
                        // 需要更新 或 删除
                        if($redis->exists($oldKey)) {
                            $oldKeyValue = $redis->get($oldKey);
                            // 只包含一个值
                            if(empty($oldKeyValue) || ($oldKeyValue === $hashHold)){
                                $redis->delete($oldKey);
                            } else {
                                $nowNewValue = str_replace([$hashHold.',',','.$hashHold],'',$oldKeyValue);
                                if(!empty($nowNewValue)) {
                                    $redis->set($oldKey,$nowNewValue);
                                } else {
                                    $redis->delete($oldKey);
                                }
                            }
                        }
                         
                        // 需要更新 或 添加
                        if($redis->exists($newKey)) {
                            $newKeyValue = $redis->get($newKey);
                            // 只包含一个值
                            if(empty($newKeyValue)){
                                $redis->set($newKey,$hashHold);
                            } else {
                                if($newKeyValue !== $hashHold){
                                    $newHashHold = str_replace([$hashHold.',', ','.$hashHold],'',$newKeyValue);
                                    if(!empty($newHashHold)) {
                                        $redis->set($newKey,$newHashHold.','.$hashHold);
                                    } else {
                                        $redis->set($newKey,$hashHold);
                                    }
                                }
                            }
                        } else {
                            $redis->set($newKey,$hashHold);
                        }
                        // 更新
                        $redis->set($key,$titleValueHash);
                    } else {
                        // 新旧Key对应的值相等,判断值对应的Key是否存在
                        $noExistKey = 'sku_hash_'.$keyValue;
                        if(!$redis->exists($noExistKey)){
                            $redis->set($noExistKey,$hashHold);
                        }
                    }
                } else {
                    $newKey = 'sku_hash_'.$titleValueHash;
                    if($redis->exists($newKey)) {
                        $newKeyValue = $redis->get($newKey);
                        // 只包含一个值
                        if(empty($newKeyValue)){
                            $redis->set($newKey,$hashHold);
                        } else {
                            if($newKeyValue !== $hashHold){
                                $newHashHold = str_replace([$hashHold.',', ','.$hashHold],'',$newKeyValue);
                                if(!empty($newHashHold)) {
                                    $redis->set($newKey,$newHashHold.','.$hashHold);
                                } else {
                                    $redis->set($newKey,$hashHold);
                                }
                            }
                        }
                    } else {
                        $redis->set($newKey,$hashHold);
                    }
                    
                    $redis->set($key,$titleValueHash);
                }
            } 
        }
         
    }
     
    public function getStringHash($str) {
        $titleValue = preg_split('/\s+/',trim($str));
        sort($titleValue);
        $titleValueHash = md5(implode('',$titleValue));
         
        return $titleValueHash;
    }
    
}