laravel广播使用笔记,基于laravel-websocket扩展包

阅读文档

Laravel 广播系统文档

laravel-websockets文档

安装需要的用的依赖

//websocket服务端
composer require beyondcode/laravel-websockets

//发布数据迁移文件
php artisan vendor:publish --provider="BeyondCode\LaravelWebSockets\WebSocketsServiceProvider" --tag="migrations"

//发布配置文件
php artisan vendor:publish --provider="BeyondCode\LaravelWebSockets\WebSocketsServiceProvider" --tag="config"

//安装队列管理面板 或者 使用php artisan queue:work也可以
composer require laravel/horizon
php artisan horizon:install

//执行迁移
php artisan migrate

//前端依赖
npm install laravel-echo pusher-js

修改配置

.env 配置

//广播驱动设置为pusher
BROADCAST_DRIVER=pusher

//队列驱动改为redis
QUEUE_CONNECTION=redis

//使用laravel-websockets做后端,pusher配置随便填写
PUSHER_APP_ID=yangliuan
PUSHER_APP_KEY=yangliuan
PUSHER_APP_SECRET=yangliuan
//注意一定要注释这行,否则laravel-websockets不生效
#PUSHER_APP_CLUSTER=mt1

//websocket端口号
LARAVEL_WEBSOCKETS_PORT=6001

config/app.php 配置

取消如下Provider的注释

/*
 * Application Service Providers...
 */
...
// App\Providers\BroadcastServiceProvider::class,

config/broadcasting.php 配置

按如下修改

'connections' => [

        'pusher' => [
            'driver' => 'pusher',
            'key' => env('PUSHER_APP_KEY'),
            'secret' => env('PUSHER_APP_SECRET'),
            'app_id' => env('PUSHER_APP_ID'),
            'options' => [
                'cluster' => env('PUSHER_APP_CLUSTER'),
                 //本地开发关闭安全连接
                'useTLS' => false,
                //本地host配置
                'host' => '127.0.0.1',
                //端口
                'port' => env('LARAVEL_WEBSOCKETS_PORT', 6001),
                //协议
                'scheme' => 'http',
            ],
        ],

config/websockets.php

'apps' => [
        [
            'id' => env('PUSHER_APP_ID'),
            'name' => env('APP_NAME'),
            'key' => env('PUSHER_APP_KEY'),
            'secret' => env('PUSHER_APP_SECRET'),
            'path' => env('PUSHER_APP_PATH'),
            'capacity' => null,
            //是否开启客户端发送消息
            'enable_client_messages' => false,
            //是否开启统计
            'enable_statistics' => true,
        ],
    ],

测试案例场景

使用laravel excel 队列导出文件后,自动提示并下载,使用公共频道 demo地址

后端代码

注册频道

route/channels.php 文件,可以自定义频道名称

Broadcast::channel('excel', function () {
    return true;
});

创建ExcelExportCompletedEvent事件

php artisan make:event ExcelExportCompletedEvent

<?php
use Illuminate\Broadcasting\Channel;//公共频道
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PresenceChannel; //存在频道可以加入和离开,需要授权
use Illuminate\Broadcasting\PrivateChannel; //私有频道,需要授权
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

//注意事件一定要实现ShouldBroadcast接口
class ExcelExportCompletedEvent implements ShouldBroadcast
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

    //public属性会自动转换为广播数据
    //自定义广播数据  文档 https://learnku.com/docs/laravel/9.x/broadcasting/12223#b2f5d1
   
    public $excel_path;

    public $disk;

    /**
     * Create a new event instance.
     *
     * @return void
     */
    public function __construct(string $excel_path, string $disk)
    {    
        //文件路径
        $this->excel_path = $excel_path;
        //磁盘
        $this->disk = $disk;
    }

    /**
     * 监听频道
    *  监听自己的定义频道名称
     * 
     *
     * @return \Illuminate\Broadcasting\Channel|array
     */
    public function broadcastOn()
    {
        return new Channel('excel');
    }
}

创建导出文件 ExcelDemoPictureQueryExport 细节略过详情看 laravel excel文档

创建通知队列,用于触发时间

php artisan make:job ExcelNotifyJob

class ExcelNotifyJob implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    protected $attach;

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

    /**
     * Execute the job.
     *
     * @return void
     */
    public function handle()
    {
        //发送广播通知
        ExcelExportCompletedEvent::dispatch($this->attach['file_name'], $this->attach['disk']);
    }
}

控制器代码

