php ubuntu 多版本安装

php 多版本安装 from ppa

https://launchpad.net/~ondrej/+archive/ubuntu/php

sudo add-apt-repository ppa:ondrej/php
sudo apt update

然后就可以用sudo apt isntall php-7.4这种方式安装对应版本php

扩展可以直接用apt install php-7.4-redis这种方式安装

不确定的扩展名可以用apt search php-版本-redis搜索包名

  • 多版本切换

如果需要多版本切换,可以使用ubuntu 的update-alternatives实现

用上面ppa的包安装的php都会作为php的候选项,比如使用命令

update-alternatives --list php
/usr/bin/php7.4
/usr/bin/php8.2

需要切换php命令的版本可以使用

update-alternatives --config php
 2 个候选项可用于替换 php (提供 /usr/bin/php)。

  选择       路径           优先级  状态
------------------------------------------------------------
* 0            /usr/bin/php8.2   82        自动模式
  1            /usr/bin/php7.4   74        手动模式
  2            /usr/bin/php8.2   82        手动模式

如果有应用不是使用apt安装的,可以使用下面的命令添加到候选项目

# 链接  名称  (实际)路径   优先级
sudo update-alternatives --install /usr/bin/vim vim /usr/bin/vim.nox 40

  • php-fpm 多版本

如果fpm也需要多版本,可以先apt安装多版本的fpm

使用systemctl start php7.4-fpm 启动对应版本的php-fpm

查看目录/etc/php/7.4/fpm/pool.d/www.conf

 listen = /run/php/php7.4-fpm.sock  //可以看到监听地址

nginx设置php文件的fastcgi=/run/php/php7.4-fpm.sock

需要切换php-fpm版本就找到对应配置的fpm监听地址,修改nginx对应网站的转发配置

PHP 对html文件进行处理

有一批静态文件需要删除一些元素,考虑使用php脚本批量处理

step1 扫描目录

use Masterminds\HTML5;
$dir = __DIR__ . '/path/to/html';
$fileNames = scandir($dir);
$html5 = new HTML5(['disable_html_ns'=> true]);

foreach ($fileNames as $fileName) {
    if (in_array($fileName, ['.', '..'])) {
        continue;
    }
    $filePath = $dir . '/' . $fileName;
    if (!is_file($filePath)) {
        continue;
    }
}

这里使用Masterminds\HTML5这个包,直接使用DOMdocument读取h5会出现问题

step2 加载文件

$document = $html5->loadHTMLFile($filePath);
$xpath = new DOMXPath($document);

step3 修改节点

clearNodes($xpath);

function clearNodes($xpath) {
    clearElements($xpath, '//div[@class="panel panel-default tab-content"]');
}

step4 保存文件

$html5->save($document, $filePath);

DateTime Class 使用

DateTime vs DateTimeImmutable

DateTime 使用modify/add/sub 等修改后,本身时间对象也会跟着修改,format输出的就是最新的时间

DateTimeImmutable修改后,本身时间对象不会修改,相关修改方法返回一个新的时间对象,用新的对象进行赋值才会修改原来的对象

修改时间方法

motify(使用方法和date类似, 传入的参数string和date构造string规则相同)

参考: https://www.php.net/manual/en/datetime.formats.relative.php

代码例子:

这里用放款后生成的一个还款通知举例

$returnTime = new DateTimeImmutable($loan['return_time']); //放款时间
$nextMonth = $returnTime->modify('first day of next month'); //下月开始还款
$notifyTime = $nextMonth->modify('+'. $loan['repayment_notify_day'] . ' days'); //还款通知时间
$repaymentTime = $nextMonth->modify('+'. $loan['repayment_last_day'] . ' days'); //最后还款时间
$notifies = [];

//$schedules 是计算出的每月应还利息本金信息
foreach ($schedules as $schedule) {
    $notifies[] = [
        'customer_id' => $loan['customer_id'],
        'loan_id' => $loan['id'],
        'notify_data' => $notifyTime->format('Y-m-d'),
        'repayment_date' => $repaymentTime->format('Y-m-d'),
        'month' => $notifyTime->format('Ym'),
        'capital_amount' => $schedule['capital'],
        'interests_amount' => $schedule['interests'],
        'total_amount' => round($schedule['interests'] + $schedule['capital'], 2),
        'status' => 'wait',
        'index' => $schedule['index'],
        'notify_time' => null,
    ];
    $notifyTime = $notifyTime->modify('+1 month');
    $repaymentTime = $repaymentTime->modify('+1 month');
}

//$notifies 就是生成的每月还款通知的对应信息了 

swoole redis连接池应用

遇到问题

有一个cli应用,需要很多协程频繁访问redis,用swoole的redis连接池实现链接;
但是遇到问题,发现内存很快溢出,第一反应是变量没释放,unset了半天没有解决;
然后通过

