分类目录归档:laravel

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;
    }
    
}

Laravel 命令行工具实例

大数据导入

<?php
 
namespace eBay\Console\Commands;
 
use Illuminate\Console\Command;
use DB;
use Schema;
 
class AmazonImport extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'amazon:import';
 
    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Import Amazon Data';
 
    /**
     * Create a new command instance.
     *
     * @return void
     */
    public function __construct()
    {
        parent::__construct();
    }
 
    /**
     * Execute the console command.
     *
     * @return mixed
     */
    public function handle()
    {
        //
        $import = DB::table('amazon_imports')->where("has_import",'<',1)->orderBy('date')->first();
        if(!isset($import->id)) { return; }
         
        // 表名
        $tableName = 'amazon_import_'.$import->date;
        // 删表,如果存在
        Schema::dropIfExists($tableName);
        // 建表
        $this->builderTable($tableName);
         
        if (Schema::hasTable($tableName)){
            $s = microtime(true);
             
            // 插入数据
            $file = 'D:/11-201.txt';
            $row = 0; $dd = [];
            if (($handle = fopen($file, "r")) !== FALSE) {
                while (($data = fgetcsv($handle, 0, "\t")) !== FALSE) {
                    $row++;
                    if($row < 2) { continue; }
                    $d = [ 
                        'marketplace_id' => $data [0],
                        'gl_product_group' => $data [1],
                        'gl_product_group_desc' => $data [2],
                        'category_desc' => $data [3],
                        'subcategory_desc' => $data [4],
                        'asin' => $data [5],
                        'item_name' => $data [6],
                        'brand_name' => $data [7],
                        'ordered_gms_usd_tim' => $data [8],
                        'ordered_units_tim' => $data [9],
                        'fba_ordered_gms_usd_tim' => $data [10],
                        
                        'fba_ordered_units_tim' => $data [11],
                        'asp' => round($data [12],15),
                        
                        'gms_rank' => $data [13],
                        'gms_view_count_tim' => $data [14],
                        'units_conversion_rate' => round($data [15],15)
                    ];
                    
                    //if($row > 5000) { break; }
                    //continue;
                    
                    $dd [] = $d;
                     
                    // 合理调整,MySQL对于批量数据大小有限制
                    if (count ( $dd ) > 1999) {
                        DB::table ( $tableName )->insert ( $dd );
                        $dd = [ ];
                    }
                }
                if(!empty($dd)) {
                    DB::table ( $tableName )->insert ( $dd );
                    $dd = [ ];
                }
                $e = microtime(true);
                echo 'Total:'.(float)($e - $s)."\n";
                 
                fclose($handle);
                 
                // 
                DB::table('amazon_imports')->where('id',$import->id)->update(['has_import'=>1]);
            }
            
            // 插表完成后,更新类目
            DB::table('amazon_categories')->truncate();            
            $prefix = DB::connection()->getTablePrefix();  
            $query = "INSERT INTO ".$prefix."amazon_categories(product_group, product_group_desc, category_desc,subcategory_desc)
                SELECT gl_product_group, gl_product_group_desc, category_desc, subcategory_desc FROM `".$prefix.$tableName."` GROUP BY gl_product_group_desc,category_desc,subcategory_desc";  
            DB::statement($query);
             
            // 插表完成后,产生比对数据
            $compares = DB::table('amazon_imports')
                ->where("has_import",'>',0)
                ->where('id','!=',$import->id)
                ->orderBy('date','desc')->take(4)->get();
             
            $this->call('amazon:caculate', [
                'from' => $import->date
            ]);
        } else {
            return;
        }
    }
     
    public function builderTable($tableName)
    {
        Schema::create($tableName, function($table)
        {
            $table->increments('id');
             
            //
            $table->tinyInteger('marketplace_id')->unsigned();
            $table->smallInteger('gl_product_group')->unsigned();
            $table->string('gl_product_group_desc',32);
            $table->string('category_desc',64);
            $table->string('subcategory_desc',64);
            $table->string('asin',16);
            $table->string('item_name',128);
            $table->string('brand_name',64);
            $table->decimal('ordered_gms_usd_tim',10,2);
            $table->decimal('fba_ordered_gms_usd_tim',10,2);
            $table->smallInteger('ordered_units_tim');
            $table->smallInteger('fba_ordered_units_tim');
            $table->double('asp',20,15);
            $table->smallInteger('gms_rank');
            $table->mediumInteger('gms_view_count_tim');
            $table->double('units_conversion_rate',20,15);
            
            $table->index('asin');
        });
    }
}

执行批量计算

<?php
 
namespace eBay\Console\Commands;
 
use Illuminate\Console\Command;
use DB;
use Schema;
 
