月度归档:2015年11月

MySQL大数据测试

批量插入数据

// Laravel框架代码
        $s = microtime(true);
        
        for($j=0;$j<100;$j++){
            $data = [];
            $ss = microtime(true);
            for($i=0;$i<5000;$i++){
                $d = [
                    'name' => str_random(64)." ".($j+1)*($i+1),
                    'cat1' => str_random(32),
                    'cat2' => str_random(32),
                    'cat3' => str_random(32),
                    'num1' => mt_rand(10000,99999),
                    'num2' => mt_rand(10000,99999),
                    'num3' => mt_rand(10000,99999).".01",
                    'num4' => mt_rand(10000,99999),
                    'num5' => mt_rand(10000,99999)
                ];
                $data[] = $d;
            }
            DB::table('list_test')->insert($data);
            $ee = microtime(true);
            
            Log::info((float)($ee-$ss));
        }
        
        $e = microtime(true);
        Log::info('Total:'.(float)($e - $s));
// 输出
[2015-11-30 13:56:10] local.INFO: 6.0573468208313  
[2015-11-30 13:56:15] local.INFO: 4.7142698764801  
[2015-11-30 13:56:20] local.INFO: 4.7502720355988  
[2015-11-30 13:56:24] local.INFO: 4.6482660770416  
[2015-11-30 13:56:29] local.INFO: 4.8062751293182  
[2015-11-30 13:56:34] local.INFO: 4.8232760429382  
[2015-11-30 13:56:39] local.INFO: 4.5732619762421  
[2015-11-30 13:56:43] local.INFO: 4.7092700004578  
[2015-11-30 13:56:48] local.INFO: 4.9792850017548  
[2015-11-30 13:56:53] local.INFO: 4.8652780056  
[2015-11-30 13:56:58] local.INFO: 4.9532828330994  
[2015-11-30 13:57:03] local.INFO: 4.9182820320129
.................................................
[2015-11-30 14:03:40] local.INFO: 4.8382771015167  
[2015-11-30 14:03:45] local.INFO: 4.7792727947235  
[2015-11-30 14:03:50] local.INFO: 4.9002799987793  
[2015-11-30 14:03:54] local.INFO: 4.7752728462219  
[2015-11-30 14:03:59] local.INFO: 4.8892788887024  
[2015-11-30 14:04:04] local.INFO: 4.7812731266022  
[2015-11-30 14:04:09] local.INFO: 4.9482831954956  
[2015-11-30 14:04:14] local.INFO: 5.0152869224548  
[2015-11-30 14:04:19] local.INFO: 5.0332868099213  
[2015-11-30 14:04:19] local.INFO: Total:494.97931194305  

每次生成5000条数据并批量插入,共插入50万条数。每次插入5000,耗时5秒左右,看起来很不满意,实际是时间应该耗费在产生数据的计算上,真正耗费在插入的事件,应该在毫秒甚至微秒级别。而且可以看到,耗费的时间,不会因为记录量上升而上升,都维持在5秒左右,这个本身就说明插入的时间非常微小。总耗时495秒,大概8分钟,MySQL的批量插入性能还可以可以肯定的。左右对比也不会差到什么地方。

如果以上的程序由于需要产生随机数据耗费了过多时间而无法准确评估,那么可以使用如下SQL来试试:

[SQL]INSERT INTO list_test_copy(name,cat1,cat2,cat3,num1,num2,num3,num4,num5) 
	SELECT name,cat1,cat2,cat3,num1,num2,num3,num4,num5 FROM list_test

受影响的行: 500000
时间: 31.640s

这个结果应该能比较准确的评估当前MySQL的性能,50w条记录批量插入,耗时31.64秒,平均每5000条耗时是0.3164秒,所有以上的5秒时间,有4秒多是用于产生随机数据的。

再来做一个全量查询:

[SQL]SELECT l.name, l.cat1, l.cat2, l.cat3,l.num1,l.num2,l.num3,l.num4,l.num5, 
	(l.num1 - ll.num1) AS diff1,
	(l.num2 - ll.num2) AS diff2,
	(l.num3 - ll.num3) AS diff3,
	(l.num4 - ll.num4) AS diff4,
	(l.num5 - ll.num5) AS diff5,
IF(ll.id IS NULL,1,0)
FROM list l LEFT JOIN list_ ll ON l.id = ll.id

受影响的行: 0
时间: 12.295s

这是一个50万的表,LEFT JOIN一个40万的表,共耗时12.295秒。以下来个大偏移,翻到49万条记录:

[SQL]SELECT l.name, l.cat1, l.cat2, l.cat3,l.num1,l.num2,l.num3,l.num4,l.num5, 
	(l.num1 - ll.num1) AS diff1,
	(l.num2 - ll.num2) AS diff2,
	(l.num3 - ll.num3) AS diff3,
	(l.num4 - ll.num4) AS diff4,
	(l.num5 - ll.num5) AS diff5,
IF(ll.id IS NULL,1,0)
FROM list l LEFT JOIN list_ ll ON l.id = ll.id LIMIT 100000

受影响的行: 0
时间: 2.395s

[SQL]SELECT l.name, l.cat1, l.cat2, l.cat3,l.num1,l.num2,l.num3,l.num4,l.num5, 
	(l.num1 - ll.num1) AS diff1,
	(l.num2 - ll.num2) AS diff2,
	(l.num3 - ll.num3) AS diff3,
	(l.num4 - ll.num4) AS diff4,
	(l.num5 - ll.num5) AS diff5,
IF(ll.id IS NULL,1,0)
FROM list l LEFT JOIN list_ ll ON l.id = ll.id LIMIT 200000

受影响的行: 0
时间: 3.592s

[SQL]SELECT l.name, l.cat1, l.cat2, l.cat3,l.num1,l.num2,l.num3,l.num4,l.num5, 
	(l.num1 - ll.num1) AS diff1,
	(l.num2 - ll.num2) AS diff2,
	(l.num3 - ll.num3) AS diff3,
	(l.num4 - ll.num4) AS diff4,
	(l.num5 - ll.num5) AS diff5,
IF(ll.id IS NULL,1,0)
FROM list l LEFT JOIN list_ ll ON l.id = ll.id LIMIT 300000

受影响的行: 0
时间: 4.735s

[SQL]SELECT l.name, l.cat1, l.cat2, l.cat3,l.num1,l.num2,l.num3,l.num4,l.num5, 
	(l.num1 - ll.num1) AS diff1,
	(l.num2 - ll.num2) AS diff2,
	(l.num3 - ll.num3) AS diff3,
	(l.num4 - ll.num4) AS diff4,
	(l.num5 - ll.num5) AS diff5,
IF(ll.id IS NULL,1,0)
FROM list l LEFT JOIN list_ ll ON l.id = ll.id LIMIT 400000

受影响的行: 0
时间: 7.568s

[SQL]SELECT l.name, l.cat1, l.cat2, l.cat3,l.num1,l.num2,l.num3,l.num4,l.num5, 
	(l.num1 - ll.num1) AS diff1,
	(l.num2 - ll.num2) AS diff2,
	(l.num3 - ll.num3) AS diff3,
	(l.num4 - ll.num4) AS diff4,
	(l.num5 - ll.num5) AS diff5,
IF(ll.id IS NULL,1,0)
FROM list l LEFT JOIN list_ ll ON l.id = ll.id LIMIT 490000

受影响的行: 0
时间: 11.576s

看起来不怎么满意?好吧,试试大偏移,但是取少来记录的情况吧:

[SQL]SELECT l.name, l.cat1, l.cat2, l.cat3,l.num1,l.num2,l.num3,l.num4,l.num5, 
	(l.num1 - ll.num1) AS diff1,
	(l.num2 - ll.num2) AS diff2,
	(l.num3 - ll.num3) AS diff3,
	(l.num4 - ll.num4) AS diff4,
	(l.num5 - ll.num5) AS diff5,
IF(ll.id IS NULL,1,0)
FROM list l LEFT JOIN list_ ll ON l.id = ll.id LIMIT 100000,100

受影响的行: 0
时间: 0.475s

[SQL]SELECT l.name, l.cat1, l.cat2, l.cat3,l.num1,l.num2,l.num3,l.num4,l.num5, 
	(l.num1 - ll.num1) AS diff1,
	(l.num2 - ll.num2) AS diff2,
	(l.num3 - ll.num3) AS diff3,
	(l.num4 - ll.num4) AS diff4,
	(l.num5 - ll.num5) AS diff5,
