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

Laravel crontab 配置问题

参考

Lnmp环境运行时一般会指定用户www运行。因此配置定时任务的时候,也需要使用www用户来运行定时任务,否则会造成laravel生成的日志是其它用户,导致laravel运行报错,没有日志的可写权限。

配置方法

1.获取当前系统PHP的环境变量

执行 env > /tmp/env.output 然后 cat /tmp/env.output

找到PATH

PATH=/usr/local/mysql/bin:/usr/local/php/bin:/usr/local/nginx/sbin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/root/bin

2.设置定时任务 -u 参数指定用户 命令参考 给www用户的crontab 添加环境变量PATH

crontab -u www -e

将PATH添加到crontab的第一行,

换行后将laravel的定时任务代码加上,截图中第二行, 制定了环境变量后,可以只写php 不用写php的完整路径 /usr/local/php/bin/php

3,不能遗漏的关键一步

在home目录下创建对应用户的文件目录,www 并修改用户权限为www 最后重启定时任务

cd home && mkdir www && chown -R www.www www
service crond restart

如果没有该用户的目录,crontab日志会有报错

(CRON) ERROR chdir failed (/home/www): No such file or directory

知识总结

Crontab详细介绍可以头部的参考链接,和《鸟哥linux私房菜-基础学习篇》第16章

任务调度分为两类: 系统任务调度 和 用户任务调度 

系统任务配置/etc/crontab

SHELL=/bin/bash
PATH=/sbin:/bin:/usr/sbin:/usr/bin
MAILTO=root
HOME=/
# For details see man 4 crontabs

# Example of job definition:
# .---------------- minute (0 - 59)
# |  .------------- hour (0 - 23)
# |  |  .---------- day of month (1 - 31)
# |  |  |  .------- month (1 - 12) OR jan,feb,mar,apr ...
# |  |  |  |  .---- day of week (0 - 6) (Sunday=0 or 7) OR sun,mon,tue,wed,thu,fri,sat
# |  |  |  |  |
# *  *  *  *  * user-name  command to be executed

前四行是用来配置crond任务运行的环境变量,

  • 第一行SHELL变量指定了系统要使用哪个shell,这里是bash,
  • 第二行PATH变量指定了系统执行命令的路径,
  • 第三行MAILTO变量指定了crond的任务执行信息将通过电子邮件发送给root用户,如果MAILTO变量的值为空,则表示不发送任务执行信息给用户,
  • 第四行的HOME变量指定了在执行命令或者脚本时使用的主目录。
  • 第五行 前五个星号代表时间, user-name代表执行的用户,command代表执行的命令

crontab – u xxx -e 是用来设置用户系统任务调度的

所有用户定义的crontab文件都被保存在/var/spool/cron目录中。其文件名与用户名一致,使用者权限文件如下

/etc/cron.deny     该文件中所列用户不允许使用crontab命令
/etc/cron.allow    该文件中所列用户允许使用crontab命令
/var/spool/cron/   所有用户crontab文件存放的目录,以用户名命名

注意单独用户的crontab配置需要设置 PATH 并创建对应用户目录

service crond start    # 启动服务
service crond stop     # 关闭服务
service crond restart  # 重启服务
service crond reload   # 重新载入配置
service crond status   # 查看状态

Laravel 踩坑记录之路由与中间件,论规范的重要性

使用一种技术,就要遵循它的规范,尤其是在没有完全了解实现原理的时候,不能随意的DIY否则就会进入坑中

不明原因(可能的原因同事对框架做了修改)导致在控制器构造方方中使用$this->middleware()方法,terminate中间件没有生效,最后在路由中使用->middleware() 方法中间件生效

Laravel 迁移文件 简单总结

参考

简介

数据库迁移就像是数据库的版本控制,可以让你的团队轻松修改并共享应用程序的数据库结构。迁移通常与 Laravel 的数据库结构生成器配合使用,让你轻松地构建数据库结构。如果你曾经试过让同事手动在数据库结构中添加字段,那么数据库迁移可以让你不再需要做这样的事情。

执行 php artisan migtate 后 数据库中会生成一个迁移文件表 migrations ,每一条记录对应一个执行过的迁移文件,怎么看每次迁移了哪些文件?在 migrations 表中有一个 batch 字段,字段值相同的为同一次迁移

创建 created方法

Schema::create('users', function (Blueprint $table) {
    //...		
});

修改 table方法

数据库因为业务需要变更时,每个表的变更创建一个单独的迁移文件方便生产执行.

需要引入composer require doctrine/dbal 扩展包

Schema::table('migration_demo', function (Blueprint $table) {

});

1 对一个字段做多种修改 例如 重命名和修改类型同时进行.原字段为type 类型int

//无效方式1
Schema::table('migration_demo', function (Blueprint $table) {
    $table->bigInteger('type')->default('0')->change();
    $table->renameColumn('type', 'demo_type');
    //经测试这两号代码颠倒顺序最后生成的语句是一样的
});

执行语句, 字段重命名时又改回了默认的int类型

ALTER TABLE migration_demo CHANGE type demo_type INT DEFAULT 0 NOT NULL
ALTER TABLE migration_demo CHANGE type type BIGINT DEFAULT 0 NOT NULL
//无效方式2
Schema::table('migration_demo', function (Blueprint $table) {
    $table->bigInteger('type')->default('0')->change()->renameColumn('type', 'demo_type');
});

执行语句 ,rename并没有生效

ALTER TABLE migration_demo CHANGE type type BIGINT DEFAULT 0 NOT NULL

同一字段执行多种变更,正确的方式

Schema::table('demo', function (Blueprint $table) {
   $table->renameColumn('name', 'demo_name');
});
Schema::table('demo', function (Blueprint $table) {
   $table->string('demo_name', 255)->default('')->change();
});

执行语句结果

ALTER TABLE demo CHANGE demo_name demo_name VARCHAR(255) DEFAULT '' NOT NULL COLLATE utf8mb4_unicode_ci	
ALTER TABLE demo CHANGE name demo_name VARCHAR(20) DEFAULT '' NOT NULL

迁移文件的另一种简介执行方式,使用DB statement 执行原生DDL语句

DB::statement("ALTER TABLE `lara`.`users` CHANGE COLUMN `remember_token` `remember_tokens` VARCHAR(255) COLLATE 'utf8mb4_unicode_ci' NULL DEFAULT NULL");

总结

个人感觉这种方式适合中小型项目和公司,在有DBA的公司应该使用专业的数据库迁移工具