/**
 * 字段导出图片 使用队列 并接受广播通知
 *
 * @param Request $request
 * @return void
 */
public function queueImages(Request $request)
{
  $file_name = 'excel-demo-'.date('YmdHis').\mt_rand(100000, 999999).'.xlsx';
  $disk = 'public';
  Excel::queue(new ExcelDemoPictureQueryExport(), $file_name, $disk)
  //导出成功后,使用任务链调用excel通知job
  //DOC:https://learnku.com/docs/laravel/8.5/queues/10395#dispatching-jobs
  //DOC:https://docs.laravel-excel.com/3.1/exports/queued.html#appending-jobs
  ->chain([
      new ExcelNotifyJob(compact('file_name', 'disk'))
  ]);

  return response()->json();
}

//下载文件方法
public function store(Request $request)
{
    $request->validate([
       'storage_path' => 'bail|required|string',
       'disk' => 'bail|nullable|string',
    ]);
    $realPath = Storage::disk($request->input('disk') ?? 'public')
       ->path($request->input('storage_path'));

    return response()->download($realPath)->deleteFileAfterSend();
}

前端代码

使用 Laravel Jetstream Inertia.js  构建

封装laravel-echo.js

import Echo from 'laravel-echo';

window.pusher = require('pusher-js');

const echo = new Echo({
    broadcaster: 'pusher',
    key: 'yangliuan',
    wsHost: window.location.hostname,
    wsPort: 6001,
    forceTLS: false,
    enabledTransports: ['ws', 'wss'],
})

export { echo }

Excel.vue

<template>
 <app-layout title="Dashboard">
  ...
  <a href="#" @click="queueImagesClick">
  <div class="mt-3 flex items-center text-sm font-semibold text-indigo-700">
  <div>队列导出图片并用广播接受通知</div>
  <div class="ml-1 text-indigo-500">
  <svg viewBox="0 0 20 20" fill="currentColor" class="w-4 h-4"><path fill-rule="evenodd" d="M10.293 3.293a1 1 0 011.414 0l6 6a1 1 0 010 1.414l-6 6a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-4.293-4.293a1 1 0 010-1.414z" clip-rule="evenodd"></path></svg>
 </div>
 </div>
 </a>
 </app-layout>
</template>

<script>
 ...
 //导入laravel echo 
 import { echo } from '@/laravel-echo'

 export default defineComponent({
    components: {
       AppLayout,
       JetApplicationLogo,
       Link
    },
    created() {
      //监听公共频道excel,响应下载excel文件
       echo.channel('excel')
           .listen('ExcelExportCompletedEvent', (e) => {
               alert('ExcelExportCompletedEvent')
               this.downloadExcel(e.excel_path,e.disk)
               console.log(e);
           })
    },
    methods: {
       //下载方法
       downloadExcel(excel_path,disk) {
          const download_url =  '/api/files/download?storage_path=' + excel_path + '&disk=' + disk
          window.open(download_url)
       },
      //点击时间调用队列导航图片接口
       queueImagesClick() {
          axios.post('/api/excel/export/queue-images').then( response => {
             console.log(response)
          })
       }
  }
})
</script>
//开启websocket服务
php artisan websockets:serve

//开启horizon队列
php artisan horizon

演示

授权频道案例

nginx代理websocket

laravel websocket文档

websocket单独使用一个域名不和http接口共享域名,配置如下

server {
    listen 80;
    server_name websocket.exhibition.demo;

    location / {
        proxy_pass             http://127.0.0.1:6001;
        proxy_read_timeout     60;
        proxy_connect_timeout  60;
        proxy_redirect         off;

        # Allow the use of websockets
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_cache_bypass $http_upgrade;
    }
}

第二种方式websocket和http接口公用一个域名

map $http_upgrade $type {
  default "";
  websocket "ws";
}

server {
    listen 80;
    server_name api2.exhibition.demo;
    root /home/yangliuan/Code/Php/business-logic/laravel-exhibition/public;

    add_header X-Frame-Options "SAMEORIGIN";
    add_header X-XSS-Protection "1; mode=block";
    add_header X-Content-Type-Options "nosniff";

    index index.php;
    charset utf-8;

    location = /favicon.ico {
        access_log off; log_not_found off;
    }

    location = /robots.txt {
        access_log off; log_not_found off;
    }
   
   //http
    location /  {
        try_files $uri $uri/ /index.php?$query_string;
    }
 
   //websocket
    location @ws  {
        proxy_pass             http://127.0.0.1:6001;
        proxy_set_header Host  $host;
        proxy_read_timeout     60;
        proxy_connect_timeout  60;
        proxy_redirect         off;

        # Allow the use of websockets
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_cache_bypass $http_upgrade;
    }

    location ~ [^/]\.php(/|$) {
        fastcgi_pass unix:/dev/shm/php-cgi.sock;
        fastcgi_index index.php;
        include fastcgi.conf;
    }

    location ~ /\.(?!well-known).* {
        deny all;
    }
}