IF(ll.id IS NULL,1,0)
FROM list l LEFT JOIN list_ ll ON l.id = ll.id LIMIT 200000,100

受影响的行: 0
时间: 0.645s

[SQL]SELECT l.name, l.cat1, l.cat2, l.cat3,l.num1,l.num2,l.num3,l.num4,l.num5, 
	(l.num1 - ll.num1) AS diff1,
	(l.num2 - ll.num2) AS diff2,
	(l.num3 - ll.num3) AS diff3,
	(l.num4 - ll.num4) AS diff4,
	(l.num5 - ll.num5) AS diff5,
IF(ll.id IS NULL,1,0)
FROM list l LEFT JOIN list_ ll ON l.id = ll.id LIMIT 300000,100

受影响的行: 0
时间: 2.706s

[SQL]SELECT l.name, l.cat1, l.cat2, l.cat3,l.num1,l.num2,l.num3,l.num4,l.num5, 
	(l.num1 - ll.num1) AS diff1,
	(l.num2 - ll.num2) AS diff2,
	(l.num3 - ll.num3) AS diff3,
	(l.num4 - ll.num4) AS diff4,
	(l.num5 - ll.num5) AS diff5,
IF(ll.id IS NULL,1,0)
FROM list l LEFT JOIN list_ ll ON l.id = ll.id LIMIT 400000,100

受影响的行: 0
时间: 5.984s

可以看到20万前偏移量,秒级完成。(我这是Windows,同等条件下,在Linux下提升一个数量级不夸张)。偏移量到40w,花费6秒,就是定位这个偏移花费了怎么多时间,这个时间就不太满意了。不过翻页的话,给10万条让你翻,就搞不过来了。

下面试试档表的大偏移:

[SQL]SELECT * FROM list_copy_copy LIMIT 100000,200

受影响的行: 0
时间: 0.449s

[SQL]SELECT * FROM list_copy_copy LIMIT 200000,200

受影响的行: 0
时间: 0.534s

[SQL]SELECT * FROM list_copy_copy LIMIT 300000,200

受影响的行: 0
时间: 0.577s

[SQL]SELECT * FROM list_copy_copy LIMIT 400000,200

受影响的行: 0
时间: 0.680s

[SQL]SELECT * FROM list_copy_copy LIMIT 499000,200

受影响的行: 0
时间: 0.774s
[/sql]
这个单表的大数据偏移,还是比较满意的。

以上数据只是在我个人计算机上做的简单测试,而且是Windows环境。

用到的测试表结构:

CREATE TABLE `list` (
`id`  int(11) UNSIGNED NOT NULL AUTO_INCREMENT ,
`name`  varchar(128) CHARACTER SET utf8 COLLATE utf8_unicode_ci NULL DEFAULT NULL ,
`cat1`  varchar(64) CHARACTER SET utf8 COLLATE utf8_unicode_ci NULL DEFAULT NULL ,
`cat2`  varchar(64) CHARACTER SET utf8 COLLATE utf8_unicode_ci NULL DEFAULT NULL ,
`cat3`  varchar(255) CHARACTER SET utf8 COLLATE utf8_unicode_ci NULL DEFAULT NULL ,
`num1`  int(11) NULL DEFAULT NULL ,
`num2`  int(11) NULL DEFAULT NULL ,
`num3`  decimal(5,2) NULL DEFAULT NULL ,
`num4`  float(64,20) NULL DEFAULT NULL ,
`num5`  float(64,20) NULL DEFAULT NULL ,
PRIMARY KEY (`id`)
)
ENGINE=InnoDB
DEFAULT CHARACTER SET=utf8 COLLATE=utf8_unicode_ci
AUTO_INCREMENT=1
ROW_FORMAT=COMPACT
;

CREATE TABLE `list_copy` (
`id`  int(11) UNSIGNED NOT NULL AUTO_INCREMENT ,
`name`  varchar(128) CHARACTER SET utf8 COLLATE utf8_unicode_ci NULL DEFAULT NULL ,
`cat1`  varchar(64) CHARACTER SET utf8 COLLATE utf8_unicode_ci NULL DEFAULT NULL ,
`cat2`  varchar(64) CHARACTER SET utf8 COLLATE utf8_unicode_ci NULL DEFAULT NULL ,
`cat3`  varchar(255) CHARACTER SET utf8 COLLATE utf8_unicode_ci NULL DEFAULT NULL ,
`num1`  int(11) NULL DEFAULT NULL ,
`num2`  int(11) NULL DEFAULT NULL ,
`num3`  decimal(5,2) NULL DEFAULT NULL ,
`num4`  float(64,20) NULL DEFAULT NULL ,
`num5`  float(64,20) NULL DEFAULT NULL ,
`diff1`  int(11) NULL DEFAULT NULL ,
`diff2`  int(11) NULL DEFAULT NULL ,
`diff3`  decimal(10,2) NULL DEFAULT NULL ,
`diff4`  float(64,20) NULL DEFAULT NULL ,
`diff5`  float(64,20) NULL DEFAULT NULL ,
`is_new`  tinyint(2) NULL DEFAULT NULL ,
PRIMARY KEY (`id`)
)
ENGINE=InnoDB
DEFAULT CHARACTER SET=utf8 COLLATE=utf8_unicode_ci
AUTO_INCREMENT=1
ROW_FORMAT=COMPACT
;

Oauth2授权类 之 1688.com

<?php
/**
 * vfeelit@qq.com
 * 2015-11-30
 * 
 *  Api授权
 */
 
namespace Alibaba;

class Auth
{
    private $appKey;
    private $appSecret;
    private $redirectUrl;
    
    private $getTokenUrl = "https://gw.open.1688.com/openapi/http/1/system.oauth2/getToken";
    
    public function __construct($appKey, $appSecret, $redirectUrl='')
    {
        $this->appKey = $appKey;
        $this->appSecret = $appSecret;
        $this->redirectUrl = $redirectUrl;
    }

    // 一次性code换取access_token 和  refresh_token
    //array(
    //    "aliId" => "8888888888",      编号
    //    "resource_owner" => "xx",     登录名称
    //    "memberId" => "xx",           会员编号
    //    "expires_in" => "36000",      access_token有效时间,10小时
    //    "refresh_token" => "xx",
    //    "access_token" => "xx",
    //    "refresh_token_timeout" => "20121222222222+0800"
    //)
    public function getToken($code, $redirectUrl)
    {
        $params = array(
            'grant_type' => 'authorization_code',
            'need_refresh_token' => 'true',
            'client_id' => $this->appKey,
            'client_secret' => $this->appSecret,
            'redirect_uri' => empty($redirectUrl)?$this->redirectUrl:trim($redirectUrl),
            'code' => $code,
            '_aop_datePattern' => 'yyyy-MM-dd HH:mm:ss',
            '_aop_timeZone' => 'GMT+0800'
        );
        return $this->_token($params);
    }

    // refresh_token换取access_token
    public function refreshToken($refreshToken)
    {
        $params = array(
            'grant_type' => 'refresh_token',
            'client_id' => $this->appKey,
            'client_secret' => $this->appSecret,
            'refresh_token' => $refreshToken,
            //对授权无效
            '_aop_datePattern' => 'yyyy-MM-dd HH:mm:ss',
            '_aop_timeZone' => 'GMT+0800'
        );      
        return $this->_token($params);
    }
    
    // 获取token
    protected function _token($params)
    {
        $baseUrl = $this->getTokenUrl.'/'.$this->appKey;
        
        $result = $this->doRequest($baseUrl,http_build_query($params),true);
    
        if((int)$result['success'] > 0) {
            $data = json_decode($result['data'],true);
            if(!isset($data['access_token'])) {
                $result['success'] = 0;
                $result['err'] = '返回数据有误(没有access_token)';
                unset($result['data']);
            } else {
                $result['data'] = $data;
            }
        }
    
        return $result;
    }
    
    // 获取授权URL
    public function getAuthUrl($redirectUrl='') {
        $baseUrl = "http://auth.1688.com/auth/authorize.htm";
        
        $pramas = array (
            'client_id' => $this->appKey,
            'site' => 'china'
        );
        
        ///
        if(!empty($redirectUrl)) {
            $pramas['redirect_uri'] = $redirectUrl;
        } else {
            $pramas['redirect_uri'] = $this->redirectUrl;
        }
        if(empty($pramas['redirect_uri'])) {
            $pramas['redirect_uri'] = "http://localhost";
        }
        
        ///
        ksort ( $pramas );
        $signStr = '';
        foreach ( $pramas as $key => $val ) {
            $signStr .= $key . $val;
        }
        $sign = strtoupper ( bin2hex ( hash_hmac ( "sha1", $signStr, $this->appSecret, true ) ) );
        
        ///
        $pramas ['_aop_signature'] = $sign;
        
        return $baseUrl . '?' . http_build_query ( $pramas );
    }
    
