月度归档:2016年05月

Symfony组件 之 Console

安装:

mkdir symfony-console
cd symfony-console
composer require symfony/console

创建一个命令行应用(命令在命令行应用中运行):

#新建命令行应用
vi console

#!/usr/bin/env php
<?php
if (file_exists(__DIR__.'/vendor/autoload.php')) {
    require __DIR__.'/vendor/autoload.php';
} else {
    require __DIR__.'/../../autoload.php';
}

$app = new Symfony\Component\Console\Application('Ifeeline Console', '1.0.0');

$app->run();

#添加权限
chmod +x console
#运行
./console
#运行结果
Console 1.0.0

Usage:
  command [options] [arguments]

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

Available commands:
  help  Displays help for a command
  list  Lists commands

从输出的结果可知,用法是commad [options] [arguments]。比如直接运行./console,那就是没有直接options和arguments。这里默认有两个命令(help和list), 直接运行命令行应用时,看起来是调用list命令。

在命令行中添加命令:

# 新建src目录,存放命令
mkdir src
cd src
vi TestCommand.php

<?php

namespace Ifeeline\Console;

use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Question\Question;
use Symfony\Component\Console\Question\ConfirmationQuestion;

class TestCommand extends Command
{
    protected function configure()
    {
        $this
            ->setName('test')
            ->setDescription('Test')
            ->addArgument(
                'arg',
                InputArgument::OPTIONAL,
                'Test Command argument: arg'
            )
            ->addOption(
                'opt',
                null,
                InputOption::VALUE_NONE,
                'Test Command option: opt'
            );
    }

    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $arg = $input->getArgument('arg');
        if ($arg) {
            $text = 'Test Command argument -> ' . $arg;
        } else {
            $text = 'Test Command';
        }

        if ($input->getOption('opt')) {
            $text = strtoupper($text);
        }

        $output->writeln($text);
    }
}

# 编辑composer.json文件,让命令类可以自动加载
{
    "require": {
        "symfony/console": "^4.0"
    },
    "autoload": {
        "psr-4": {
            "Ifeeline\\Console\\": "src"
        }
    }
}

# 最后composer update
composer update

添加命令到命令行应用:

#!/usr/bin/env php
<?php
if (file_exists(__DIR__.'/vendor/autoload.php')) {
    require __DIR__.'/vendor/autoload.php';
} else {
    require __DIR__.'/../../autoload.php';
}

$app = new Symfony\Component\Console\Application('Console', '1.0.0');

$app->add(new Ifeeline\Console\TestCommand);

$app->run();

运行:

./console test ifeeline
ifeeline

./console test --opt ifeeline
IFEELINE

Symfony的命令行工具包符合http://docopt.org/描述的规范。

总结来说,一个命令需要实现两个方法。一个配置命令的参数与选项,一个是命令执行主体。命令的输入与输出可用$input和$output控制。

分清楚Argument和Option:

Arguments使用空格分隔,跟在命令名称之后。它们是有顺序的,可以是可选(OPTIONAL)或者必填(REQUIRED)或者是数组(IS_ARRAY)。如果参数类型是IS_ARRAY类型,说明它可以接收多个值,由于参数是有顺序的,所以这个参数应该是最后一个定义的参数,因为参数本身是用空格分隔的

与参数不同,选项是没有顺序的(意味着你可以随意指定)并以为两个横杆开始(可以配置以一个横杆开头)。选项总是可选的,可以设置它接受一个值或没有值(让其简单作为一个布尔值,指定就是true,否则就是false)。

Option Value
InputOption::VALUE_IS_ARRAY This option accepts multiple values (e.g. --dir=/foo --dir=/bar)
InputOption::VALUE_NONE Do not accept input for this option (e.g. --yell)
InputOption::VALUE_REQUIRED This value is required (e.g. --iterations=5), the option itself is still optional
InputOption::VALUE_OPTIONAL This option may or may not have a value (e.g. --yell or --yell=loud)

在命令中运行命令:

protected function execute(InputInterface $input, OutputInterface $output)
{
    $command = $this->getApplication()->find('demo');

    $arguments = array(
        'command' => 'demo',
        'arg'    => 'ifeeline',
        '--opt'  => true,
    );

    $input = new ArrayInput($arguments);
    $returnCode = $command->run($input, $output);

    // ...
}

更多内容可参考官方文档:http://symfony.com/doc/current/components/console.html

Symfony组件 之 Finder

The Finder component finds files and directories via an intuitive fluent interface.(流式接口搜索文件和目录)

use Symfony\Component\Finder\Finder;

$finder = new Finder();
$finder->files()->in(__DIR__);