参考

websocket实现手机扫码登陆

PHP开发生态 常用笔记

软件包生态介绍

其实PHP有三套软件包生态:PEARPECLComposer

当然PEAR可以忽略不计:作为软件包,其托管的代码基本移植成了Composer包;

作为拓展安装工具,pecl是pear的别名,直接用pecl就完事。

所以PHP软件生态主要是两套:PECL和Composer。

PECL 扩展库 官方手册

开发辅助工具

Composer使用经验总结

PHP辅助工具扩展 使用笔记

phpstan 静态检测工具

PHP-CS-Fixer 编码格式化修复工具

laravel-ide-helper laravel IDE工具

PHP

m9rco/algorithm-php php实现算法

Guzzle Http客户端

Intervention/image 最好用图片工具

所有很棒的 PHP 功能的直接总结

guanguans/notify 项目出现异常时,发送通知到钉钉等…

ezyang/htmlpurifier XSS 过滤

php-casbin 权限控制

BaconQrCode 二维码工具

vinkla/hashids 唯一hashid生成工具

ratchetphp/Ratchet websocket库 php5.4起,适合旧版本

文件压缩

https://github.com/maennchen/ZipStream-PHP zip压缩

https://github.com/alchemy-fr/Zippy 支持.zip,.tar,.tar.gz,tar.bz2,

https://github.com/wapmorgan/UnifiedArchive 支持zip,rar,7z,tar

框架选择总结

业务快速开发用laravel,性能选webman,想用协程选hyperf,异步用ReactPHP

Laravel

php-fpm模式

PHP生态最好的web开发框架,社区活跃,简单实用,具备敏捷开发特质,扩展包和解决方案多,本身集成功能特别多。start数最多。

缺点性能比较差

适合业务复杂或对性能要求不高的场景,提升性能需要单独处理。

什么是laravel

LX1 Laravel / PHP 扩展包视频教程

框架方法API文档 https://laravel.com/api/8.x/

laravel编码最佳实践

laravel编码技巧

laravel-boilerplate laravel样板项目

laravel-devinit 项目初始化工具

stechstudio/laravel-zipstream ZipStream-PHP的laravel封装

yangliuan/generator crud代码生成器

laravel-permission RBAC权限扩展包

php-casbin/laravel-authz 权限控制

laravel-enum enum类型支持

mews/purifier XSS 过滤

overtrue/laravel-lang 语言包支持

spatie/laravel-translatable 数据库多语言包

Astrotomic/laravel-translatable 数据库多语言包

jenssegers laravel-mongodb mongodb ORM 支持

barryvdh/laravel-snappy html转pdf 使用 wkhtmltopdf

barryvdh/laravel-dompdf html转pdf 使用 dompdf

simple-qrcode 二维码生成工具 在线文档

Eloquent Filter – 模型关联查询过滤

Maatwebsite/Laravel-Excel excel 导入导出

yajra/laravel-oci8 Oracle DB driver for Laravel 4|5|6|7|8 via OCI8

protonemedia/laravel-ffmpeg ffmpeg

Laravel-Phone– 全球手机号和电话验证

laravel-search 搜索扩展包 驱动支持  ElasticsearchAlgolia, and ZendSearch 

laravel-geoip 根据访问者的 IP 地址确定网站访问者的位置

laravel-queue-rabbitmq laravel rabbitmq驱动

 l5-repository Laravel 5 – Repositories to abstract the database layer

超大文件 xml/JSON/CSV 读取解析的方案 参考

阿里云文件存储 laravel-filesystem-oss

七牛云文件存储 flysystem-qiniu

Xethron/migrations-generator 数据库转换迁移文件 <= 5.5

kitloong/laravel-migrations-generator 数据库转换迁移文件 >=5.5

Laravel集成的Faker数据模拟 参考文章 Github https://fakerphp.github.io/

laravel-exception-notify 多种通道的 laravel 异常通知(钉钉群机器人、飞书群机器人、Server 酱、企业微信群机器人、息知)

laravel-modules laravel-plugin 插件机制

codestudiohq / laravel-totem laravel 定时任务管理仪表盘