    // 发起请求
    public function doRequest($url='', $data='', $post=false)
    {
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, trim($url));
        curl_setopt($ch, CURLOPT_HEADER, false);
        curl_setopt($ch, CURLOPT_TIMEOUT, 60);
        curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 60);
        if($post === false){
            curl_setopt($ch, CURLOPT_POST, false);
        }else{
            curl_setopt($ch, CURLOPT_POST, true);
            if(!empty($data)) {
                curl_setopt($ch, CURLOPT_POSTFIELDS,$data);
            }
        }
        if ((int)preg_match('/^HTTPS/i', $url) > 0) {
            curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
            curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
        }
    
        curl_setopt($ch,CURLOPT_RETURNTRANSFER,true);
        curl_setopt($ch,CURLOPT_FOLLOWLOCATION,true);
        curl_setopt($ch,CURLOPT_MAXREDIRS,10);
    
        curl_setopt($ch, CURLOPT_USERAGENT, "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:21.0) Gecko/20100101 Firefox/21.0");
        $result = curl_exec($ch);
        $errn = curl_errno($ch);
        curl_close($ch);
    
        $return = ['success' => 0,'err' => '发生错误'];
        if((int)$errn > 0) {
            $return['err'] = curl_strerror((int)$errn);
        }else{
            $return['success'] = 1;
            unset($return['err']);
            $return['data'] = $result;
        }
        return $return;
    }
}

Laravel Session详解

Laravel中Session的实现并没有使用PHP本身的Session扩展。而是自己实现了一套Session实现。好处是灵活,不好是不兼容使用PHP原生Session实现的应用。

Session是一个服务,当然是由SessionServiceProvider.php引入框架的:

<?php
namespace Illuminate\Session;
use Illuminate\Support\ServiceProvider;

class SessionServiceProvider extends ServiceProvider
{
    public function register()
    {
        $this->registerSessionManager();

        $this->registerSessionDriver();

        $this->app->singleton('Illuminate\Session\Middleware\StartSession');
    }

    protected function registerSessionManager()
    {
        $this->app->singleton('session', function ($app) {
            return new SessionManager($app);
        });
    }

    protected function registerSessionDriver()
    {
        $this->app->singleton('session.store', function ($app) {
            $manager = $app['session'];
            return $manager->driver();
        });
    }
}

注册了一个session,对应SessionManager实例,session.store是$manager->driver()返回的实例,它是Session存在的默认配置。关于Manager的实现都是老套路。最后关键的是绑定了一个Illuminate\Session\Middleware\StartSession。

在框架启动流程中:

namespace App\Http;
 
use Illuminate\Foundation\Http\Kernel as HttpKernel;
 
class Kernel extends HttpKernel {
    protected $middleware = [
        'Illuminate\Session\Middleware\StartSession',
    ];
 
}

这里的$middleware就是定义路由前需要调用的中间件,所以Session的逻辑就在这里被勾进来的。大概看一下这个中间件的hanlder方法:

    public function handle($request, Closure $next)
    {
        $this->sessionHandled = true;

        // If a session driver has been configured, we will need to start the session here
        // so that the data is ready for an application. Note that the Laravel sessions
        // do not make use of PHP "native" sessions in any way since they are crappy.
        if ($this->sessionConfigured()) {
            $session = $this->startSession($request);

            $request->setSession($session);
        }

        $response = $next($request);

        // Again, if the session has been configured we will need to close out the session
        // so that the attributes may be persisted to some storage medium. We will also
        // add the session identifier cookie to the application response headers now.
        if ($this->sessionConfigured()) {
            $this->storeCurrentUrl($request, $session);

            $this->collectGarbage($session);

            $this->addCookieToResponse($response, $session);
        }

        return $response;
    }

首先启动Session,然后调用$request->setSession($session),这样$request->session()就可用了。接下来存储当前URL,垃圾回收,添加会话Cookie到响应。从这里来看,每次响应都应该有一个setCookie的响应头。

简单来说,每次都会从客户端cookie中取回Session ID,由于这个Session ID由Cookie传输,其加解密工作由Cookie完成,如果接收不到这个ID就启动一个新的ID。然后使用这个ID,通过Handler类提供的方法,把存在的数据load回来(没有就是空),产生会话token(如果没有的话),标记会话启动。

不同的Driver实际上就是对应应用了不同Hander的Store对象(以上的描述的过程就是在Store中完成的),Store应用Hanlder方法取回和保存数据,所以如果要实现把Session数据保存到其它介质,只要实现Hanlder接口即可。另外,保存的Session数据是可以做加密保存的,这个取决于conf/session.php中的配置。

对Session的操作的方法,都是由Store对象提供的,可以通过$request->session()获取到SessionManager,也可以使用全局的方法session(),或者app(“session”)都是可以的,不过需要之一,这里返回的都是一个SessionManager实例,然而可以通过它间接调用默认Driver(Store对象)的方法,app(‘session.store’)才是真正对应Store对象。

用法:

$value = $request->session()->get('key', 'default');

$value = $request->session()->get('key', function() {
    return 'default';
});

$data = $request->session()->all();

#使用session()
Route::get('home', function () {
    // 从session中获取数据...
    $value = session('key');

    // 存储数据到session...
    session(['key' => 'value']);
});

if ($request->session()->has('users')) {
    //
}

$request->session()->put('key', 'value');

$request->session()->push('user.teams', 'developers');

$value = $request->session()->pull('key', 'default');

$request->session()->forget('key');
$request->session()->flush();

$request->session()->regenerate();

$request->session()->flash('status', 'Task was successful!');

$request->session()->reflash();
$request->session()->keep(['username', 'email']);

另外一个比较坑的地方,估计就是会话数据的存储了,从源代码来看,Session数据视乎是在发送前些前才一次性把会话数据同步到存储介质,那么就是说如果中间异常退出,那么之前保存的数据就无法持久化。

Laravel 帮助函数

$vendorDir . '/laravel/framework/src/Illuminate/Foundation/helpers.php',
$vendorDir . '/laravel/framework/src/Illuminate/Support/helpers.php',

Laravel框架会全局include这两个文件,都是一些有用的Helper函数。

数组:

$array = array_add(['name' => 'Desk'], 'price', 100);
// ['name' => 'Desk', 'price' => 100]

list($keys, $values) = array_divide(['name' => 'Desk']);
// $keys: ['name']
// $values: ['Desk']

#多维数组变成点引用
$array = array_dot(['foo' => ['bar' => 'baz']]);
// ['foo.bar' => 'baz'];

#从数组中移除给定键值对
$array = ['name' => 'Desk', 'price' => 100];
$array = array_except($array, ['price']);
// ['name' => 'Desk']

#返回满足条件的第一个元素,第三参数可以指定为默认值
$array = [100, 200, 300];
$value = array_first($array, function ($key, $value) {
    return $value >= 150;});
// 200

#多维数组装换成一维
$array = ['name' => 'Joe', 'languages' => ['PHP', 'Ruby']];
$array = array_flatten($array);
// ['Joe', 'PHP', 'Ruby'];

$array = ['products' => ['desk' => ['price' => 100]]];
array_forget($array, 'products.desk');
// ['products' => []]

#点语法取值
$array = ['products' => ['desk' => ['price' => 100]]];
$value = array_get($array, 'products.desk');
// ['price' => 100]

$array = ['name' => 'Desk', 'price' => 100, 'orders' => 10];
$array = array_only($array, ['name', 'price']);
// ['name' => 'Desk', 'price' => 100]

#摘取值
$array = [
    ['developer' => ['name' => 'Taylor']],
    ['developer' => ['name' => 'Abigail']]];
$array = array_pluck($array, 'developer.name');
// ['Taylor', 'Abigail'];

#拉
$array = ['name' => 'Desk', 'price' => 100];
$name = array_pull($array, 'name');
// $name: Desk
// $array: ['price' => 100]

#点语法设置值
$array = ['products' => ['desk' => ['price' => 100]]];
array_set($array, 'products.desk.price', 200);
// ['products' => ['desk' => ['price' => 200]]]

