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

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

laravel sanctum 错误自定义返回

laravel sanctum 错误自定义返回

现象

sanctum 认证失败会自动返回401或419 errercode,
前端认证逻辑这里会受到影响,所以希望可以自定义错误返回。

解决方法

根据

https://laravel.com/docs/9.x/errors#introduction

错误处理定义的handler内处理,这个类继承自框架的vendor\laravel\framework\src\Illuminate\Foundation\Exceptions\Handler.php

    public function render($request, Throwable $e)
    {
        if (method_exists($e, 'render') && $response = $e->render($request)) {
            return Router::toResponse($request, $response);
        }

        if ($e instanceof Responsable) {
            return $e->toResponse($request);
        }

        $e = $this->prepareException($this->mapException($e));

        if ($response = $this->renderViaCallbacks($request, $e)) {
            return $response;
        }

        return match (true) {
            $e instanceof HttpResponseException => $e->getResponse(),
            $e instanceof AuthenticationException => $this->unauthenticated($request, $e),
            $e instanceof ValidationException => $this->convertValidationExceptionToResponse($e, $request),
            default => $this->renderExceptionResponse($request, $e),
        };
    }

这里用到php8的match表达式,顺便学习一下

https://www.php.net/manual/en/control-structures.match.php

可以看到会调用到unauthenticated这个方法,方法是protected的,所以可以在项目里继承自这个handler的类里面覆写这个方法

    protected function unauthenticated($request, AuthenticationException $exception) {
        return response()->json(['code' => 403, 'message' => '未授权']);
    }