性能加速组件

laravel-s laravel-swoole Laravel Octane

symfony

Symfony 是一个用于 Web 和控制台应用程序的 PHP框架和一组可重用的 PHP 组件。企业级开发框架。引领了很多php业界标准,很多框架的底层组件都使用symfony。

累计下载上亿次

ThinkPHP

基础入门框架,简单实用尤其是3.2.3版本,生态

php-casbin/think-authz 权限控制

好用的CMS框架 https://www.thinkcmf.com/

Yii

没用过不做介绍

Swoole

Swoole 是一个使用 C++ 语言编写的基于异步事件驱动和协程的并行网络通信引擎(PHP底层扩展),为 PHP 提供协程高性能网络编程支持。提供了多种通信协议的网络服务器和客户端模块,可以方便快速的实现 TCP/UDP服务高性能WebWebSocket服务物联网实时通讯游戏微服务等,使 PHP 不再局限于传统的 Web 领域。

CLI模式常驻内存运行

API发展由swoole公司维护和决定. 社区产生分裂 国内原开发者为主swoole 国外开发者为主 openswoole

官方文档 社区

学习成本较高,从业务开发和就业角度(岗位少)来说,不如学golang。对于掌握了底层知识的人来说,学习起来很快。岗位较少。

缺点对传统php-fpm模式下的原有生态组件有不兼容情况,需要重新造轮子

通过开启一键协程功能,对php阻塞函数进行Hook,来达到兼容目的。

使用问题

学习Swoole要掌握的知识

学习Swoole 其中编程须知必须要看

退出终止和阻塞函数不能使用,静态变量非必要不能使用,会增加内存溢出风险

用户案例 大厂用的多

hyperf

简单对比测试了几个基于 swoole 的框架

Workerman (工人)

高性能php容器,网络引擎框架,性能非常高,php框架中 benchmark (基准) 排行第一 。生态相对冷门,对底层基础有要求,很多东西需要自己构建,没有开箱即用的现成轮子,需要其他第三方组件

CLI模式常驻内存运行

workerman 基础容器框架

gatewayworker 基于Workerman开发的一个项目框架 用于快速开发TCP长连接应用,例如app推送服务端、即时IM服务端、游戏服务端、物联网、智能家居等等

webman 基于workerman开发的高性能HTTP服务框架。webman用于替代传统的php-fpm架构,提供超高性能可扩展的HTTP服务。你可以用webman开发网站,也可以开发HTTP接口或者微服务。适合中小型对性能要求高项目

以最小内核提供最大的扩展性与最强的性能。

webman仅提供最核心的功能(路由、中间件、session、自定义进程接口)。其余功能全部复用composer生态,这意味着你可以在webman里使用最熟悉的功能组件,例如在数据库方面开发者可以选择使用Laravel的illuminate/database,也可以是ThinkPHP的ThinkORM,还可以是其它组件如Medoo。在webman里集成他们是非常容易的事情。

workerman原理

ReactPHP

ReactPHP是PHP中用于事件驱动编程的底层库。它的核心是一个事件循环,在此基础上它提供了底层实用程序,例如:流抽象、异步DNS解析器、网络客户端/服务器、HTTP客户端/服务器以及进程间通信。第三方库可以使用这些组件创建异步网络客户端/服务器等。

官方文档

Yar

Yar(yet another RPC framework, 教主问我为啥都是Ya打头, 呵呵, 因为这样名字好起)是我在3个多月前, 为了解决一个实际的问题, 而开发的一个PHP扩展的, RPC框架, 和现有的RPC框架(xml-rpc, soap)不同, 这是一个轻量级的框架, 支持多种打包协议(msgpack, json, php), 并且最重要的一个特点是, 它是可并行化的..

github地址 鸟哥博客 php官方文档

Yaf

PHP framework  written in c and built as a PHP extension.

github地址 php官方文档

Yaf and Phalcon, which is faster?

Phalcon

Phalcon is an open source web framework delivered as a C extension for the PHP language providing high performance and lower resource consumption.

github地址 官网 官方文档

参考

chiraggude/awesome-laravel

吐槽一下PHP的生态

Mysql FAQ

隐式类型转换

例如 pid 字段类型是 int,我们 where pid = “1”,这样就会触发隐式类型转换。字段为数字类型,字符串数字转换成数字时,并不会导致索引失效如下图

当where查询操作符**左边为字符类型**时发生了隐式转换,那么会导致索引失效,造成全表扫描效率极低。