foreach ($finder as $file) {
    // Print the absolute path
    print $file->getRealpath()."\n";

    // Print the relative path to the file, omitting the filename
    print $file->getRelativePath()."\n";

    // Print the relative path to the file
    print $file->getRelativePathname()."\n";
}

Finder方法in()是唯一必须调用的方法,用来指定搜索哪个目录。Finder的方法都是流式接口,意思就是调用它的方法后返回Finder对象本身。

方法列表:

// 指定搜索路径
$finder->in(__DIR__);
$finder->files()->in(__DIR__)->in('/elsewhere');
$finder->in('src/Symfony/*/*/Resources');
$finder->in(__DIR__)->exclude('ruby'); #排除目录
$finder->ignoreUnreadableDirs()->in(__DIR__); #排除不可读目录
$finder->in('ftp://example.com/pub/'); #指定远程目录
#注册一个自定义流
use Symfony\Component\Finder\Finder;
$s3 = new \Zend_Service_Amazon_S3($key, $secret);
$s3->registerStreamWrapper("s3");
$finder = new Finder();
$finder->name('photos*')->size('< 100K')->date('since 1 hour ago');
foreach ($finder->in('s3://bucket-name') as $file) {
    // ... do something
    print $file->getFilename()."\n";
}

// 文件与目录
$finder->files(); #仅搜索文件
$finder->directories(); // 仅搜索目录
$finder->files()->followLinks(); // 跟踪符号链接
$finder->ignoreVCS(false); // 默认符号VCS文件

// 排序(排序需要先读取所有文件和目录)
$finder->sortByModifiedTime()
$finder->sortByChangedTime()
$finder->sortByAccessedTime()
$finder->sortByType()
$finder->sortByName()

// 文件名过滤
$finder->files()->name('*.php');
$finder->files()->name('/\.php$/'); #正则
$finder->files()->notName('*.rb');

// 文件内容过滤
$finder->files()->contains('lorem ipsum');
$finder->files()->contains('/lorem\s+ipsum$/i'); #正则
$finder->files()->notContains('dolor sit amet');

// 限定路径
$finder->path('some/special/dir');
$finder->path('foo/bar');
$finder->path('/^foo\/bar/');
$finder->notPath('other/dir');

// 文件大小过滤
$finder->files()->size('< 1.5K');
$finder->files()->size('>= 1K')->size('<= 2K');

//文件时间过滤(strtotime能用的格式都能用)
$finder->date('since yesterday');

//目录深度
$finder->depth('== 0');
$finder->depth('< 3');

//读取文件内容
use Symfony\Component\Finder\Finder;

$finder = new Finder();
$finder->files()->in(__DIR__);

foreach ($finder as $file) {
    $contents = $file->getContents();

    // ...
}

MySQL 插入数据之replace和ignore

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

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

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

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

<?php

namespace Ebt\ModelExtend;

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

用法:

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

PHP时间验证范例

$dt = "2016-02-30 01:04:44";
$e=date_parse_from_format("Y-m-d H:i:s",$dt);        
print_r($e);

// 2月份肯定没有30号,发出警告
Array
(
    [year] => 2016
    [month] => 2
    [day] => 30
    [hour] => 1
    [minute] => 4
    [second] => 44
    [fraction] =>
    [warning_count] => 1
    [warnings] => Array
        (
            [19] => The parsed date was invalid
        )

    [error_count] => 0
    [errors] => Array
        (
        )

    [is_localtime] =>
)

$dt = "2016-02-22 01:04:60";
$e=date_parse_from_format("Y-m-d H:i:s",$dt);        
print_r($e);
// 时间没有60秒
Array
(
    [year] => 2016
    [month] => 2
    [day] => 22
    [hour] => 1
    [minute] => 4
    [second] => 60
    [fraction] =>
    [warning_count] => 1
    [warnings] => Array
        (
            [19] => The parsed time was invalid
        )

    [error_count] => 0
    [errors] => Array
        (
        )

    [is_localtime] =>
)

$dt = "2016-2-2 01:04:20";
$e=date_parse_from_format("Y-m-d H:i:s",$dt);        
print_r($e);
// 合法,不过好像不是很满意,月份和日期望是两位数
Array
(
    [year] => 2016
    [month] => 2
    [day] => 2
    [hour] => 1
    [minute] => 4
    [second] => 20
    [fraction] =>
    [warning_count] => 0
    [warnings] => Array
        (
        )

    [error_count] => 0
    [errors] => Array
        (
        )

    [is_localtime] =>
)

从输出可以看到,要保证给定时间合法,输出的数组warning_count和error_count都应该等于0,从输出来看,它只是根据给定的格式来解析时间,所以2和02都是合法的月份。那么,在验证通过后,可以对时间来一次格式化:

$dt = "2016-2-2 01:04:20";
$e=date_parse_from_format("Y-m-d H:i:s",$dt);  
if(($e['warning_count'] === 0) && ($e['error_count'] === 0)) {
    $dt = date("Y-m-d H:i:s", strtotime($dt));
    echo $dt;
}

看起来,这样做法是可以的。另外,strtotime()在转换一个字符串为时间戳时,如果不成功就返回false,看如下例子:

        // 输出2016-03-02 01:04:20 
        $dt = "2016-2-31 01:04:20";
        $fdt = strtotime($dt);
        if(false !== $fdt) {
            echo date("Y-m-d H:i:s", $fdt)."\n";
        } else {
            echo $dt." xxx\n";
        }
               
        // 无法转换
        $dt = "2016-2-2 01:04:61";
        $fdt = strtotime($dt);
        if(false !== $fdt) {
            echo date("Y-m-d H:i:s", $fdt)."\n";
        } else {
            echo $dt." xxx\n";
        }

可以看到,2月31号,它认为是合法的,实际会给你变成3月2号,但是61秒就无法向上进一变成05分02秒,很明显,这个不是我们想要的,不符合预期。

Editplus编辑器常规设置

View -> Syntax Highlighting			语法高亮
View -> Word Highlighting			词高亮(选中某个词时高亮相同的词)
View -> Brace Highlighting			定界符高亮配对
View -> Line Number				行号
View -> Indent Guide				对齐线

View -> Code Folding -> Use Code Folding	代码折叠

View -> While Spaces -> Spaces			空格字符是否显示
View -> While Spaces -> Tabs			Tab符是否显示
View -> While Spaces -> Line Breaks		Tab符是否显示

Document -> Word Wrap				自动换行
Document -> Auto Indent				自动对齐线
Document -> File Encoding			文件编码查看转换

对于Editplus,我只是用它来替换Windows下的默认文本编辑器而已。有时候要快速打开一个文件时,它的语法高亮,词高亮,行号,对齐线,自动换行,查看文件编码和转换文件编码也是我经常需要使用的。至于代码提示,自动完成和定位,特别是代码格式化,都不是它擅长的。

习惯使用Dreamweaver来处理HTML CSS JS,用IDE来编写PHP程序,用Editpus替代Windows默认编辑器来快速查看编辑文件。

editplus_coding

PHP DOM文档操作

PHP中的DOM扩展(默认启用,可用–disable-dom关闭)依赖libxml(默认启用),这个扩展提供了操作DOM文档的全部方法,不过这个这些操作方法相对还是原始了一点,这个类似于在JavaScript中操作使用原生的函数操作DOM,相比之下,我们可能更加喜欢使用JQuery来代替这个原始的操作,同样,在PHP中我们也喜欢有类似Jquery这样的工具,可以非常方便的操作DOM文档。

目前在PHP的第三方包中,phpQuery几乎实现了JQuery大部分通用的方法,不过这个包已经很久未更新了。另外一个我觉得不错的是Symfony的组件包DomCrawler,虽然它没有提供JQuery那么多的方法,也没有一一对应它的方法,但是使用它来替代phpQuery,完全是可以的。

官方文档参考:https://symfony.com/doc/current/components/dom_crawler.html,从这个包的composer.json中可以知道,它依赖CssSelector,这个也是Symfony的组件包之一,DomCrawler用它来把CSS选择器转换成xpath。

DomCrawler实际是PHP DOM扩展的包装器而已。在DOM中, DOMNode是基本类,其它的DOMElement和DOMDocument都是继承DOMNode的,可以理解为它们是特殊一点的DOMNode。以下是对DomCrawler的一些使用例子(来自官方文档)。

        $html = <<<'HTML'
<!DOCTYPE html>
<html>
    <body>
        <p class="message">Hello World!</p>
        <p>Hello Crawler!</p>
    </body>
</html>
HTML;
        $crawler = new Crawler($html);
        
        foreach ($crawler as $domElement) {
            var_dump($domElement->nodeName);
        }
        //$crawler = $crawler->filterXPath('descendant-or-self::body/p');
        
        $crawler = $crawler->filter('body > p');
        $crawler->each(function($c, $i){
            echo $c->html();
        });
        
        // 原生DOMElement
        foreach($crawler as $cr) {
            $cro = new Crawler($cr);
            
            echo $cro->html();
        }

这里的两种遍历的方法,第一个是$crawler对象的each方法,看一下这个方法源代码:

    public function each(\Closure $closure)
    {
        $data = array();
        foreach ($this as $i => $node) {
            $data[] = $closure($this->createSubCrawler($node), $i);
        }

        return $data;
    }