#排序
$array = [
    ['name' => 'Desk'],
    ['name' => 'Chair'],
];
$array = array_values(array_sort($array, function ($value) {
    return $value['name'];
}));
/*
    [
        ['name' => 'Chair'],
        ['name' => 'Desk'],
    ]
*/

#排序
$array = [100, '200', 300, '400', 500];
$array = array_where($array, function ($key, $value) {
    return is_string($value);
});
// [1 => 200, 3 => 400]

#返回第一个元素
$array = [100, 200, 300];
$first = head($array);
// 100

#返回最后一个元素
$array = [100, 200, 300];
$last = last($array);
// 300

数组扩展里面,点语法是不错的。这些函数都是Support下的Arr类的再次封装。

路径函数:

#app目录绝对路径
$path = app_path();

#根目录绝对路径
$path = base_path();

#
$path = config_path();

#
$path = database_path();

#
$path = public_path();

#
$path = storage_path();

字符串:

$camel = camel_case('foo_bar');
// fooBar

$class = class_basename('Foo\Bar\Baz');
// Baz

#在给定字符串上运行htmlentities,blade模板中的{{}}实际会转换成echo e()
echo e('<html>foo</html>');
// &lt;html&gt;foo&lt;/html&gt;

$value = ends_with('This is my name', 'name');
// true

$snake = snake_case('fooBar');
// foo_bar

$value = str_limit('The PHP framework for web artisans.', 7);
// The PHP...

$value = starts_with('This is my name', 'This');
// true

$value = str_contains('This is my name', 'my');
// true

$string = str_finish('this/string', '/');
// this/string/

$value = str_is('foo*', 'foobar');
// true
$value = str_is('baz*', 'foobar');
// false

#单词复数,只对英文
$plural = str_plural('car');
// cars
$plural = str_plural('child');
// children

#指定长度,随机字符串
$string = str_random(40);

#单词复数转为单数,只对英文
$singular = str_singular('cars');
// car

#将给定字符串生成URL友好的格式:
$title = str_slug("Laravel 5 Framework", "-");
// laravel-5-framework

#
$value = studly_case('foo_bar');
// FooBar

echo trans('validation.required'):

函数start_with() end_with() str_is() str_random()的实现很不错。 这些函数都是Support下的Str类的再次封装。

URL函数

$url = action('HomeController@getIndex');
$url = action('UserController@profile', ['id' => 1]);

$url = asset('img/photo.jpg');

echo secure_asset('foo/bar.zip', $title, $attributes = []);

$url = route('routeName');

#绝对路径
echo url('user/profile');
echo url('user/profile', [1]);

其它函数

#Auth
$user = auth()->user();

return back();

#Hash   Hash::make()
$password = bcrypt('my-secret-password');

$value = config('app.timezone');$value = config('app.timezone', $default);
config(['app.debug' => true]); #设置值

{!! csrf_field() !!}
$token = csrf_token();

dd($value);

elixir($file);

$env = env('APP_ENV');
// Return a default value if the variable doesn't exist...
$env = env('APP_ENV', 'production');

event(new UserRegistered($user));

$user = factory('App\User')->make();

#产生<input type="hidden" name="_method" value="delete" />
<form method="POST">
    {!! method_field('delete') !!}</form>

#获取一次性存放在session中的值:
$value = old('value');

return redirect('/home');

return response('Hello World', 200, $headers);return response()->json(['foo' => 'bar'], 200, $headers)

$value = value(function() { return 'bar'; });

return view('auth.login');

$value = with(new Foo)->work();

PHP 图片处理库 intervention/image

https://packagist.org/packages/intervention/image
https://github.com/Intervention/image
http://image.intervention.io/

安装使用:

php composer.phar require intervention/image

或者直接去下载源代码,只要保证能自动装载即可。

例子:

use Intervention\Image\ImageManagerStatic as Image;

//Image::configure(array('driver' => 'imagick'));
$image = Image::make("D:/3.jpg");
$warter = Image::make("D:/w/2.png");

$image->insert($warter,'top-left',15,15);
        
$image->save("D:/o.jpg");

合并两张图片就是如此的简单。make方法可以根据传入的第一参数类型,自动的返回一个Image对象,这是一个强大的工厂方法,当然,默认使用的GD驱动,也可以使用Image::configure(array(‘driver’ => ‘imagick’))来切换到imagick,不过提供的API是一致的。

以上的ImageManagerStatic提供了一个静态用法,实际上它是Intervention\Image\ImageManager的封装,所以以上例子可以改装为:

use Intervention\Image\ImageManager;

$im = new ImageManager();
$im->configure(array('driver' => 'imagick'));

$image = $im->make("D:/3.jpg");
$warter = $im->make("D:/w/2.png");

$image->insert($warter,'top-left',15,15);   
$image->save("D:/o.jpg");

看起来使用静态的方式还是简便一点(这也是其可以存在的理由)。

这个库的基本封装流程:ImageManager管理不同的driver(GD 和 imagick),每个driver都有一个decoder和encoder,decoder用来识别输入,比如直接输入文件路径,base64编码字符串,二进制代码等,具体工作是由一个init方法(在抽象类中)完成的,它返回具体Image对象,这个方法是一个工厂方法;decoder处理输出,比如要正确处理JPG或PNG输出,就需要它来识别并处理,具体来说就是process方法,根据不同fromat,调用不同的处理方法,处理的结果保存在公共的result属性中,Image中的save()方法就是间接调用了process方法。一般,如果要保存一个图片,就调用save()方法,如果需要返回处理的字符串,就调用encode()方法,比如要放回用于URL展示的图片字符流,就用$image->encode(‘data-url’),实际上这个字符串是保存到$image的$encoded字段中的,而它实现了__toString()方法:

    public function __toString()
    {
        return $this->encoded;
    }

比如如下例子:

$image = Image::make("D:/3.jpg");
echo $image->encode('data-url');

data:image/jpeg;base64,/9j/4AAQSkZJRg*************************

不得不说,这确实很便利。这个库对于图片识别,处理,输出识别提供了简单的实现,不需要去直接使用稍微“丑陋”的PHP函数。

另外,这个包提供了适配于Laravel的ServiceProvider,这个个人感觉就可以忽略了。要使用时,直接use一下就可以开始使用了。

使用实例:

$image = Image::make($file);

// 调整到具体尺寸,会变形 
$image->resize($toWidth, $toHeight);

// 调整高度并维持比例(等比例拉伸,aspectRatio方法维持等比例)
$image->resize(null, $toHeight, function ($constraint) {
      $constraint->aspectRatio();
});
// 调整宽度并维持比例(等比例拉伸,aspectRatio方法维持等比例)
$image->resize($toWidth, null, function ($constraint) {
      $constraint->aspectRatio();
});

等比例拉伸图片,以上方法对应提供了:

$img = Image::make($file)->heighten($toHeight);

$img = Image::make($file)->widen($toWidth);

按照给定尺寸的比例拉伸图片,然后抽取给定尺寸的块:

$toWidth = 385;
$toHeight = 285;

$file = '/path/to.jpg';

$image = Image::make($file);

$width = $image->width();
$height = $image->height();

$radio1 = $width / $height;
$radio2 = $toWidth / $toHeight;
if ($radio1 > $radio2) {
    $image->resize(null, $toHeight, function ($constraint) {
        $constraint->aspectRatio();
    });
} else {
    $image->resize($toWidth, null, function ($constraint) {
        $constraint->aspectRatio();
    });
}

$image->crop($toWidth, $toHeight, 0, 0);

这个拉伸或输小的比率是根据给定的宽度和高度决定的。原始的图片拉伸到两条边都大于等于给定的宽度或高度。这种情况也对应有一个方法:

$toWidth = 385;
$toHeight = 285;

$file = '/path/to.jpg';

$image = Image::make($file);

$image->fit(toWidth, $toHeight);

注意fit方法第三参数是一个闭包回调,可以控制拉伸的方式,一般不需要设置,第四参数是控制crop的位置,默认是居中

以下是方法列表:

/**
 * @method \Intervention\Image\Image backup(string $name = 'default')                                                                                                     Backups current image state as fallback for reset method under an optional name. Overwrites older state on every call, unless a different name is passed.
 * @method \Intervention\Image\Image blur(integer $amount = 1)                                                                                                            Apply a gaussian blur filter with a optional amount on the current image. Use values between 0 and 100.
 * @method \Intervention\Image\Image brightness(integer $level)                                                                                                           Changes the brightness of the current image by the given level. Use values between -100 for min. brightness. 0 for no change and +100 for max. brightness.
 * @method \Intervention\Image\Image cache(\Closure $callback, integer $lifetime = null, boolean $returnObj = false)                                                              Method to create a new cached image instance from a Closure callback. Pass a lifetime in minutes for the callback and decide whether you want to get an Intervention Image instance as return value or just receive the image stream.
 * @method \Intervention\Image\Image canvas(integer $width, integer $height, mixed $bgcolor = null)                                                                       Factory method to create a new empty image instance with given width and height. You can define a background-color optionally. By default the canvas background is transparent.
 * @method \Intervention\Image\Image circle(integer $radius, integer $x, integer $y, \Closure $callback = null)                                                           Draw a circle at given x, y, coordinates with given radius. You can define the appearance of the circle by an optional closure callback.
 * @method \Intervention\Image\Image colorize(integer $red, integer $green, integer $blue)                                                                                Change the RGB color values of the current image on the given channels red, green and blue. The input values are normalized so you have to include parameters from 100 for maximum color value. 0 for no change and -100 to take out all the certain color on the image.
 * @method \Intervention\Image\Image contrast(integer $level)                                                                                                             Changes the contrast of the current image by the given level. Use values between -100 for min. contrast 0 for no change and +100 for max. contrast.
 * @method \Intervention\Image\Image crop(integer $width, integer $height, integer $x = null, integer $y = null)                                                          Cut out a rectangular part of the current image with given width and height. Define optional x,y coordinates to move the top-left corner of the cutout to a certain position.
 * @method void                      destroy()                                                                                                                            Frees memory associated with the current image instance before the PHP script ends. Normally resources are destroyed automatically after the script is finished.
 * @method \Intervention\Image\Image ellipse(integer $width, integer $height, integer $x, integer $y, \Closure $callback = null)                                          Draw a colored ellipse at given x, y, coordinates. You can define width and height and set the appearance of the circle by an optional closure callback.
 * @method mixed                     exif(string $key = null)                                                                                                             Read Exif meta data from current image.
 * @method mixed                     iptc(string $key = null)                                                                                                             Read Iptc meta data from current image.
 * @method \Intervention\Image\Image fill(mixed $filling, integer $x = null, integer $y = null)                                                                           Fill current image with given color or another image used as tile for filling. Pass optional x, y coordinates to start at a certain point.
 * @method \Intervention\Image\Image flip(mixed $mode = 'h')                                                                                                              Mirror the current image horizontally or vertically by specifying the mode.
 * @method \Intervention\Image\Image fit(integer $width, integer $height = null, \Closure $callback = null, string $position = 'center')                                  Combine cropping and resizing to format image in a smart way. The method will find the best fitting aspect ratio of your given width and height on the current image automatically, cut it out and resize it to the given dimension. You may pass an optional Closure callback as third parameter, to prevent possible upsizing and a custom position of the cutout as fourth parameter.
 * @method \Intervention\Image\Image gamma(float $correction)                                                                                                             Performs a gamma correction operation on the current image.
 * @method \Intervention\Image\Image greyscale()                                                                                                                          Turns image into a greyscale version.
 * @method \Intervention\Image\Image heighten(integer $height, \Closure $callback = null)                                                                                 Resizes the current image to new height, constraining aspect ratio. Pass an optional Closure callback as third parameter, to apply additional constraints like preventing possible upsizing.
 * @method \Intervention\Image\Image insert(mixed $source, string $position = 'top-left', integer $x = 0, integer $y = 0)                                                 Paste a given image source over the current image with an optional position and a offset coordinate. This method can be used to apply another image as watermark because the transparency values are maintained.
 * @method \Intervention\Image\Image interlace(boolean $interlace = true)                                                                                                 Determine whether an image should be encoded in interlaced or standard mode by toggling interlace mode with a boolean parameter. If an JPEG image is set interlaced the image will be processed as a progressive JPEG.
 * @method \Intervention\Image\Image invert()                                                                                                                             Reverses all colors of the current image.
 * @method \Intervention\Image\Image limitColors(integer $count, mixed $matte = null)                                                                                     Method converts the existing colors of the current image into a color table with a given maximum count of colors. The function preserves as much alpha channel information as possible and blends transarent pixels against a optional matte color.
 * @method \Intervention\Image\Image line(integer $x1, integer $y1, integer $x2, integer $y2, \Closure $callback = null)                                                  Draw a line from x,y point 1 to x,y point 2 on current image. Define color and/or width of line in an optional Closure callback.
 * @method \Intervention\Image\Image make(mixed $source)                                                                                                                  Universal factory method to create a new image instance from source, which can be a filepath, a GD image resource, an Imagick object or a binary image data.
 * @method \Intervention\Image\Image mask(mixed $source, boolean $mask_with_alpha)                                                                                        Apply a given image source as alpha mask to the current image to change current opacity. Mask will be resized to the current image size. By default a greyscale version of the mask is converted to alpha values, but you can set mask_with_alpha to apply the actual alpha channel. Any transparency values of the current image will be maintained.
 * @method \Intervention\Image\Image opacity(integer $transparency)                                                                                                       Set the opacity in percent of the current image ranging from 100% for opaque and 0% for full transparency.
 * @method \Intervention\Image\Image orientate()                                                                                                                          This method reads the EXIF image profile setting 'Orientation' and performs a rotation on the image to display the image correctly.
 * @method mixed                     pickColor(integer $x, integer $y, string $format = 'array')                                                                          Pick a color at point x, y out of current image and return in optional given format.
 * @method \Intervention\Image\Image pixel(mixed $color, integer $x, integer $y)                                                                                          Draw a single pixel in given color on x, y position.
 * @method \Intervention\Image\Image pixelate(integer $size)                                                                                                              Applies a pixelation effect to the current image with a given size of pixels.
 * @method \Intervention\Image\Image polygon(array $points, \Closure $callback = null)                                                                                    Draw a colored polygon with given points. You can define the appearance of the polygon by an optional closure callback.
 * @method \Intervention\Image\Image rectangle(integer $x1, integer $y1, integer $x2, integer $y2, \Closure $callback = null)                                             Draw a colored rectangle on current image with top-left corner on x,y point 1 and bottom-right corner at x,y point 2. Define the overall appearance of the shape by passing a Closure callback as an optional parameter.
 * @method \Intervention\Image\Image reset(string $name = 'default')                                                                                                      Resets all of the modifications to a state saved previously by backup under an optional name.
 * @method \Intervention\Image\Image resize(integer $width, integer $height, \Closure $callback = null)                                                                   Resizes current image based on given width and/or height. To contraint the resize command, pass an optional Closure callback as third parameter.
 * @method \Intervention\Image\Image resizeCanvas(integer $width, integer $height, string $anchor = 'center', boolean $relative = false, mixed $bgcolor = '#000000')      Resize the boundaries of the current image to given width and height. An anchor can be defined to determine from what point of the image the resizing is going to happen. Set the mode to relative to add or subtract the given width or height to the actual image dimensions. You can also pass a background color for the emerging area of the image.
 * @method mixed                     response(string $format = null, integer $quality = 90)                                                                               Sends HTTP response with current image in given format and quality.
 * @method \Intervention\Image\Image rotate(float $angle, string $bgcolor = '#000000')                                                                                    Rotate the current image counter-clockwise by a given angle. Optionally define a background color for the uncovered zone after the rotation.
 * @method \Intervention\Image\Image sharpen(integer $amount = 10)                                                                                                        Sharpen current image with an optional amount. Use values between 0 and 100.
 * @method \Intervention\Image\Image text(string $text, integer $x = 0, integer $y = 0, \Closure $callback = null)                                                        Write a text string to the current image at an optional x,y basepoint position. You can define more details like font-size, font-file and alignment via a callback as the fourth parameter.
 * @method \Intervention\Image\Image trim(string $base = 'top-left', array $away = array('top', 'bottom', 'left', 'right'), integer $tolerance = 0, integer $feather = 0) Trim away image space in given color. Define an optional base to pick a color at a certain position and borders that should be trimmed away. You can also set an optional tolerance level, to trim similar colors and add a feathering border around the trimed image.
 * @method \Intervention\Image\Image widen(integer $width, \Closure $callback = null)                                                                                     Resizes the current image to new width, constraining aspect ratio. Pass an optional Closure callback as third parameter, to apply additional constraints like preventing possible upsizing.
 * @method StreamInterface           stream(string $format = null, integer $quality = 90)                                                                                 Build PSR-7 compatible StreamInterface with current image in given format and quality.
 * @method ResponseInterface         psrResponse(string $format = null, integer $quality = 90)                                                                            Build PSR-7 compatible ResponseInterface with current image in given format and quality.
 */