name为字符串类型,传入了数字类型001 转换后索引失效

导致查询结果不符合预期

类型隐式转换规则

当操作符与不同类型的操作数一起使用时,会发生类型转换以使操作数兼容,某些转换是隐式发生的

如果不与数字比较,则将十六进制值视为二进制字符串


如果参数之一是a TIMESTAMP或 DATETIME column,而另一个参数是常量,则在执行比较之前,该常量将转换为时间戳。这样做是为了使ODBC更友好。对于的参数,此操作未完成 IN()。为了安全起见,在进行比较时请始终使用完整的日期时间,日期或时间字符串。例如,要在BETWEEN与日期或时间值一起使用时获得最佳结果 ,请使用CAST()将值显式转换为所需的数据类型。


一个或多个表中的单行子查询不被视为常量。例如,如果子查询返回要与DATETIME 值进行比较的整数,则比较将作为两个整数进行。整数不转换为时间值。要将操作数作为DATETIME值进行比较 ,请使用 CAST()将子查询值显式转换为DATETIME


如果参数之一是十进制值,则比较取决于另一个参数。如果另一个参数是十进制或整数值,则将参数作为十进制值进行比较;如果另一个参数是浮点值,则将参数作为浮点值进行比较。


在所有其他情况下,将参数作为浮点数(实数)进行比较。例如,将字符串和数字操作数进行比较,将其作为浮点数的比较。

条件的由字符转为浮点时候

不以数字开头的字符串都将转换为0。如’abc’、’a123bc’、’abc123’都会转化为0;
以数字开头的字符串转换时会进行截取,从第一个字符截取到第一个非数字内容为止。比如’123abc’会转换为123,’012abc’会转换为012也就是12,’5.3a66b78c’会转换为5.3,其他同理。

避免规则,通过程序处理,尽量使类型与数据库字段保持一致

参考

mysql-8.隐式转换导致索引失效或查出不符合where条件结果

类型转换官方文档

Laravel多态关联的应用案例

多对多 多态

需求如图用户关注不同的实体
用户表
专家表
主播表
作者表
关注中间表
//用户模型
class User extends Authenticatable
{
    use HasFactory, Notifiable, HasApiTokens, DateFormat, Filterable;

    protected $table = 'users';
    
    //关注的用户
    public function followUser()
    {
        return $this->morphedByMany($this, 'followable', 'followable', 'user_id', 'followable_id')
            ->withTimestamps();
    }
    //关注的专家
    public function followExpert()
    {
        return $this->morphedByMany('App\Models\Expert', 'followable', 'followable', 'user_id', 'followable_id')
            ->withTimestamps();
    }
    //关注的主播
    public function followAnchors()
    {
        return $this->morphedByMany('App\Models\Anchors', 'followable', 'followable', 'user_id', 'followable_id')
            ->withTimestamps();
    }
    //关注的作者
    public function followAuthors()
    {
        return $this->morphedByMany('App\Models\Authors', 'followable', 'followable', 'user_id', 'followable_id')
            ->withTimestamps();
    }
    //关注者,即主动关注用户的人
    public function followerUser()
    {
        return $this->morphToMany($this, 'followable', 'followable', 'followable_id', 'user_id')
            ->withTimestamps();
    }
}
//专家模型
class Expert extends BaseModel
{
    protected $table = 'expert';
    //关注者,即主动关注专家的人
    public function followerUser()
    {
        return $this->morphToMany('App\Models\User', 'followable', 'followable', 'followable_id', 'user_id')
            ->withTimestamps();
    }
}
//主播模型
class Anchors extends BaseModel
{
    protected $table = 'anchors';
    //关注者,即主动关注主播的人
    public function followerUser()
    {
        return $this->morphToMany('App\Models\User', 'followable', 'followable', 'followable_id', 'user_id')
            ->withTimestamps();
    }
}
//作者模型
class Authors extends BaseModel
{
    protected $table = 'authors';
    //关注者,即主动关注作者的人
    public function followerUser()
    {
        return $this->morphToMany('App\Models\User', 'followable', 'followable', 'followable_id', 'user_id')
            ->withTimestamps();
    }
}
//控制器代码
class FollowController extends Controller
{
  //关注/取消
  public function toggle(Request $request)
 {
    $request->validate([
         'followable_id'=>'bail|required|integer',
        'followable_type'=>'bail|required|string|in:user,expert,authors,anchors'
    ]);
    //利用可变方法动态创建模型对象
    $type_model = "\App\Models\\".ucfirst($request->input('followable_type'));
    $follow = $type_model::findOrFail($request->input('followable_id'));
    //调用关注人关联和toggle方法动态切换关注和取消关注
    $result = $follow->followerUser()->toggle($request->user()->id);

    if ($result['attached']) {
        $status = 1;
    } else {
        $status = 0;
    }

    return response()->json(compact('status'));
 }

//关注列表
public function index(Request $request)
 {
    $request->validate([
      'followable_type'=>'bail|required|string|in:user,expert,authors,anchors'
    ]);
    //根据类型可变方法动态加载我的关注关联,实现4个类型列表
    $followable_funname = 'follow'.ucfirst($request->input('followable_type'));
    $builder = $request->user()->$followable_funname();

    if ($request->input('followable_type') == 'user') {
        $builder->select('users.id', 'name', 'avatar');
    } elseif ($request->input('followable_type') == 'expert') {
        $builder->select('expert.id', 'name', 'nickname', 'photo', 'tags')
                ->where('is_show', 1);
    } elseif ($request->input('followable_type') == 'authors') {
        $builder->select('authors.id', 'authors.user_id', 'introduction')
                ->with([
                    'user'=>function ($query) {
                        $query->select('id', 'name', 'avatar');
                    }
                ])
                ->where('status', 1);
    } elseif ($request->input('followable_type') == 'anchors') {
        $builder->select('anchors.id', 'anchors.user_id', 'games_type')
                ->with([
                    'user'=>function ($query) {
                        $query->select('id', 'name', 'avatar');
                    }
                ])
                ->where('status', 1);
   }
   //按关注时间排序
   $followed = $builder->orderBy('followable.created_at', 'desc')
           ->forPage($request->input('page', 1), $request->input('per_page', 1))
           ->get();

   return $followed;
}

}

