halaei / helpers
PHP和Laravel的杂项助手
Requires
- php: >=7.4.0
Requires (Dev)
- illuminate/cache: ^8.0
- illuminate/console: ^8.0
- illuminate/database: ^8.0
- mockery/mockery: ^1.0
- phpunit/phpunit: ^9.0
- predis/predis: ^1.1
- symfony/process: ^5.0
README
关于此包
这是一个收集的杂项工具集合,集成在一个包中供您使用
- 监督器:在无限循环中安全地运行一段代码。
- 数据对象:轻松将API调用和数据库模型中的解码json数据转换为具有类型提示、关系和您自己的逻辑的对象。
- Eloquent缓存:用于您Eloquent模型的轻量级缓存仓库。
- Redis锁:具有阻塞原子锁(不调用sleep())的高性能并发管理。
- 进程:一个具有较少功能的Symfony\Process版本,不会陷入无限循环!
- NumCrypt:混淆自增整数。
监督器
监督器帮助您在无限循环中运行命令,使您能够监视和控制它。监督器是Laravel守护进程队列实现的通用化。
- 监督器防止命令消耗过多内存,冻结或运行时间过长。
- 如果发出
artisan queue:restart命令,- 或达到内存限制,
- 或收到
SIGTERM信号, - 监督器将优雅地停止执行给定命令的循环。
- 当发出
artisan down命令,- 或收到
SIGUSR2信号时, - 监督器会暂停循环。
- 或收到
- 监督器通过事件监听器进行高度配置
- 如果对
LoopBeginning事件的监听器返回false,则监督器会暂停循环。 - 如果对
LoopCompleting事件的监听器返回false,则监督器会停止并终止进程。 - 通过监听
RunSucceed、RunFailed和SupervisorStopping事件,可以获得额外的管理和监控能力。
- 如果对
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》获得。功能包括:
- 通过《EloquentCache::find()`方法缓存通过id查询的结果。
- 通过《EloquentCache::findBySecondaryKey()`方法缓存通过次级键查询的结果。
- 通过《EloquentCache::invalidateCache》使特定模型的缓存失效,这样其他进程就不会使用该模型的缓存。
- 在更新和删除模型时清理缓存,通过《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许可证许可。