halaei/helpers

PHP和Laravel的杂项助手

0.9.1 2021-08-28 06:44 UTC

README

Build Status Latest Stable Version Total Downloads Latest Unstable Version License

关于此包

这是一个收集的杂项工具集合,集成在一个包中供您使用

  • 监督器:在无限循环中安全地运行一段代码。
  • 数据对象:轻松将API调用和数据库模型中的解码json数据转换为具有类型提示、关系和您自己的逻辑的对象。
  • Eloquent缓存:用于您Eloquent模型的轻量级缓存仓库。
  • Redis锁:具有阻塞原子锁(不调用sleep())的高性能并发管理。
  • 进程:一个具有较少功能的Symfony\Process版本,不会陷入无限循环
  • NumCrypt:混淆自增整数。

监督器

监督器帮助您在无限循环中运行命令,使您能够监视和控制它。监督器是Laravel守护进程队列实现的通用化。

  • 监督器防止命令消耗过多内存,冻结或运行时间过长。
  • 如果发出artisan queue:restart命令,
    • 或达到内存限制,
    • 或收到SIGTERM信号,
    • 监督器将优雅地停止执行给定命令的循环。
  • 当发出artisan down命令,
    • 或收到SIGUSR2信号时,
    • 监督器会暂停循环。
  • 监督器通过事件监听器进行高度配置
    • 如果对LoopBeginning事件的监听器返回false,则监督器会暂停循环。
    • 如果对LoopCompleting事件的监听器返回false,则监督器会停止并终止进程。
    • 通过监听RunSucceedRunFailedSupervisorStopping事件,可以获得额外的管理和监控能力。
use Halaei\Helpers\Supervisor\Supervisor;

app(Supervisor::class)->supervise(GetTelegramUpdates::class);

class GetTelegramUpdates
{
    function handle(Api $api)
    {
        $updates = $api->getUpdates(...);
        $this->queueUpdates($updates);
    }
    function queueUpdates($updates)
    {
        ...
    }
}

要配置监督器的行为,可以将Halaei\Helpers\Supervisor\SupervisorOptions实例作为supervise()方法的第二个参数传递。

Graceful QuitsOnSignals特质。

要优雅地终止收到信号时运行的命令,请使用如下示例中的QuitsOnSignals特质

use Halaei\Helpers\Supervisor\QuitsOnSignals;

class SomeCommand
{
    use QuitsOnSignals;

    public function handle()
    {
        $this->listenToSignals();
        try {
            while(someConditionHolds()) {
                $this->process(); // a process that must be atomic (it shouldn't abort in the middle).
                $this->quitIfSignaled();
            }
        } finally {
            //Optional. Required if this process has other stuff to do after handle().
            $this->stopListeningToSignals();
        }
    }
}

数据对象

DataObject的一个实例是对键值数组的面向对象表示。数据对象的构造函数接受一个键值数组,并将其项转换为在类方法relations()中定义的类型。定义了魔法方法,以便通过

  • $object->some_property属性,
  • $object->getSomeProperty()获取器方法,
  • $object->setSomeProperty('new value')设置器方法,使数据对象的属性可访问。

数据对象是可数组的、可序列化的、可原生的,具有toArray()toJson()toRaw()方法。

use Halaei\Helpers\Objects\DataObject;

class Order extends DataObject
{
    public static function relations()
    {
        return [
            'items' => [Item::class], // items is a collection of objects of type Item.
            'destination' => Location::class, //delivered_to is an object of type Location
            'customer_mobile' => [Mobile::class, 'decode'], // customer mobile is a string that can be casted to a Mobile object via 'decode' static function.
        ];
    }
}

/**
 * @property string $item_code
 * @property int $quantity
 * @property float $unit_price
 * @property float $total_price
 */
class Item extends DataObject
{
}

/**
 * @property float $lat
 * @property float $lon
 */
class Location extends DataObject
{
}

class Mobile extends DataObject
{
    public static function decode($str)
    {
        if (preg_match('/^\+(\d+)-(\d+)$/', $str, $parts)) {
            return new self(['code' => $parts[1], 'number' => $parts[2]]);
        }
    }