把自身对象保存的$node进行遍历,放入createSubCrawler()方法,这个方法接收的参数类型是\DOMElement|\DOMElement[]|\DOMNodeList|null,这个说明$crawler对象里面保存的$node就是原生的DOMNode,在each的时候又使用DomCrawler来包装这个DOMNode让其是一个DomCrawler对象,这样就可以直接操作DomCrawler的方法,我们几乎可以把$crawler对象看做是JQuery中的美元符($)。

不过这里的createSubCrawler()方法是私有的,不能在外部使用,那么如果我有一个DOMElement(DOMNode),应该如何让它变成一个Crawler对象呢,很简单,就是以上的第二个例子,直接new一个Crawler,把DOMNode传入构造函数,这个构造函数实际内部会调用add()方法,这个方法负责实例化Crawler,查看add()方法可以知道,它除了接受DOMNode和DOMNodeList外(包括DOMElement等)还接受一个数组和字符串,数组自然应该是add()方法可以接受的参数类型,如果还是数组就会递归。

明白这个之后,操作就很简单了。直接来一些例子就好了:

$crawler->filter('body > p')->eq(0);
$crawler->filter('body > p')->first();
$crawler->filter('body > p')->last();
$crawler->filter('body > p')->siblings();
$crawler->filter('body > p')->nextAll();
$crawler->filter('body > p')->previousAll();
$crawler->filter('body')->children();
$crawler->filter('body > p')->parents();

$crawler = new Crawler('<html><body /></html>');

$crawler->addHtmlContent('<html><body /></html>');
$crawler->addXmlContent('<root><node /></root>');

$crawler->addContent('<html><body /></html>');
$crawler->addContent('<root><node /></root>', 'text/xml');

$crawler->add('<html><body /></html>');
$crawler->add('<root><node /></root>');

/////////////////////////////////////////
// 节点值
$tag = $crawler->filterXPath('//body/*')->nodeName();
$message = $crawler->filterXPath('//body/p')->text();
$class = $crawler->filterXPath('//body/p')->attr('class');
$attributes = $crawler
    ->filterXpath('//body/p')
    ->extract(array('_text', 'class'))

/////////////////////////////////////////
$document = new \DOMDocument();
$document->loadXml('<root><node /><node /></root>');
$nodeList = $document->getElementsByTagName('node');
$node = $document->getElementsByTagName('node')->item(0);

$crawler->addDocument($document);
$crawler->addNodeList($nodeList);
$crawler->addNodes(array($node));
$crawler->addNode($node);
$crawler->add($document);

/////////////////////////////////////
$html = '';

foreach ($crawler as $domElement) {
    $html .= $domElement->ownerDocument->saveHTML($domElement);
}

$html = $crawler->html();

这个包还提供提供了针对链接和表单的特定操作,用来快速操作它们。

在filter中可以使用的选择器语法,几乎和JQuery是一样的,这个包中实际是把CSS选择器转换成xpath来执行,它是由CssSelector来驱动的。

Laravel 数据统计实例

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

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

namespace App\Console\Commands;

use Illuminate\Console\Command;

use DB,Storage,Validator;

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

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

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

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

向Composer发布代码

Composer可以很方便管理应用的包与包依赖,自定义的包也可以发布到到packagist.org,然后通过Composer拉回来,已达到快速部署。当然,如果自定义的包非开源的,就不要这么干了。

首先,在github.com上创建账户,并把项目推送到github.com,这样会得到一个链接。
然后到packagist.org,点击使用Github账户登录,接着点击submit,这时候会要求输入github.com项目地址,接下来就是检查,这个检查会通知你当前可能重复的其它包,然后输入你包的名称(会从composer.json中自动检出),然后确认,这样包就发布出去了。

由于代码托管在github上,当向github推送代码时,packagist.org默认并不会同步更新,所以为了让packagist.org可以同步,需要在github中设置,参考:https://packagist.org/about#how-to-update-packages

To do so you can:

Go to your GitHub repository
Click the “Settings” button
Click “Integrations & services”
Add a “Packagist” service, and configure it with your API token, plus your Packagist username
Check the “Active” box and submit the form
You can then hit the “Test Service” button to trigger it and check if Packagist removes the warning about the package not being auto-updated.

简单来说这是一个事件通知服务,在github中设置一个钩子,当由事件触发时,通过packagist提供的API通知packagist,packagist就会去和github做同步操作。

接下来就是运行:

composer require ifeeline/wkhtmltox:dev-master

如果要拉开发分支,在包后加冒号接dev-master即可。

以下是一个例子:
ifeeline_whhtmltox