Laravel 加解密详解

Laravel 中的加解密是PHP原生加解密扩展(OpenSSL,mcrypt)的简单封装。一个加解密类需要实现Illuminate\Contracts\Encryption\Encrypter接口:

namespace Illuminate\Contracts\Encryption;

interface Encrypter
{
    /**
     * Encrypt the given value.
     *
     * @param  string  $value
     * @return string
     */
    public function encrypt($value);

    /**
     * Decrypt the given value.
     *
     * @param  string  $payload
     * @return string
     */
    public function decrypt($payload);
}

对应加密 和 解密方法。除此,Laravel对加解密类提供了一个基类(抽象类,没有实现Illuminate\Contracts\Encryption\Encrypter接口,意味着最终的类可以继承这个类,并实现这个接口),这个类就不粘贴了,基本上,继承这个类的最终实现,在加密中携带解密需要的参数:

$data['iv'] = 
$data['value'] = 
$data['mac'] = 

这里的value就是密文,mac就是iv与value的签名。所以在解密时,需要验证这借个数据是存在的,并且通过mac验证密文的完整性。查看Illuminate\Encryption\Encrypter的encrypt方法的实现:

    public function encrypt($value)
    {
        $iv = Str::randomBytes($this->getIvSize());

        $value = openssl_encrypt(serialize($value), $this->cipher, $this->key, 0, $iv);

        if ($value === false) {
            throw new EncryptException('Could not encrypt the data.');
        }

        // Once we have the encrypted value we will go ahead base64_encode the input
        // vector and create the MAC for the encrypted value so we can verify its
        // authenticity. Then, we'll JSON encode the data in a "payload" array.
        $mac = $this->hash($iv = base64_encode($iv), $value);

        return base64_encode(json_encode(compact('iv', 'value', 'mac')));
    }

先随机获取iv,然后序列化明文,传递明文、加密算法、key、和$iv, 加密得到密文,然后组装一个有iv 密文 mac组成的数组,先json_encode后base64_encode。

最终,需要使用就仅仅两个方法,encrypt和decrypt(也是接口规定的两个方法)。不过这里需要注意,conf/app.php中的配置:

'key' => env('APP_KEY', 'SomeRandomString'),
'cipher' => 'AES-256-CBC',

如果需要使用AES-256-CBC,那么key必须是32个8bit的字符,AES-128-CBC对应的key是16位,因为这个方法限制:

    //Illuminate\Encryption\Encrypter
    public static function supported($key, $cipher)
    {
        $length = mb_strlen($key, '8bit');

        return ($cipher === 'AES-128-CBC' && $length === 16) || ($cipher === 'AES-256-CBC' && $length === 32);
    }

之所以有这个限制,那是因为这个加解密类使用open_ssl的原因。

然而,如果不符合这个要求,就会去实例化一个Illuminate\Encryption\McryptEncrypter(用到PHP的mcrypt扩展函数),它没有限制key的长度,原理上也类似,都是对称加密算法的实现。不过很明显,第一种使用open_ssl的方式是其推荐的方法。

以上所说的检测流程,是Illuminate\Encryption\EncryptionServiceProvider中提供的:

    public function register()
    {
        $this->app->singleton('encrypter', function ($app) {
            $config = $app->make('config')->get('app');

            $key = $config['key'];

            $cipher = $config['cipher'];

            if (Encrypter::supported($key, $cipher)) {
                return new Encrypter($key, $cipher);
            } elseif (McryptEncrypter::supported($key, $cipher)) {
                return new McryptEncrypter($key, $cipher);
            } else {
                throw new RuntimeException('No supported encrypter found. The cipher and / or key length are invalid.');
            }
        });
    }

全局用一个Facade对应了这个实例 — Crypt。系统内用到加解密的地方有cookie的数据加密,包括会话ID的加密(Laravel没有使用PHP的会话扩展)。

另外,需要知道,加解密有分对称和非对称加解密,这里提供的方法是对称加解密,不过是对称还是非对称,都是可逆的,而那些密码哈希,一般都是指单向不可逆的算法,跟这里说的加解密是两会事,密码哈希类,Lravel中提供了Hash Facade来操作这个(全局的bcrypt()方法)。

Laravel这里仅仅提供了对称加解密的实现,而实际可能用到非对称的加解密,所以只能说是它提供了够用的实现。类似Zend/Crypt组件就提供了相对比较丰富的实现。

Laravel Hash详解

Laravel 中的Hash封装原生的PHP函数。

Hash类对应的接口:

<?php
namespace Illuminate\Contracts\Hashing;

interface Hasher
{
    public function make($value, array $options = []);
    public function check($value, $hashedValue, array $options = []);
    public function needsRehash($hashedValue, array $options = []);
}

三个方法,make()产生密码对应的哈希,check()检查密码与哈希是否匹配,needsRehash()检查哈希值是否需要更新。

具体的实现在:

<?php

namespace Illuminate\Hashing;

use RuntimeException;
use Illuminate\Contracts\Hashing\Hasher as HasherContract;

class BcryptHasher implements HasherContract
{
    protected $rounds = 10;

    public function make($value, array $options = [])
    {
        $cost = isset($options['rounds']) ? $options['rounds'] : $this->rounds;

        $hash = password_hash($value, PASSWORD_BCRYPT, ['cost' => $cost]);

        if ($hash === false) {
            throw new RuntimeException('Bcrypt hashing not supported.');
        }

        return $hash;
    }

    public function check($value, $hashedValue, array $options = [])
    {
        if (strlen($hashedValue) === 0) {
            return false;
        }

        return password_verify($value, $hashedValue);
    }

    public function needsRehash($hashedValue, array $options = [])
    {
        $cost = isset($options['rounds']) ? $options['rounds'] : $this->rounds;

        return password_needs_rehash($hashedValue, PASSWORD_BCRYPT, ['cost' => $cost]);
    }

    public function setRounds($rounds)
    {
        $this->rounds = (int) $rounds;

        return $this;
    }
}

这里的make()方法实现,实际是password_hash()的封装,这个函数参考http://php.net/password_hash。它有两个选项,一个是salt(PHP7中过时),另一个是cost,cost大概意思应该环绕几圈(递归环绕几圈,基本就无解了)。Laravel默认使用这个选项并且默认值为10。它也提供了setRounds()让我们可以随意设置cost。

对应的服务器提供者在框架中的作为一个延时服务:

<?php

namespace Illuminate\Hashing;

use Illuminate\Support\ServiceProvider;

class HashServiceProvider extends ServiceProvider
{
    protected $defer = true;

    public function register()
    {
        $this->app->singleton('hash', function () { return new BcryptHasher; });
    }

    public function provides()
    {
        return ['hash'];
    }
}

简单的返回BcryptHasher实例而已。有一个Facade对应了这个实例:Hash。可以直接使用Hash,也可以使用app(‘hash’)。

另外,Laravel全局提供了一个bcrypt()函数来对应Hash::make()或app(‘hash’)->make():

if (! function_exists('bcrypt')) {
    function bcrypt($value, $options = [])
    {
        return app('hash')->make($value, $options);
    }
}

所以,如果要对密码产生哈希,简单使用bcrypt()即可。

举例,对字符串admin产生哈希:

$pwd = 'admin';
echo bcrypt($pwd)."\n";
echo Hash::make($pwd)."\n";
echo app('hash')->make($pwd)."\n";

//输出
$2y$10$FZ4UIC8XRFOzO5G7oo1QD.ZVitC0ajC0w/B0qhIeC8XdSeG6jgWBi
$2y$10$cnmloAFVLuEH4UmoUtVXYOBt2mXjBkp4oeTg/iQbBP85w6aBd/Eru
$2y$10$zDjHITj2FjF9WMLmNWCXq.S2EtO5fxVQKgK0/WdSM/iJiI3adT/Hm

三个输出的字符串,是admin的哈希,可以看到每次输出是不同的。

ORM – illuminate/database组件

{
    "config": {
        "preferred-install": "dist",
        "secure-http": false
    },
    "repositories": [
        {"type": "composer", "url": "http://packagist.phpcomposer.com"},
        {"packagist": false}
    ],
    "require": {
        "illuminate/database": "^5.1"
    }
}

php composer.phar install
# OR
php composer.phar require illuminate/database

使用:

<?php
if(file_exists('vendor/autoload.php')){
    $loader = include '/vendor/autoload.php';
}else{
    exit("Autoload Failed. ");
}

// Capsule跟Laravel的DB Facade用法一样
use Illuminate\Database\Capsule\Manager as Capsule;

$capsule = new \Illuminate\Database\Capsule\Manager;
// 添加链接
// 注意:collation务必正确指定,不会根据charset自定设置
$capsule->addConnection([
    'driver'    => 'mysql',
    'host'      => 'localhost',
    'database'  => 'test',
    'username'  => 'root',
    'password'  => '',
    'charset'   => 'utf8',
    'collation' => 'utf8_general_ci',
    'prefix'    => '',
]);
// 这样就可以使用Capsule::xxx这样的方法
$capsule->setAsGlobal();
// 启动ORM支持
$capsule->bootEloquent();
//
$demo = Capsule::table('datatables_demo')->where('id', '>', 10)->take(5)->get();
//print_r($demo);

// ORM测试
class User extends \Illuminate\Database\Eloquent\Model {
	protected $table = 'user';
}
$users = User::where('id', '>', 0)->get();
print_r($users->toArray());

想知道$capsule->setAsGlobal();干了啥,看如下例子:

<?php
if(file_exists('vendor/autoload.php')){
    $loader = include '/vendor/autoload.php';
}else{
    exit("Autoload Failed. ");
}

// Capsule跟Laravel的DB Facade用法一样
use Illuminate\Database\Capsule\Manager as DB;

$db = new DB;
// 添加链接
// 注意:collation务必正确指定,不会根据charset自定设置
$db->addConnection([
    'driver'    => 'mysql',
    'host'      => 'localhost',
    'database'  => 'test',
    'username'  => 'root',
    'password'  => '',
    'charset'   => 'utf8',
    'collation' => 'utf8_general_ci',
    'prefix'    => '',
]);
// 这样就可以使用Capsule::xxx这样的方法
$db->setAsGlobal();
// 启动ORM支持
$db->bootEloquent();
//
$demo = DB::table('datatables_demo')->where('id', '>', 10)->take(5)->get();
//print_r($demo);

这样就跟Laravel中的DB Facade一致了。不过这里的DB并不是一个Facade。实际上,use Illuminate\Database\Capsule\Manager as DB是把Illuminate\Database\Capsule\Manager类设置了别名叫DB,自然,能通过Illuminate\Database\Capsule\Manager类访问的静态方法,也可以通过DB来方法,比如Illuminate\Database\Capsule\Manager有table()方法,那么DB::table()自然是可以的。

Illuminate\Database\Capsule\Manager类中:

    public static function __callStatic($method, $parameters)
    {
        return call_user_func_array([static::connection(), $method], $parameters);
    }

    public static function connection($connection = null)
    {
        return static::$instance->getConnection($connection);
    }

当访问类中不存在的静态方法时,_callStatic()方法会被触发,而它实际是调用数据库链接的静态方法,而数据库链接是通过调用static::$instance实例的getConnection()获取的,setAsGlobal()方法就是把当前实例赋值给静态变量static::$instance,这样就实现了在静态方法中调用实例方法。

下面跟踪一下DB::table(),大体看下流程:

// 调用table()方法,实际调用了static::$instance->connection($connection)的table()方法
    public static function table($table, $connection = null)
    {
        return static::$instance->connection($connection)->table($table);
    }
// static::$instance->connection($connection)有调用了getConnection
    public static function connection($connection = null)
    {
        return static::$instance->getConnection($connection);
    }
// 调用了$this->manager的connection($name)方法
// $this->manager是一个Illuminate\Database\DatabaseManager实例,在Illuminate\Database\Capsule\Manager构造是被创建
// 这个实例就是static::$instance
    public function getConnection($name = null)
    {
        return $this->manager->connection($name);
    }

// $this->manager->connection($name)方法中调用makeConnection()来更加不同的配置生成具体的链接
// 这个链接生成的过程会调用具体的Connector建立连接器
// Manager的connections数据保存的是已经链接上数据库的具体链接对象(下次就可以直接使用)
// 所以,大部分操作函数都是直接或间接对连接对象的方法的调用
    public function connection($name = null)
    {
        list($name, $type) = $this->parseConnectionName($name);

        // If we haven't created this connection, we'll create it based on the config
        // provided in the application. Once we've created the connections we will
        // set the "fetch mode" for PDO which determines the query return types.
        if (! isset($this->connections[$name])) {
            $connection = $this->makeConnection($name);

            $this->setPdoForType($connection, $type);

            $this->connections[$name] = $this->prepare($connection);
        }

        return $this->connections[$name];
    }

简单来说,Illuminate\Database\Capsule\Manager的大部分静态方法实际是调用了Illuminate\Database\DatabaseManager实例中管理的Connection对象,Connection对象的具体的链接又是由Connector来处理的。Connection类就是最终参考。具体来说就是Illuminate\Database\MySqlConnection,而它继承自Illuminate\Database\Connection,比如table()方法,statement()方法,都在这里定义。比如getSchemaBuilder()会产生一个结构构建器,而query()方法会产生一个查询构建器。ORM的实现自然是要依赖具体的Connection对象的。

最后,我们还关心一个掉线重连的方法:最终的产生的SQL会由run()方法来运行:

    protected function run($query, $bindings, Closure $callback)
    {
        $this->reconnectIfMissingConnection();

        $start = microtime(true);

        // Here we will run this query. If an exception occurs we'll determine if it was
        // caused by a connection that has been lost. If that is the cause, we'll try
        // to re-establish connection and re-run the query with a fresh connection.
        try {
            $result = $this->runQueryCallback($query, $bindings, $callback);
        } catch (QueryException $e) {
            if ($this->transactions >= 1) {
                throw $e;
            }

            $result = $this->tryAgainIfCausedByLostConnection(
                $e, $query, $bindings, $callback
            );
        }

        // Once we have run the query we will calculate the time that it took to run and
        // then log the query, bindings, and execution time so we will report them on
        // the event that the developer needs them. We'll log time in milliseconds.
        $time = $this->getElapsedTime($start);

        $this->logQuery($query, $bindings, $time);

        return $result;
    }

首先调用reconnectIfMissingConnection()来判断是否已经掉线,如果掉线则重新链接。然后运行SQL,运行是如果抛出异常,判断是否是因为掉线而抛出的异常,如果是则重连后再次运行SQL。这里有一个双重保险,在队列监听器中,这个就特别有用了。

Laravel 5 IDE代码提示

Laravel中引入了Facade,这样的搞法,IDE是晕菜的,无法识别而进行代码提示,如果没有代码提示,除非你记住所有的方法(或者你需要的部分方法),或者自己去对应的类翻看,否则,写代码的效率是低效的。实际要解决这个代码提示问题,并不太困难,简单来说,把这些Facade类继承对应的类,然后在其中写对应的静态方法。https://github.com/barryvdh/laravel-ide-helper这里提供的,正是这样的实现。

安装步骤:

## 依赖 这个东西就是产生代码的工具
composer require barryvdh/laravel-ide-helper

## 在config/app.php的providers中加入
Barryvdh\LaravelIdeHelper\IdeHelperServiceProvider::class,

## 产生用于Facade提示的代码
php artisan ide-helper:generate 
#######
#或者修改composer.json文件,添加"php artisan ide-helper:generate"
#以后更新都会重新生成
"scripts":{
    "post-update-cmd": [
        "php artisan clear-compiled",
        "php artisan ide-helper:generate",
        "php artisan optimize"
    ]
},
#######

命令”php artisan ide-helper:generate”实际会在根目录下面生成_ide_helper.php文件,可以在.gitignore中添加此文件用来禁止提交到版本库。

由于Laravel的容器技术,比如app(‘events’),IDE也无法对其进程识别,不过在phpStorm中,可以运行php artisan ide-helper:meta来产生一个映射文件:

php artisan ide-helper:meta


<?php
namespace PHPSTORM_META {