    public function toRaw()
    {
        return '+'.$this->code.'-'.$this->number;
    }
}

$array = [
    'id' => 1234,
    'items' => [
        [
            'item_code' => '#100',
            'quantity' => 5,
            'unit_price' => 24,
            'total_price' => 120,
        ],
        [
            'item_code' => '#200',
            'quantity' => 1,
            'unit_price' => 80,
            'total_price' => 80,
        ],
    ],
    'final_price' => 200,
    'delivered_to' => [
        'lat' => 37.74123543,
        'lon' => 49.43254355,
    ],
    'customer_mobile' => '+98-9131231212',
];
$order = new Order($array);
echo get_class($order->delivered_to); // Location
echo $order->items[0]->quantity;// 5
var_dump($order->toArray()['mobile_number']); // ['code' => '+98', 'number' => '9131231212']
var_dump($order->toRaw()['mobile_number']); // +98-9131231212

将模型属性转换为数据对象和集合

要将模型属性转换为对象,请使用HasCastables特质,并通过static $castables定义转换,就像在DataObject类中定义关系一样。这个特质是为文档型数据库(如mongodb)设计的 - 与jenssegers/laravel-mongodb包配合使用效果极佳。要与SQL数据库一起使用,应使用$casts属性来处理JSON编码/解码。

class Order extends DataObject
{
    protected static $castables = [
        'items' => [Item::class],
        'destination' => Location::class,
        'customer_mobile' => [Mobile::class, 'decode'],
    ];
}

Eloquent缓存

《EloquentCache》类是一个带有缓存功能的键值存储库,用于您的eloquent模型。如果您想使用此存储库来缓存模型,则可以可选地让您的模型实现《Cacheable》接口。《Cacheable》的实现也可以通过《CacheableTrait》获得。功能包括:

  1. 通过《EloquentCache::find()`方法缓存通过id查询的结果。
  2. 通过《EloquentCache::findBySecondaryKey()`方法缓存通过次级键查询的结果。
  3. 通过《EloquentCache::invalidateCache》使特定模型的缓存失效,这样其他进程就不会使用该模型的缓存。
  4. 在更新和删除模型时清理缓存,通过《EloquentCache::update()`和《EloquentCache::delete()`方法。

Eloquent批量更新 & 插入忽略

当性能很重要时,使用单个《update》查询一次性更新表中的多行非常重要。为此,请使用《Collection::update》宏

$newSensorData = [
    12 => ['value' => 32, 'observed_at' => '2016-06-30 12:30:01'],
    13 => ['value' => 33, 'observed_at' => '2016-06-30 12:30:05'],
    16 => ['value' => 30, 'observed_at' => '2016-06-30 12:30:05'],
];
$sensors = Sensor::whereIn('id', array_keys($newSensorData))->get();
foreach($sensors as $sensor) {
    //some calculations then save the new values
    $sensor->value = $newSensorData[$sensor->id]['value'];
    $sensor->observed_at = $newSensorData[$sensor->id]['observed_at'];
}
$sensors->update();