use Swoole\Coroutine;
$coros = Coroutine::listCoroutines();

定时查看了一下协程数量,发现协程缓慢在增加,慢慢的内存就溢出了。
限制了一下协程数量,扩充了一下连接池大小,挂起的协程数量明显减少了,但还是越来越多,猜测是从redis连接池获取连接的时候获取的连接不可用,然后一直挂起了。

解决方式

从连接池中取出连接后测试一下,可以使用再取出,不然push(null)让连接池生成一个新的,接着继续取,知道连接可用,这样处理后协程数量就一直不会一直增长了。

    protected function redisOperation($callback) {
        $connection = $this->pool->get();
        while(!$connection->ping()) {
            $this->pool->put(null);
            $connection = $this->pool->get();
        }
        $result = call_user_func($callback, $connection);
        $this->pool->put($connection);
        return $result;
    }

问题顺利解决,撒花

linux下编译安装php8

下载:

https://www.php.net/downloads

configure

从宝塔安装的php看下通用的一些扩展编译参数
# php -i |grep configure

./configure 
--prefix=/www/server/php/74  --with-config-file-path=/www/server/php/74/etc  --enable-fpm  --with-fpm-user=www  --with-fpm-group=www  --enable-mysqlnd  --with-mysqli=mysqlnd  --with-pdo-mysql=mysqlnd  --with-iconv-dir  --with-freetype  --with-jpeg  --with-zlib  --with-libxml-dir=/usr  --enable-xml  --disable-rpath  --enable-bcmath  --enable-shmop  --enable-sysvsem  --enable-inline-optimization  --with-curl  --enable-mbregex  --enable-mbstring  --enable-intl  --enable-ftp  --enable-gd  --with-openssl  --with-mhash  --enable-pcntl  --enable-sockets  --with-xmlrpc  --enable-soap  --with-gettext  --disable-fileinfo  --enable-opcache  --with-sodium  --with-webp

稍微修改以下

--prefix=/usr/local/php/81 --with-config-file-path=/usr/local/php/81/etc  --enable-fpm  --with-fpm-user=www  --with-fpm-group=www  --enable-mysqlnd  --with-mysqli=mysqlnd  --with-pdo-mysql=mysqlnd  --with-iconv-dir  --with-freetype  --with-jpeg  --with-zlib  --with-libxml-dir=/usr  --enable-xml  --disable-rpath  --enable-bcmath  --enable-shmop  --enable-sysvsem  --enable-inline-optimization  --with-curl  --enable-mbregex  --enable-mbstring  --enable-intl  --enable-ftp  --enable-gd  --with-openssl  --with-mhash  --enable-pcntl  --enable-sockets  --with-xmlrpc  --enable-soap  --with-gettext  --disable-fileinfo  --enable-opcache  --with-sodium  --with-webp

configure跑跑看缺什么就装对应软件

缺少 安装
libxml-2.0 libxml2-dev
libcurl libcurl4-openssl-dev
libpng libpng-dev
libwebp libwebp-dev
libjpeg libjpeg-dev
freetype2 libfreetype-dev
oniguruma libonig-dev
libsodium libsodium-dev

….

安装

执行make && make install

BCC 异或校验 php实现

问题描述

对接一个硬件设备协议要求进行bcc校验,数据包是十六进制表示的,用php处理

实现方式

    public static function dec2hex($data, $wordLength) {
        return str_pad(dechex($data), $wordLength * 2, '0', STR_PAD_LEFT);
    }

    //十六进制数据生成bcc校验码(十六进制)
    public static function bcc($data) {
        $data = str_split($data, 2);
        $length = count($data);
        $result = intval($data[0], 16);
        for($i=0; $i<$length-1; $i++) {
            $result ^= intval($data[$i+1], 16);
        }
        return self::dec2hex($result, 1);
    }

其它

bcc校验工具

dcat admin 多文件上传

问题描述

需要解决dcat admin 表单多附件管理的(附件名和保存文件名分开保存)

解决方案