这种抽象的写法可以减少接口数量,要看具体需求是否合适

Laravel项目迁移时,文件存储软链接更新问题

使用laravel本地存储文件时,通常存放到public磁盘,即根目录下storage/app/public

执行命令 创建软链接后才可以通过公共访问文件

php artisan storage:link

laravel 7以上自定义软链接目录

项目迁移更换服务器后,磁盘的挂载目录变了,导致软链接路径不能生效了需要更新软连接

方法一

通过ln命令更新

ln –snf  [新的源文件或目录]  [目标文件或目录]

方法二

使用php artisan storage:link命令重新生成,首先要删除原先的软链接目录,删除软链接时一定要注意路径后边不能带/

cd /laravel根目录/public
rm -rf storage 正确的删除软链接方式
rm -rf storage/ 错误的删除软链接方式,会删除软链接对应的真实目录导致文件丢失

Laravel 响应文件

以实时响应二维码为例

public function qrcode($activity_id, $invite)
    {   
        //二维码内容
        $activity_url = config('app.activity_url') . '?' . http_build_query(['id' => $activity_id, 'invite' => $invite]);
        //二维码图片文件的二进制数据
        $qrcodeStr = Qrcode::format('png')->size(80)->generate($activity_url);
        //直接响应文件,并设置对应的响应头
        return response($qrcodeStr)->withHeaders([
            'Access-Control-Allow-Origin' => '*',
            'Access-Control-Allow-Headers' => '*',
            'Access-Control-Allow-Methods' => 'GET,POST,PUT,DELETE,PATCH,OPTIONS',
            'Content-Type' => 'image/png;charset=utf-8',
            'Content-Disposition' => 'inline;filename="' . $invite . '.png"'
        ]);
    }

框架实现原理,如上图所示,使用Symfony 组件响应字符串数据,就相当于原生php的

header(....);//设置文件响应头
echo $bindata;//输出文件的二进制数据字符串

Laravel 在SLB下获取客户端IP和正确加载Https访问样式

如果项目部署在云服务负载均衡下,会导致无法获取正确的客户端IP地址

解决方案

参考文档https://learnku.com/docs/laravel/8.x/requests/9369#453bc9

修改 App\Http\Middleware\TrustProxies 中间件 信任所有代理#

如果你使用 Amazon AWS 或其他的「云」的负载均衡服务,你可能不知道负载均衡器的实际 IP 地址。在这种情况下,你可以使用 * 来信任所有代理:
/**
 * 此应用的信任代理
 *
 * @var string|array
 */
protected $proxies = '*';

上述配置还可以解决,telescope和horizon在SLB下通过https访问无法正确加载样式和js问题

获取IP其它解决方案