生成的SQL语句(假设传感器#12的旧值为32)

UPDATE `sensors` SET
`value` = CASE 
    WHEN `id` = 13 THEN 33
    WHEN `id` = 16 THEN 30
ELSE `value` END,
`observed_at` = CASE
    WHEN `id` = 12 THEN  '2016-06-30 12:30:01'
    WHEN `id` = 13 THEN '2016-06-30 12:30:05'
    WHEN `id` = 16 THEN '2016-06-30 12:30:05'
ELSE `observed_at` END
WHERE id in (12, 13, 16)

要启用批量更新功能(+插入忽略),请在《config/app.php》中 providers 列表中注册《\Halaei\Helpers\Eloquent\EloquentServiceProvider::class》。

基于Redis的阻塞互斥锁

《\Halaei\Helpers\Redis\Lock》类提供了基于Redis的互斥锁,具有自动释放计时器。实现基于《rpoplpush》和《brpoplpush》Redis命令。

需要锁的进程应在循环中调用《lock($name, $tr = 2)》方法,直到返回true;其中《$name》是锁的名称,《$tr》是自动释放计时器,以秒为单位(具有毫秒分辨率)。如果锁处于挂起状态,《lock》方法将在最多《$tr》秒内等待,直到锁被释放。

当进程完成后,必须立即通过调用《unlock($name)》方法释放锁,使用与调用《lock》方法相同的《$name》参数。如果进程持有锁超过《$tr》秒,则被视为失败进程,锁将自动释放。

注意:如果锁在调用《lock》方法后自动释放,调用者可能不会意识到锁的释放。因此,他们应该再次调用《lock》。

use \Halaei\Helpers\Redis\Lock;

$lock = app(Lock::class);

...
// 1. Wait for lock named 'critical_section'
while (! $lock->lock('critical_section', 0.1) {}
// 2. Do some critical job
//...
// 3. Release the lock
$lock->unlock('critical_section', 0.1);

进程

《Halaei\Helpers\Process\Process》类是Symfony\Process的简化版本,它解决了一个无限循环问题。(注意:将我的修复传递给Symfony并保证不会破坏其他东西并不容易,因此我在这个包中修复了这个问题。)为了总结这个错误,如果进程没有读取所有输入,Symfony\Process可能会陷入无限循环并且永远不会发现进程已经不再运行。

目前,《Halaei\Helpers\Process》没有《Symfony\Process》的所有功能。所以,如果您没有向进程传递大量输入,或者您确定您的进程在读取所有输入之前永远不会终止,并且如果您想使用读取进程输出等功能,请放心使用《Symfony\Process》。

您可以像创建《Symfony\Process\Process》一样创建《Halaei\Helpers\Process》的实例。要运行进程,您可以通过调用《$process->run()`》来运行它,这将始终在进程退出后返回一个《Halaei\Helpers\Process\ProcessResult》实例,或者调用《$process->mustRun()`》,它只在进程以零退出时返回《ProcessResult》,但在超时、非零退出代码或启动进程失败的情况下抛出《Halaei\Helpers\Process\ProcessException》异常。

use Halaei\Helpers\Process\Process;
use \Halaei\Helpers\Process\ProcessException;

$input = "Some input string";
$timout = 10; // seconds
$p = new Process(['some', 'command', 'with', 'args'], null, null, $input, $timout);
try {
    dump($p->mustRun()->stdOut);
} catch (ProcessException $e) {
    dump ($e->getCode(), $e->result->stdErr, $e->result->stdOut, $e->result->exitCode);
}

未来计划

运行并发进程(异步)。

清理处理队列作业之间的数据库事务。

为确保即使在队列作业处理混乱后,默认数据库连接也没有问题,请在 AppServiceProvider::boot() 函数中调用 Halaei\Helpers\Listeners\RefreshDBConnections::boot()

安全终止长时间运行的工人

如果不十分小心,长时间运行的工人可能会引起意外问题。为了安全地终止长时间运行的工人,请在 AppServiceProvider::boot() 中调用 Halaei\Helpers\Listeners\RandomWorkerTerminator::boot()

禁用blade @parent

Laravel框架中的@parent特性实现中存在一个轻微的安全漏洞。因此,您可能需要禁用此功能。如果您的config/app.php文件中,将\Illuminate\View\ViewServiceProvider::class替换为\Halaei\Helpers\View\ViewServiceProvider::class

数字加密

如果您需要将自增数字混淆成看似随机的字符串,请使用Numcrypt

use Halaei\Helpers\Crypt\NumCrypt;

$crypt = new NumCrypt();
echo $crypt->encrypt(36); // 53k7hx
echo $crypt->decrypt('53k7hx'); // 36

NumCrypt构造函数接受输出中接受的字符集的字符集以及用于加密(使用XOR)的密钥。默认情况下,字符集是[a-z0-9],密钥是308312529。

注意:NumCrypt并非旨在提供密码学安全性。

use Halaei\Helpers\Crypt\NumCrypt;

$crypt = new NumCrypt('9876543210abcdef', 0 /*dont XOR*/);
echo $crypt->encrypt(16, 0 /*no padding*/); // 89
echo $crypt->decrypt('999989'); // 36

许可证

此软件包是开源软件,受MIT许可证许可。