Laravel 使用


Laravel 使用


引言

Laravel 是一个 Web 应用框架, 有着表现力强、语法优雅的特点。Laravel 致力于提供出色的开发体验, 同时提供强大的特性,例如完全的依赖注入,富有表现力的数据库抽象层,队列和计划任务,单元和集成测试等等。

源码地址:https://github.com/laravel/laravel

Laravel 官网:https://laravel.com/

Laravel 学院 - Laravel 6 中文文档:https://laravelacademy.org/books/laravel-docs-6

Laravel 中文网: http://laravel.p2hp.com/

Laravel 中文网 - Laravel 6.*版 中文使用文档: http://laravel.p2hp.com/cndocs/6.x/mixhttps://learnku.com/docs/laravel/6.x

版本

Version   PHP (*)    Release              Bug Fixes Until      Security Fixes Until
6 (LTS)  7.2 - 8.0   September 3rd, 2019  January 25th, 2022   September 6th, 2022
7        7.2 - 8.0   March 3rd, 2020      October 6th, 2020    March 3rd, 2021
8        7.3 - 8.1   September 8th, 2020  July 26th, 2022      January 24th, 2023
9        8.0 - 8.1   February 8th, 2022   August 8th, 2023     February 6th, 2024
10       8.1 - 8.3   February 14th, 2023  August 6th, 2024     February 4th, 2025
11       8.2 - 8.3   March 12th, 2024     September 3rd, 2025  March 12th, 2026
12       8.2 - 8.3   Q1 2025              Q3 2026              Q1 2027

学习

参考:20个最佳Laravel免费和付费教程资源 https://www.wbolt.com/laravel-tutorial.html

实现伪多进程

不使用pcntl,pthreads,swoole的前提下,Laravel 如何实现伪多进程

一、前言

众所周知,多进程/多线程可以并行/并发的执行多个任务,提高运行效率。

PHP默认是不支持多进程/多线程的,需要安装pcntl/pthreads扩展来支持。 协程如果不用swoole等框架,那么实现比较复杂。

以上方法均不使用,那么该如何提高程序的运行效率呢?

二、思路

对于耗时的任务, 通常会推送到任务队列中,然后队列消费进程从任务队列中获取任务执行。

一个队列是可以开启多个消费进程的,那么执行任务的效率是比单个进程顺序执行效率多很多的。

如果不需要等待所有任务的执行完成来获取结果的话,其实开启多个队列消费进程已经够了。

如果需要等待所有任务完成才返回结果的操作,比如在定时任务中需要读取Mysql的100条记录,去调用第三方的API, 这个三方API很Low,调用一次需要2s,最终需要生成这100条记录的CSV文件。顺序执行至少需要200s才能完成, 如果有4个队列消费进程, 那么只需要50s左右即可完成。

主要问题在于如何解决进程间通信?因为需要知道这些子任务是否执行完,以及需要知道任务的执行结果。 那么是否可以将使用redis来通信呢?用redis进行计数操作,每个任务执行完将计数器加1, 每个任务的执行结果都放在redis的list/hash中,当计数器的总数等于任务总数的时候, 就可以断定任务已经执行完成,然后取出redis存放的结果,生成csv文件。

三、实现

有实际项目在线上使用,在这里只贴伪代码展示一下实现思路,有兴趣的可以自己实现试试。

定时任务command,这里主要是程序入口, 设置好计数器, 并且等待队列任务执行完成,获取结果。

<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use Illuminate\Foundation\Bus\DispatchesJobs;

/**
 * 定时任务
 */
class ConsoleCommand extends Command
{
    use DispatchesJobs;

    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'The name and signature of the console command';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'The console command description.';

    /**
     * Create a new command instance.
     *
     * @return void
     */
    public function __construct()
    {
        parent::__construct();
    }

    /**
     * Execute the console command.
     *
     * @return int
     */
    public function handle()
    {
        $recordL1st = [1,2,3,4,5,6,7,8,9,10];
        $taskService = app(abstract:TaskService:class);
        $taskService->init(count($recordList));  //初始化计数器,设置任务总数
        
        foreach ($recordL1st as $record)
        {
            //将任务推送到calculate队列中
            CalculateJob:dispatch($record)->onQueue(queue:'calculate');
        }
        
        // 这里等待任务完成
        while ($taskService->isFinish()){
            sleep(seconds:1);
        }
        
        // 获取任务结果
        $result = $taskService->getResult();
    }
}