   /**
    * PhpStorm Meta file, to provide autocomplete information for PhpStorm
    * Generated on 2017-02-13.
    *
    * @author Barry vd. Heuvel <barryvdh@gmail.com>
    * @see https://github.com/barryvdh/laravel-ide-helper
    */
    $STATIC_METHOD_TYPES = [
        new \Illuminate\Contracts\Container\Container => [
            '' == '@',
            'events' instanceof \Illuminate\Events\Dispatcher,
            'router' instanceof \Illuminate\Routing\Router,
            'url' instanceof \Illuminate\Routing\UrlGenerator,
            'redirect' instanceof \Illuminate\Routing\Redirector,
            'Illuminate\Contracts\Routing\ResponseFactory' instanceof \Illuminate\Routing\ResponseFactory,
            'Illuminate\Contracts\Http\Kernel' instanceof \App\Http\Kernel,
            'Illuminate\Contracts\Console\Kernel' instanceof \App\Console\Kernel,
            'Illuminate\Contracts\Debug\ExceptionHandler' instanceof \App\Exceptions\Handler,
            'auth' instanceof \Illuminate\Auth\AuthManager,
            'auth.driver' instanceof \Illuminate\Auth\Guard,
            'Illuminate\Contracts\Auth\Access\Gate' instanceof \Illuminate\Auth\Access\Gate,
            'illuminate.route.dispatcher' instanceof \Illuminate\Routing\ControllerDispatcher,

实际上就是告诉IDE某某key对应的就是某某类。命令运行后会在根目录下生成.phpstorm.meta.php文件,可以在.gitignore中添加此文件用来禁止提交到版本库。(注意,这个仅仅针对phpStorm IDE有效)。

另外,Barryvdh\LaravelIdeHelper\IdeHelperServiceProvider::class添加到系统的是一个延时服务提供者,延时服务提供者如果没有使用到,在整个系统中就仅仅占用一个字符串大小的内存,所以并不会对实际项目产生性能影响。

Laravel 分页详解

对大数据集合进行分页的操作非常的常见。以至于必须封装成一个傻瓜式的组件。分页永远离不开两个东西,1、分页器,2、渲染器。Laravel中出现构建器或者模型都提供了paginate(和simplePaginate)方法,它就是返回一个分页器。话说,分页包含什么,当然是当前页面的数据集合和分页相关的信息(比如页大小,当前页码,总页数等等这类信息),而渲染器就用这个分页器的信息构建输出。

我们从查询构建起的paginate方法进入:

// 方法实现
    public function paginate($perPage = 15, $columns = ['*'], $pageName = 'page', $page = null)
    {
        $page = $page ?: Paginator::resolveCurrentPage($pageName);

        $total = $this->getCountForPagination($columns);

        $results = $this->forPage($page, $perPage)->get($columns);

        return new LengthAwarePaginator($results, $total, $perPage, $page, [
            'path' => Paginator::resolveCurrentPath(),
            'pageName' => $pageName,
        ]);
    }

// 例子 
$articles = DB::table("articles")->select('articles.title','articles.body','comments.page_id','comments.id')
        ->leftJoin("comments","articles.id","=","comments.page_id")
        ->paginate(5);

方法paginate()第一参数指定了页大小(默认就是15),第二参数指定了要哪些字段(一般在查询时指定),第三参数指定了页码名称,第四就是页码。实际的例子只指定了第一参数(页大小),那么页码是如何取到的?(玛尼的),好吧,它有一个服务提供者,在框架初始化时被load进来(app.php中定义)

class PaginationServiceProvider extends ServiceProvider
{
    /**
     * Register the service provider.
     *
     * @return void
     */
    public function register()
    {
        Paginator::currentPathResolver(function () {
            return $this->app['request']->url();
        });

        Paginator::currentPageResolver(function ($pageName = 'page') {
            return $this->app['request']->input($pageName);
        });
    }
}

Illuminate\Pagination\Paginator的静态方法currentPathResolver和currentPageResolver,分别把当前的路径(?号之前)和 获取到当前页。回头看那个paginate()方法,里面有Paginator::resolveCurrentPage()和resolveCurrentPath(),最后放回一个LengthAwarePaginator()的实例,这个东西的方法,大部分来自Illuminate\Pagination\AbstractPaginator,打开看看就一目了然了。

分页器的实例已经获取到了,那么渲染器是如何工作的呢?先看一个实例:

$articles = DB::table("articles")->select('articles.title','articles.body','comments.page_id','comments.id')
        ->leftJoin("comments","articles.id","=","comments.page_id")
        ->paginate(5);
        
return view('test.test', ['articles' => $articles]);
//视图
<div class="container">
    @foreach ($articles as $article)
        {{ $article->title }}
        <br />
    @endforeach
</div>

{!! $articles->render() !!}

好吧,一切尽在分页器中。可以直接foreach它,可以直接调用它的render()渲染输出,重点在render()方法:

    public function render(Presenter $presenter = null)
    {
        if (is_null($presenter) && static::$presenterResolver) {
            $presenter = call_user_func(static::$presenterResolver, $this);
        }

        $presenter = $presenter ?: new BootstrapThreePresenter($this);

        return $presenter->render();
    }

如果没有提供渲染器,检查static::$presenterResolver,它需要给出一个闭包函数,这个闭包函数需要一个参数(最终使用具体的实例传入这个这个闭包),返回一个渲染器。否则就整一个默认的BootstrapThreePresenter($this)给你。针对static::$presenterResolver,几乎没有悬念,必定有那么一个方法设置它,方法在AbstractPaginator抽象类中:

    public static function presenter(Closure $resolver)
    {
        static::$presenterResolver = $resolver;
    }

这样,我们定义一个自定义的渲染器:

<?php

namespace App\Help;

use Illuminate\Pagination\BootstrapThreePresenter;

class MyPagination extends BootstrapThreePresenter {
    public function getActivePageWrapper($text) {
        return '<div class="active item">' . $text . '</div>';
    }
    public function getDisabledTextWrapper($text) {
        return '<div class="disabled item">' . $text . '</div>';
    }
    public function getAvailablePageWrapper($url, $page, $rel = null) {
        return '<a href="' . $url . '" class="item">' . $page . '</a>';
    }
    public function render() {
        if ($this->hasPages ()) {
            return sprintf ( '%s %s %s', $this->getPreviousButton (), $this->getLinks (), $this->getNextButton () );
        }
        
        return '';
    }
}

// 补充以下实例
$articles = DB::table("articles")->select('articles.title','articles.body','comments.page_id','comments.id')
        ->leftJoin("comments","articles.id","=","comments.page_id")
        ->paginate(5);

// render()就可应用自定义的渲染器
LengthAwarePaginator::presenter(function($articles){
    return new MyPagination($articles);
});
//
        
return view('test.test', ['articles' => $articles]);
//视图
<div class="container">
    @foreach ($articles as $article)
        {{ $article->title }}
        <br />
    @endforeach
</div>

{!! $articles->render() !!}

这样,自定义的分页渲染器就弄好了。如果不希望每次都搞一次,可以把代码写入App\Providers\AppServiceProvider的register方法中:

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use Illuminate\Pagination\LengthAwarePaginator;
use App\Helper\MyPagination;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot()
    {
        //
    }

    /**
     * Register any application services.
     *
     * @return void
     */
    public function register()
    {
        //
        LengthAwarePaginator::presenter(function($pager){
            return new MyPagination($pager);
        });
    }
}

有时候,分页链接需要携带自定义参数:

$articles = DB::table("articles")->select('articles.title','articles.body','comments.page_id','comments.id')
        ->leftJoin("comments","articles.id","=","comments.page_id")
        ->paginate(5);
$articles->appends(['xx'=>'xx','oo'=>'oo']);

// 产生的链接如下
http://laravel.ifeeline.com/test/test?xx=xx&oo=oo&page=2

以下是分页器的方法列表:

    $results->count()
    $results->currentPage()
    $results->hasMorePages()
    $results->lastPage() (使用simplePaginate时无效)
    $results->nextPageUrl()
    $results->perPage()
    $results->total() (使用simplePaginate时无效)
    $results->url($page)

使用场景,获取当前请求URL:

// 先取回当前页码
$page = $results->currentPage();
// 然后传入url
$results->url($page)
// http://laravel.ifeeline.com/test/test?xx=xx&oo=oo&page=2

有时候为了使用Ajax进行分页,那么就需要返回一个JSON,而分页器实现了Illuminate\Contracts\Support\JsonableInterface,所以可以调用toJson()方法输出JSON数据:

{
   "total": 50,
   "per_page": 15,
   "current_page": 1,
   "last_page": 4,
   "next_page_url": "http://laravel.app?page=2",
   "prev_page_url": null,
   "from": 1,
   "to": 15,
   "data":[
        {
            // Result Object
        },
        {
            // Result Object
        }
   ]
}

Laravel的分页实现就是这样了。平心而论,也没有多先进。很多的实现思路都是这样。