上传文件管理

  • 首先是新建上传文件表

    Schema::create('uploads', function (Blueprint $table) {
    $table->string('disk', 30)->comment('存储位置');
    $table->string('type', 20)->default('file')->comment('文件类型');
    $table->string('name', 150)->nullable()->comment('文件名称');
    $table->string('path', 150)->comment('文件路径');
    });
  • 新建上传文件用的路由

    $router->any('/upload/handle/{disk}/{dir}', 'UploadController@handle');
  • 上传处理

    public function handle($diskName, $dir) {
        $disk =$this->disk($diskName);
    
        // 判断是否是删除文件请求
        if ($this->isDeleteRequest()) {
            Upload::where('path', request()->key)->delete();
            // 删除文件并响应
            return $this->deleteFileAndResponse($disk);
        }
    
        // 获取上传的文件
        $file = $this->file();
        $newName =  Common::uniqueID().'.'.$file->getClientOriginalExtension();
        $result = $disk->putFileAs($dir, $file, $newName);
        $path = "{$dir}/$newName";
        $upload = new Upload;
        $upload->name = request()->name;
        $upload->path = $path;
        $upload->disk = $diskName;
        $upload->save();
    
        return $result
            ? $this->responseUploaded($path, $disk->url($path))
            : $this->responseErrorMessage('文件上传失败');
    }
  • 扩展字段

    class CustomMultiFile extends MultipleFile
    {
    protected function initialPreviewConfig()
    {
        $previews = [];
        foreach ($this->value() as $path => $name) {
            $previews[] = [
                'id'   => $path,
                'path' => Helper::basename($name),
                'url'  => $this->objectUrl($path),
            ];
        }
    
        return $previews;
    }
    }
  • 注册扩展字段

    Form::extend('customMultiFile', CustomMultiFile::class);
  • 表单字段

    $form->customMultiFile('attachment', '附件')->disk('public')
     ->url('upload/handle/public/attachment')
     ->autoUpload()->autoSave(false)
     ->removable(true)->limit(10);

PHP实现随机红包

    //随机红包(总份数、最大金额、最小金额、总数)
    protected function random_redpaper($total, $max, $min, $sum){
        $sum = $sum * 100;
        //按分单位,后面全部用整数金额

        $users = array_fill(0, $total, $min);
        $left = $sum - $min * $total;

        //将剩余金额按rand(1, ceil($left/$total))每次进行随机分配, $left 小于50直接分配完成
        for(;$left > 0;){
            $amount = $left > 50 ? rand(1, ceil($left/$total)) : $left;
            $user_index = rand(0, $total-1);
            if($users[$user_index] + $amount > $max){
                $amount = $max - $users[$user_index];
                $users[$user_index] = $max;
            }else{
                $users[$user_index] += $amount;
            }
            $left -= $amount;
        }

        //恢复按元单位
        foreach($users as &$user){
            $user = $user/100;
        }
        return $users;
    }

Laravel model 使用trait+boot初始化

目标

用trait来给一些model添加通用的boot方法,加上相同的globalscope

解决方法

试了一下,直接在trait里面写boot方法是没用的,然后找到了这个问答

https://laracasts.com/discuss/channels/eloquent/cant-use-trait-if-i-have-a-boot-method-declared

原来源码里已经写好了这个调用了,搜索一下model里面的boot方法

/**
     * Bootstrap the model and its traits.
     *
     * @return void
     */
    protected static function boot()
    {
        static::bootTraits();
    }

    /**
     * Boot all of the bootable traits on the model.
     *
     * @return void
     */
    protected static function bootTraits()
    {
        $class = static::class;

        $booted = [];

        static::$traitInitializers[$class] = [];

        foreach (class_uses_recursive($class) as $trait) {
            $method = 'boot'.class_basename($trait);

            if (method_exists($class, $method) && ! in_array($method, $booted)) {
                forward_static_call([$class, $method]);

                $booted[] = $method;
            }

            if (method_exists($class, $method = 'initialize'.class_basename($trait))) {
                static::$traitInitializers[$class][] = $method;

                static::$traitInitializers[$class] = array_unique(
                    static::$traitInitializers[$class]
                );
            }
        }
    }

哈哈,原来框架已经实现好了,感叹一下确实想的太周到了哈!!
只要在trait里面写
‘boot’.class_basename($trait)
也就是boot+trait名字的方法就会被调用了。

PHP常量表的实现

实际项目过程,经常遇到一些键值组成的常量表,今天想了一下怎么用比较好的方式实现这种结构。

  • 一个想法是建一个常量表,通过数据库来管理常量,这个实现比较直接,就不展开了。

  • 另一个想法是把这种结构用类来表示。

abstract class Constants{
    static $map = [];
    public static function getKey($value){
        return !empty($key = array_search($value, static::$map)) ? $key : null;
    }

    public static function getValue($key){
        return isset(static::$map[$key])? static::$map[$key] : null;
    }

    public static function getMap(){
        return static::$map;
    }
}

class Phone extends Constants{
    static $map = [
        10086 => '移动',
        10000 => '电信'
    ];

}

echo Phone::getValue(10000), PHP_EOL;
echo Phone::getKey('电信'), PHP_EOL;

这里本来Constants 这个类本来打算设置一个$map的abstract的属性,但是却报错了,属性不支持定义为abstract。

然后一顿搜索,找到了一个替代方案

abstract class Foo_Abstract {
  abstract public function get_tablename();
}

class Foo extends Foo_Abstract {
  protected $tablename = 'tablename';
  public function get_tablename() {
    return $this->tablename;
  }
}

不过这样代码会多很多,所以感觉用上面的实现也足够了吧。