具体任务Job代码, 主要执行耗时任务,以及执行完成计数器进行加1,需要注意的是异常也要保证计数器+1, 否则任务总数跟执行总数不相等,那么主进程command会卡死。

<?php

namespace App\Jobs;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;

/**
 * 队列消费者
 */
class CalculateJob implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public $record;

    /**
     * Create a new job instance.
     *
     * @return void
     */
    public function __construct($record)
    {
        $this->record = $record;
    }

    /**
     * Execute the job.
     * 执行具体耗时任务
     *
     * @return void
     */
    public function handle()
    {
        try {
            sleep(seconds::2);  //执行任务
            app(abstract:TaskService::class)->addResult($this->job->getJobId(), result::123);  //保存结果到redis
            
            app(abstract:TaskService::class)->do();  //计数器+1
        } catch (\Exception Se) {
            // 记录错误
            app(abstract:TaskService::class)->do();  //需要注意失败计数器也+1,要保证执行总数等于任务总数,不然主进程会卡死。
        }
    }
}

将任务相关信息保存到redis的hash数据结构中,需要保存任务总数的key,用于统计的已执行任务总数的key, 计数可以用时间复杂度为O(1)的hincrby命令。

<?php

class TaskService
{
    protected ShashTableName 'counter:hash';//hash table name
    protected StotalTaskKey 'total_task:count';//total
    protected SexecutedTaskKey 'executed_task:count';//executed
    protected $LockKey='counter:Lock';//锁名称
    
    // 初始化
    public function init(Scount)
    {
        app(abstract:'redis.connection')->hset($this->hashTableName, $this->totaLTaskKey,$count);
    }
    
    // 计数器+1
    public function do()
    {
        do {
            $isLock = app(abstract:'redis.connection')->set($this->LockKey,1,'ex',1,'nx');
            if (sisLock){
                app(abstract:'redis.connection')->hincrby($this->hashTableName,$this->executedTaskKey,1);   
            }
        } while (!$isLock);
    }

    // 结果也可以保存到该hash中, 通过hvals获取结果前先把两个count的key删除即可

    public function isFinish():bool
    {
        $executedCount = app(abstract:'redis.connection')->hget($this->hashTableName,$this->executedTaskKey);
        // 其实定义常量就行了。
        $totalCount = app(abstract:'redis.connection')->hget($this->hashTableName,$this->totalTaskKey);
        
        return $executedCount = $totalCount;
    }

    // 获取最终结果
    public function getResult()
    {
        app(abstract:'redis.connection')->hdel($this->hashTabLeName,$this->totaLTaskKey,$this->executedTaskKey);
        return app(abstract:'redis.connection')->hvals($this->hashTableName);
    }

    // 保存一条结果
    public function addResult($jobId, Sresult)
    {
        app(abstract:'redis.connection')->hset($this->hashTableName,$jobId,$result);
    }
    
}

该代码示例只适合学习使用,如果需要生产使用需要自己处理细节问题。

开启多个任务队列, 可手动开启几个终端手动执行:php artisan queue:work redis --queue=calculate启动多个队列消费进程, 也可以通过supervisor来启动, 通过配置numprocs=8参数来限制进程数量。

Redis事务

Laravel的官方文档中对Redis事务的使用并没有做介绍,https://laravelacademy.org/post/19973 ,所以这里记录一下具体示例。 具体Redis事务的文档,可以参阅 http://doc.redisfans.com/topic/transaction.html

这里举一个 Laravel 框架中 Redis事务 使用的例子:

use Illuminate\Support\Facades\Redis;

......

$uuid = 100001;

try {
    Redis::multi();
    
    Redis::set($uuid, 'uuid:' . $uuid);  
    Redis::hMSet('user:' . $uuid, ['id' => $uuid, 'uuid' => $uuid]]);
    Redis::lPush('uuid_list', $uuid);
    Redis::sAdd('user_id_key', $uuid);

    $results = Redis::exec();
} catch (\Exception $e) {
    Redis::discard();

    throw new \Exception('Redis缓存数据保存失败,' . $e->getMessage());
}

结合 数据库事务 使用 Redis事务 再举个例子:

use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Redis;

......

$uuid = 100001;