参考https://help.aliyun.com/document_detail/54007.html?spm=a2c4g.11186623.6.933.5b203253t8UrIO

背景信息

七层负载均衡(HTTP/HTTPS协议)服务需要对应用服务器进行配置,然后使用X-Forwarded-For的方式获取客户端的真实IP地址。真实的客户端IP存放在HTTP头部的X-Forwarded-For字段,格式如下:

X-Forwarded-For: 用户真实IP, 代理服务器1-IP, 代理服务器2-IP,...

当使用此方式获取客户端真实IP时,获取的第一个地址就是客户端真实IP。

配置Nginx服务器

  1. 执行如下命令,安装http_realip_module。 wget http://nginx.org/download/nginx-1.0.12.tar.gz tar zxvf nginx-1.0.12.tar.gz cd nginx-1.0.12 ./configure --user=www --group=www --prefix=/alidata/server/nginx --with-http_stub_status_module --without-http-cache --with-http_ssl_module --with-http_realip_module make make install kill -USR2 `cat /alidata/server/nginx/logs/nginx.pid` kill -QUIT `cat /alidata/server/nginx/logs/ nginx.pid.oldbin`
  2. 执行如下命令,打开nginx.conf文件。vi /alidata/server/nginx/conf/nginx.conf
  3. 在以下配置信息后添加新的配置字段和信息。 fastcgi connect_timeout 300; fastcgi send_timeout 300; fastcgi read_timeout 300; fastcgi buffer_size 64k; fastcgi buffers 4 64k; fastcgi busy_buffers_size 128k; fastcgi temp_file_write_size 128k;需要添加的配置字段和信息为: set_real_ip_from IP_address; real_ip_header X-Forwarded-For;说明 如果您要获取代理服务器的地址,可以将代理服务器的网段添加到set_real_ip_from <IP_address>,如负载均衡的IP地址段100.64.0.0/10(100.64.0.0/10 是阿里云保留地址,其他用户无法分配到该网段内,不会存在安全风险)和高防IP地址段。多个IP地址段用逗号分隔。
  4. 执行如下命令,重启Nginx。/alidata/server/nginx/sbin/nginx -s reload

然后获取请求头

//原生
$_SERVER['X-Forwarded-For'];
//laravel
$request->header('X-Forwarded-For');

HTTP监听访问正常但是HTTPS监听访问网址不加载样式

https://help.aliyun.com/document_detail/178368.htm?spm=a2c4g.11186623.2.17.116f7503fHVPRT

【漏洞预警】Laravel <= 8.4.2 Debug模式 _ignition 远程代码执行漏洞

https://help.aliyun.com/noticelist/articleid/1060782748.html

2021年1月13日,阿里云应急响应中心监控到国外某安全研究团队披露了Laravel <= 8.4.2 存在远程代码执行漏洞。

漏洞描述

Laravel 是一个免费的开源 PHP Web 框架,旨在实现的Web软件的MVC架构。2021年1月13日,阿里云应急响应中心监控到国外某安全研究团队披露了 Laravel <= 8.4.2 存在远程代码执行漏洞。当Laravel开启了Debug模式时,由于Laravel自带的Ignition功能的某些接口存在过滤不严,攻击者可以发起恶意请求,通过构造恶意Log文件等方式触发Phar反序列化,从而造成远程代码执行,控制服务器。漏洞细节已在互联网公开。阿里云应急响应中心提醒 Laravel 用户尽快采取安全措施阻止漏洞攻击。

影响版本

Laravel 框架 < 8.4.3

facade ignition 组件 < 2.5.2

安全版本

Laravel 框架 >= 8.4.3

facade ignition 组件 >= 2.5.2

安全建议

建议将 Laravel 框架升级至8.4.3及其以上版本,或者将 facade ignition组件升级至 2.5.2 及其以上版本。

相关链接

https://github.com/facade/ignition/pull/334

https://www.ambionics.io/blog/laravel-debug-rce

Laravel Test 采坑记录

laravel 的单元测试(tests/Unit目录下),不支持很多内置函数,适合测试不依赖laravel方法的独立方法。

否则会报错,例如在一个job中引用了config(‘xxxx’),执行test时会报错。

将测试方法放到tests/Feature 中,运行成功,因此laravel unit测试里的方法适合独立的自定义方法,并且没有依赖laravel本身的方法。

PHP如何获取HTTP请求(内容)

参考

写这篇文章的起因,逛论坛读到一位博主的成长感悟,说他面试,被一个问题卡住了。PHP接受GET,POST请求分别$_GET,$_POST或$_REQUEST ,那么PHP如何接受PUT,PATCH,DELETE,OPTIONS请求。