class AmazonCaculate extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'amazon:caculate {from}';
 
    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Caculate Amazon Data';
 
    /**
     * Create a new command instance.
     *
     * @return void
     */
    public function __construct()
    {
        parent::__construct();
    }
 
    /**
     * Execute the console command.
     *
     * @return mixed
     */
    public function handle()
    {
        // 取回参数
        $date = $this->argument('from');
        if(!empty($date)) {
            $has = DB::table("amazon_imports")->where('date',$date)->where('has_import','>',0)->first();
            if(!isset($has->id)) {
                echo 'Import Date:'.$date." Do Not Import.\n";
                return;
            }
        }
        
        // 取回需要比较的日期,当前记录的前4个
        $compares = DB::table('amazon_imports')
            ->where("has_import",'>',0)
            ->where('id','<', $has->id)
            ->orderBy('date','desc')->take(4)->get();
        $nowTable = "amazon_import_".$has->date;
        
        // 原表不存在,就不要比较了
        if (!Schema::hasTable($nowTable)){
            echo "Table ".$nowTable." DO NOT Existes\n";
            return;
        }

        foreach($compares as $compare) {
            $beforeTable = "amazon_import_".$compare->date;
            // 被比较的表不存在,就到此结束,后面的咔嚓
            if (!Schema::hasTable($beforeTable)){
                break;
            }
            
            $tableName = 'amazon_'.$has->date."_".$compare->date;
            // 如果结果表已经存在,不需要重复
            if (!Schema::hasTable($tableName)){
                // 删表,如果存在
                // Schema::dropIfExists($tableName);
                // 建表
                $this->builderTable($tableName);
            
                // 批量插入
                $prefix = DB::connection()->getTablePrefix();
                $query = "INSERT INTO `".$prefix.$tableName."`
                    (marketplace_id,
                    gl_product_group,
                    gl_product_group_desc,
                    category_desc,
                    subcategory_desc,
                    asin,
                    item_name,
                    brand_name,
                    
                    ordered_gms_usd_tim,
                    ordered_gms_usd_tim_diff,
                    
                    fba_ordered_gms_usd_tim,
                    fba_ordered_gms_usd_tim_diff,
                    
                    ordered_units_tim,
                    ordered_units_tim_diff,
                    
                    fba_ordered_units_tim,
                    fba_ordered_units_tim_diff,
                    
                    asp,
                    asp_diff,
                    
                    gms_rank,
                    gms_rank_diff,
                    
                    gms_view_count_tim,
                    gms_view_count_tim_diff,
                    
                    units_conversion_rate,
                    
                    is_new)
                SELECT 
                    n.marketplace_id, 
                    n.gl_product_group, 
                    n.gl_product_group_desc, 
                    n.category_desc,
                    n.subcategory_desc,
                    n.asin,
                    n.item_name,
                    n.brand_name,
                    n.ordered_gms_usd_tim,
                    (n.ordered_gms_usd_tim - b.ordered_gms_usd_tim) AS ordered_gms_usd_tim_diff,
                    
                    n.fba_ordered_gms_usd_tim,
                    (n.fba_ordered_gms_usd_tim - b.fba_ordered_gms_usd_tim) AS fba_ordered_gms_usd_tim_diff,
                    
                    n.ordered_units_tim,
                    (n.ordered_units_tim - b.ordered_units_tim) AS ordered_units_tim_diff,
                    
                    n.fba_ordered_units_tim,
                    (n.fba_ordered_units_tim - b.fba_ordered_units_tim) AS fba_ordered_units_tim_diff,
                    
                    n.asp,
                    (n.asp - b.asp) AS asp_diff,
                    
                    n.gms_rank,
                    (n.gms_rank - b.gms_rank) AS gms_rank_diff,
                    
                    n.gms_view_count_tim,
                    (n.gms_view_count_tim - b.gms_view_count_tim) AS gms_view_count_tim_diff,
                    
                    n.units_conversion_rate,
                    
                    IF(b.id IS NULL,1,0)
                FROM `".$prefix.$nowTable."` n LEFT JOIN `".$prefix.$beforeTable."` b ON n.asin = b.asin";
                
                DB::statement($query);
            } 
        }
    }
     
    public function builderTable($tableName)
    {
        Schema::create($tableName, function($table)
        {
            $table->increments('id');
             
            //
            $table->tinyInteger('marketplace_id')->unsigned();
            $table->smallInteger('gl_product_group')->unsigned();
            $table->string('gl_product_group_desc',32);
            $table->string('category_desc',64);
            $table->string('subcategory_desc',64);
            $table->string('asin',16);
            $table->string('item_name',128);
            $table->string('brand_name',64);
            $table->decimal('ordered_gms_usd_tim',10,2);
            $table->decimal('ordered_gms_usd_tim_diff',10,2);
            
            $table->decimal('fba_ordered_gms_usd_tim',10,2);
            $table->decimal('fba_ordered_gms_usd_tim_diff',10,2);
            
            $table->integer('ordered_units_tim');
            $table->integer('ordered_units_tim_diff');
            
            $table->integer('fba_ordered_units_tim');
            $table->integer('fba_ordered_units_tim_diff');
            
            $table->double('asp',20,15);
            $table->double('asp_diff',20,15);
            
            $table->integer('gms_rank')->unsigned();
            //$table->smallInteger('gms_rank_before')->unsigned();
            $table->integer('gms_rank_diff');
            
            $table->integer('gms_view_count_tim');
            $table->integer('gms_view_count_tim_diff');
            
            $table->double('units_conversion_rate',20,15);
            $table->tinyInteger('is_new')->default(0);
            
            $table->index('asin','asin_idx');
            
            //$table->index('gl_product_group_desc');
            //$table->index('category_desc');
            //$table->index('subcategory_desc');
            $table->index(['gl_product_group_desc','category_desc','subcategory_desc'],'category_idx');
        });
    }
}

命令行工具是非常便利的,数据库的操作相比之下也省了不少功夫。

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一下就可以开始使用了。

以下是方法列表:

/**
 * @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组件就提供了相对比较丰富的实现。