DB::beginTransaction();
try {
    // mysql
    DB::insert('insert into users (uuid, content) values (?, ?)', [$uuid, 'uuid:' . $uuid]);

    // Redis
    try {
        Redis::multi();
        
        Redis::set($uuid, 'uuid:' . $uuid);  
        Redis::hMSet('user:' . $uuid, ['id' => $uuid, 'uuid' => $uuid]]);
        Redis::lPush('uuid_list', $uuid);
        Redis::sAdd('user_id_key', $uuid);
    
        $results = Redis::exec();
    } catch (\Exception $e) {
        Redis::discard();
    
        throw new \Exception('Redis缓存数据保存失败,' . $e->getMessage());
    }

    DB::commit();
} catch (\Throwable $e) {
    DB::rollBack();
    throw new \Exception('数据保存失败,' . $e->getMessage());
}

Laravel的数据库事务,可以参阅 https://laravelacademy.org/post/19968

查看SQL执行语句

开发中碰到这样一种情况,你想查看Model查询时具体的SQL执行语句怎么办?

你可以使用 DB::enableQueryLog() 方法,如下:

// 启用查询日志  
DB::enableQueryLog();

// 具体执行逻辑
$model = Mails::query()->select('mails.*');
$model->leftJoin('user as u1', 'mails.to', '=', 'u1.uuid')->where('u1.type', 1);
$records = $model->orderByDesc('mails.id')->paginate(10)->toArray();
$total = $records['total'];
$data = $records['data'];

// 获取查询日志  
$queries = DB::getQueryLog();  

// 打印最后一个查询的 SQL 语句  
if (!empty($queries)) {  
    $lastQuery = end($queries);  
    $sql = $lastQuery['query'];  
    $bindings = $lastQuery['bindings'];  
    $sqlWithBindings = str_replace(array('%', '?'), array('%%', "'" . implode("','", $bindings) . "'"), $sql);  
    echo $sqlWithBindings;  
}

// 输出:select `mails`.* from `mails` left join `user` as `u1` on `mails`.`to` = `u1`.`uuid` where `u1`.`type` = '1' order by `mails`.`id` desc limit 10 offset 0

看一下上面 $queries 的具体内容:

array(2) {
  [0]=>
  array(3) {
    ["query"]=>
    string(118) "select count(*) as aggregate from `mails` left join `user` as `u1` on `mails`.`to` = `u1`.`uuid` where `u1`.`type` = ?"
    ["bindings"]=>
    array(1) {
      [0]=>
      int(1)
    }
    ["time"]=>
    float(162.54)
  }
  [1]=>
  array(3) {
    ["query"]=>
    string(151) "select `mails`.* from `mails` left join `user` as `u1` on `mails`.`to` = `u1`.`uuid` where `u1`.`type` = ? order by `mails`.`id` desc limit 10 offset 0"
    ["bindings"]=>
    array(1) {
      [0]=>
      int(1)
    }
    ["time"]=>
    float(29.69)
  }
}

生成命令行脚本

在 Laravel 6 中,可以通过编写 Artisan 命令行来创建自己的脚本。 Artisan 是 Laravel 的命令行界面,它提供了大量的工具来执行各种常见的开发任务,也可以编写自己的 Artisan 命令来处理自定义任务。

以下是如何编写 Laravel 6 中的自定义 Artisan 命令的基本步骤:

1、创建命令类

使用 Artisan 命令行工具来生成一个新的console命令类:

php artisan make:command YourCommandName

这将在 app/Console/Commands 目录下创建一个新的命令类文件。

2、编辑命令类

打开生成的命令类文件,可以看到 signature 和 description 属性,以及 handle 方法。

signature 属性定义了命令的名称和参数。
description 属性提供了命令的简短描述。
handle 方法是命令的主体,其中包含了你希望执行的代码。

如:

<?php  
 
namespace App\Console\Commands;  
  
use Illuminate\Console\Command;  
  
class YourCommandName extends Command  
{  
    /**  
     * The name and signature of the console command.  
     *  
     * @var string  
     */  
    protected $signature = 'your-command-name {--option=}';  
  
    /**  
     * The console command description.  
     *  
     * @var string  
     */  
    protected $description = 'Command description';  
  
    /**  
     * Create a new command instance.  
     *  
     * @return void  
     */  
    public function __construct()  
    {  
        parent::__construct();  
    }  
  
    /**  
     * Execute the console command.  
     *  
     * @return int  
     */  
    public function handle()  
    {  
        $option = $this->option('option');  
  
        // 在这里编写你的代码逻辑  
        $this->info('Your command has been executed with option: ' . $option);  
    }  
}

3、注册命令

在 app/Console/Kernel.php 文件的 $commands 数组中注册命令,这样 Laravel 就知道要加载它了。