引申问题PHP如何处理(接收)HTTP请求?

前置知识只是HTTP请求方法有哪些?https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Methods

HTTP1.0: GET POST HEAD
HTTP1.1: GET POST HEAD OPTIONS PUT PATCH DELETE TRACE 
HTTP2.0: GET POST HEAD OPTIONS PUT PATCH DELETE TRACE

PHP原生方法如何获取请求参数

$_GET,$_POST,$_REQUEST,file_get_content('php://input')

GET请求

GET query param $_GET $_REQUEST 可以获取

GET x-www-form-urlencoded php://input 可以获取

GET Form-data php://input 可以获取

GET application/json php://input 可以获取

POST请求

POST query param $_GET 和 $_REQUEST 可以获取

POST x-www-form-urlencoded $_POST $_REQUEST php://input 可以获取

POST Form-data $_POST $_REQUEST 可以获取

POST application/json php://input 可以获取

PUT 请求

PUT query-param $_GET 和 $_REQUEST 可以获取

PUT x-www-form-urlencoded php://input 可以获取

PUT form-data php://input 可以获取

PUT application/json php://input 可以获取

PATCH 请求

PATCH query-param $_GET 和 $_REQUEST 可以获取

PATCH x-www-form-urlencoded php://input 可以获取

PATCH form-data php://input 可以获取

PATCH application/json php://input 可以获取

DELETE 请求

DELETE query-param $_GET 和 $_REQUEST 可以获取

DELETE x-www-form-urlencoded php://input 可以获取

DELETE form-data php://input 可以获取

DELETE application/json php://input 可以获取

Laravel框架如何处理HTTP请求

支持的请求方法和数据交互类型

请求类型支持的请求方法说明
query paramGET,POST,PUT,DELETE,PATCH,OPTIONS查询字符串, 即url ? 后边的参数&和=拼接
url带数值GET,POST,PUT,DELETE,PATCH,OPTIONS通过/分割的 示例/xxx.com/user/1 1就是参数值通过/分割的 示例/xxx.com/user/1 1就是参数值
form-dataPOSTmultipart/form-data 支持二进制数据上传文件必须使用此类型
x-www-form-urlencoded (form)POST,PUT,DELETE,PATCH,OPTIONSapplication/x-www-form-urlencoded 数据被编码成以 ‘&’ 分隔的键-值对
appliction/jsonGET,POST,PUT,DELETE,PATCH,OPTIONSjson类型

获取方法

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class UserController extends Controller
{
    /**
     * 存储一个新用户
     *
     * @param  Request  $request
     * @return Response
     */
    public function store(Request $request,$id)
    {   
        //接受所有类型参数,无法获取上传文件
        $name = $request->input('name');
        //仅能接受查询字符串参数,?号后的参数
        $name = $request->query('name');
        //接受所有类型参数,包含上传文件,动态属性获取,触发__get()魔术方法
        $name = $request->name;
        //获取上传文件
        $file = $request->file('file');
        //同input是底层Symfony提供的方法,无法获取上传文件
        $name = $request->get('name');
        //获取路由参数,直接访问注入的$id变量
        dump($id);
        //获取原始输入数据symfony提供的方法,等于原生php的file_get_content('php://input')
        $request->getContent() 
    }
}

原理和知识总结

  • $_GET 可以获取所有类型的query param(url传参数)
  • php://input 可以获取所有请求Body 的内容, 除post请求的form-data
  • $_POST 可以获取POST 请求的 form-data 和 x-www-form-urlencoded

laravel使用了symfony的HTTP请求类获取去请求,底层还是通过PHP超全局变量来获取请求参数

Symfony\Component\HttpFoundation\Request
/**
     * Creates a new request with values from PHP's super globals.
     *
     * @return static
     */
    public static function createFromGlobals()
    {
        //使用php超全局变量获取请求数据
        $request = self::createRequestFromFactory($_GET, $_POST, [], $_COOKIE, $_FILES, $_SERVER);

        if ($_POST) {
            $request->request = new InputBag($_POST);
        } elseif (0 === strpos($request->headers->get('CONTENT_TYPE'), 'application/x-www-form-urlencoded')
            && \in_array(strtoupper($request->server->get('REQUEST_METHOD', 'GET')), ['PUT', 'DELETE', 'PATCH'])
        ) {
            parse_str($request->getContent(), $data);
            $request->request = new InputBag($data);
        }

        return $request;
    }