protected $commands = [  
    // ...  
    \App\Console\Commands\YourCommandName::class,  
];

4、运行命令

通过 Artisan 命令行来运行自定义命令:

> php artisan your-command-name --option=value

Inertia 脚手架

官方称Inertia是前后端开发的粘合剂,开发像往常一样,不需要做开发习惯的修改。但实际开发过程中感觉到Inertia还是像框架。

Inertia 是一种构建经典服务器驱动型网络应用程序的新方法。我们称之为现代单体。 Inertia 允许您创建完全由客户端渲染的单页面应用程序,而不会像现代 SPA单页应用编程 那样复杂。Inertia 通过利用现有的服务器端模式来实现这一目标。 Inertia 没有客户端路由,也不需要 API。只需像以往一样构建控制器和页面视图即可! Inertia 与任何后端框架都能完美配合,但它针对 Laravel 进行了微调。

简单来说就是可以在后端项目代码开发中,直接写现在单页面及时响应前端代码,避免了前后端分离成两个项目的弊端,减少了项目维护成本。

官网 https://inertiajs.com/

示例项目,Laravel 结合Vue 开发的CRM项目 https://gitee.com/ibaiyang/pingcrm, 其中使用到了 inertia,php^7.3,laravel^8.0,vue^2.6.11。 在线体验地址 https://demo.inertiajs.com

Jetstream 框架

Laravel Jetstream 是一款设计精美的 Laravel 应用程序入门套件,为下一个 Laravel 应用程序提供了完美的起点。 Jetstream 为应用程序提供登录、注册、电子邮件验证、双因素身份验证、会话管理、通过 Laravel Sanctum 的 API 以及可选的团队管理功能。

官网文档 https://jetstream.laravel.com/introduction.html

鉴于上面原因,可以再向上走一步,直接把Jetstream在Laravel中安装好。项目地址 https://gitee.com/ibaiyang/jetstream1-base, jetstream版本^1.0的一个基础项目,其中使用到了Tailwind CSS、 inertia,php^7.3.0,laravel^8.0。

Laravel Mix

官网文档 https://laravel-mix.com/docs/6.0/what-is-mix

什么是Mix:Webpack是一个令人难以置信的强大的模块,用于准备你的JavaScript和资产的浏览器。 唯一可以理解的缺点是,它需要一个学习曲线。 为了降低学习曲线,对于我们其他人来说Mix是Webpack顶部的一个薄层。它提供了一个简单的、流利的API,用于动态构建WebPack配置。

Basic Laravel Workflow

Let’s review a general workflow that you might adopt for your own projects.

Step 1: Install Laravel

laravel new my-app

Step 2: Install Node Dependencies

By default, Laravel ships with Laravel Mix as a dependency. This means you can immediately install your Node dependencies.

npm install

Step 3: Visit webpack.mix.js

Think of this file as your home base for all front-end configuration.

let mix = require('laravel-mix');

mix.js('resources/js/app.js', 'js').sass('resources/sass/app.scss', 'css');

Using the code above, we’ve requested JavaScript ES2017 + module bundling, as well as Sass compilation.

Step 4: Compilation

If those files don’t exist in your project, go ahead and create them. Populate app.js with a basic alert, and app.scss with any random color on the body tag.

// resources/js/app.js

alert('Hello World');
// resources/sass/app.scss
$primary: red;

body {
    color: $primary;
}

When you’re ready, let’s compile.

npx mix

You should now see two new files within your project’s public directory.

./public/js/app.js
./public/css/app.css

Excellent! Next, let’s get into a groove. It’s a pain to re-run npx mix every time you change a file. Instead, let’s have Mix (and ultimately webpack) watch these files for changes.

npx mix watch

Perfect. Make a minor change to resources/js/app.js and webpack will automatically recompile.

Tip: You may also use mix.browserSync('myapp.test') to automatically reload the browser when any relevant file in your Laravel app is changed.

Step 5: Update Your View

Laravel ships with a static welcome page. Let’s use this for our demo. Update it to:

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="utf-8" />
        <title>Laravel</title>

        <link rel="stylesheet" href="/css/app.css" />
    </head>
    <body>
        <h1>Hello World</h1>

        <script src="/js/app.js"></script>
    </body>
</html>

Run your Laravel app. It works!






参考资料

不使用pcntl/pthreads/swoole的前提下, laravel该如何实现伪多进程? https://mp.weixin.qq.com/s/tzFV5RKxGZI5Typw2K0BJg


返回