一次接口nginx502的排查经历

接口是一个生成全景作品的接口,接口会经历下载、切图、上传三个步骤。接口大部分时间运行是正常的,图片数量过多时会Nginx502。

排查经历

首先了解502是什么类型的错误,502 错误表明 Nginx 作为代理服务器时,无法从上游服务PHP-FPM获取有效响应。

先排查nginx配置的超时时间

是否超时时间过短导致的服务断开。nginx超时相关配置为/www/server/nginx/conf/proxy.conf

proxy_connect_timeout 60;# 连接上游超时
proxy_read_timeout 300;# 读取响应超时
proxy_send_timeout 120;# 发送请求超时

调大连接上游超时和读取响应超时,程序在多图时依然502。

考虑调整php.ini配置

看到接口设置了set_time_limit(0);和ini配置max_execution_time一样只影响脚本本身执行的时间,已经设置了不超时,可是程序还是提前退出了。

考虑调整php-fpm配置

/www/server/php/73/etc/php-fpm.conf

其中有一个配置是

request_terminate_timeout = 300s #整个PHP进程生存时间

进程超时优先大于脚本执行超时,如下图示:

所以会出现set_time_limit(0);不生效

临时解决

调大上述参数

终极解决

切图任务不要放在控制器处理,改用消息队列。

一次搬迁服务器到ctyun

记一次帮别人搬迁全景服务器到ctyun,原服务器在腾讯续费八千多,在ctyun买了一个s6 485配置的机器5年3000,于是搬迁到ctyun了。服务器数据盘有大概480g全景图,不准备在ctyun买数据盘存储了,500g数据盘5年价格3000多。在腾讯领了一个半年1t试用cos,把数据备份了一份到cos,跑了约一天全部上传完成。由于之前全景图在七牛,浏览的时候回源本地数据盘的图片,导致七牛的数据不完整的,决定再传一份到七牛,传了约1天跑了10%数据,感觉上传较慢,由于是在服务器到期前两三天搬迁的,服务器停机也没上传完成。我就把七牛的回源地址指向cos解决。此外数据库的数据有一部分是指向本地路径,此时本地数据已经删除了,cos有完整备份,本地图片nginx代理访问cos解决。

配置本地图片代理访问cos
# 设置目标 COS 信息
set $cos_bucket "xxx";
set $cos_region "ap-xxx";
set $cos_host "${cos_bucket}.cos.${cos_region}.myqcloud.com";
# DNS 解析配置
resolver 114.114.114.114 223.5.5.5 8.8.8.8 valid=30s;
resolver_timeout 5s;
# 核心代理配置:精确匹配 /vrimages/ 路径
location ^~ /vrimages/ {
  # 精确代理到 COS 路径
  proxy_pass https://$cos_host$request_uri;
  # 必须的头部设置
  proxy_set_header Host $cos_host;
  proxy_set_header X-Real-IP $remote_addr;
  proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
  proxy_set_header X-Forwarded-Proto $scheme;
  # HTTPS 配置
  proxy_ssl_server_name on;
  proxy_ssl_session_reuse on;
  proxy_ssl_protocols TLSv1.2 TLSv1.3;
  # 性能优化
  proxy_connect_timeout 5s;
  proxy_send_timeout 10s;
  proxy_read_timeout 20s;
  proxy_buffer_size 64k;
  proxy_buffers 16 64k;
  proxy_busy_buffers_size 128k;
  # 添加源识别头
  add_header X-Origin "Tencent-COS";
  add_header X-Cos-Bucket $cos_host;
  # 缓存控制
  proxy_cache_valid 200 304 30d;
  add_header Cache-Control "public, max-age=2592000";
}
备份cos和七牛
./coscli cp ./xxx/vrimages/ cos://xxx/vrimages -r
./qshell account ak as name #设置账号
./qshell qupload2 --src-dir=/www/wwwroot/xxx/vrimages/ --bucket=xxx --accelerate --check-exists --log-level=error --record-root=/www/wwwroot/log

上传七牛一定要指定log-level,不然会产生很多日志。

项目集成illuminate/database

迁移的项目很古老了,原来的查询写法比较难用,集成了illuminate/database,提升开发效率。

<?php
require_once __DIR__."/../vendor/autoload.php";
$db_config = include_once __DIR__."/../config/db.php";
use Illuminate\Support\Facades\DB;
use Illuminate\Database\Capsule\Manager as Capsule;
use Illuminate\Container\Container;
$container = new Container;
$capsule = new Capsule($container);
$capsule->addConnection($db_config);
// 设置全局静态访问
$capsule->setAsGlobal();
// 启动 Eloquent ORM
$capsule->bootEloquent();
// 将数据库管理器实例绑定到容器,键名为'db'
$container->instance('db', $capsule->getDatabaseManager());
Container::setInstance($container);
// 设置 Facade 的根容器
Illuminate\Support\Facades\Facade::setFacadeApplication($container);
return $capsule;
<?php
require __DIR__ . '/vendor/autoload.php';
$capsule = require __DIR__ . '/bootstrap/database.php';
// 设置默认时区
date_default_timezone_set('Asia/Shanghai');
// 示例使用
use Illuminate\Support\Facades\DB;
DB::enableQueryLog(); //使支持查询日志
//查询删除超过30天的作品
$worksmain = DB::table('worksmain')->select('pk_works_main')->where(function($query){
    //$query->where('delete_time','<=',time()-30*86400)->where('delete_time','>',0);
    $query->where('delete_time','>',0);
})->get();
require_once __DIR__.'/source/include/cls_queue.php';
foreach($worksmain as $v){
    QueueClient::push('delWorksImage',['pk_works_main'=>$v->pk_works_main]);
}
//print_r(DB::getQueryLog());

ThinkPHP8集成RabbitMQ

环境配置
# 添加Erlang仓库
wget -O- https://packages.erlang-solutions.com/ubuntu/erlang_solutions.asc | sudo apt-key add -
echo "deb https://packages.erlang-solutions.com/ubuntu $(lsb_release -sc) contrib" | sudo tee /etc/apt/sources.list.d/rabbitmq.list
# 更新包索引
sudo apt-get update
# 安装Erlang
sudo apt-get install -y erlang-base \
                        erlang-asn1 erlang-crypto erlang-eldap erlang-ftp erlang-inets \
                        erlang-mnesia erlang-os-mon erlang-parsetools erlang-public-key \
                        erlang-runtime-tools erlang-snmp erlang-ssl \
                        erlang-syntax-tools erlang-tftp erlang-tools erlang-xmerl
# 添加RabbitMQ仓库
echo "deb https://dl.bintray.com/rabbitmq/debian $(lsb_release -sc) main" | sudo tee /etc/apt/sources.list.d/rabbitmq.list
# 导入RabbitMQ公钥
wget -O- https://www.rabbitmq.com/rabbitmq-release-signing-key.asc | sudo apt-key add -
# 安装 rabbitmq-server
sudo apt-get install rabbitmq-server
sudo systemctl start rabbitmq-server
sudo systemctl enable rabbitmq-server  # 设置开机自启
# 安装librabbitmq开发库
sudo apt-get install -y librabbitmq-dev
# 安装php 扩展
pecl install amqp
# 安装php 客户端
composer require videlalvaro/php-amqplib
集成代码
//rabbitmq配置
<?php
return [
    'host' => env('RABBITMQ_HOST', 'localhost'),
    'port' => env('RABBITMQ_PORT', 5672),
    'user' => env('RABBITMQ_USER', 'guest'),
    'password' => env('RABBITMQ_PASSWORD', 'guest'),
    'vhost' => env('RABBITMQ_VHOST', '/'),
    'connection_timeout' => 3.0,
    'read_write_timeout' => 3.0,
    'heartbeat' => 0,
];
<?php
namespace app\service;

use PhpAmqpLib\Connection\AMQPStreamConnection;
use PhpAmqpLib\Message\AMQPMessage;
use think\facade\Config;

class RabbitMQService
{
    protected $connection;
    protected $channel;

    public function __construct()
    {
        $config = Config::get('rabbitmq');
        $this->connection = new AMQPStreamConnection(
            $config['host'],
            $config['port'],
            $config['user'],
            $config['password'],
            $config['vhost'],
            false,
            'AMQPLAIN',
            null,
            'en_US',
            $config['connection_timeout'],
            $config['read_write_timeout'],
            null,
            $config['heartbeat']
        );
        $this->channel = $this->connection->channel();
    }

    public function getChannel()
    {
        return $this->channel;
    }

    public function close()
    {
        $this->channel->close();
        $this->connection->close();
    }
}
<?php
namespace app\service;
use PhpAmqpLib\Message\AMQPMessage;

class RabbitMQProducer extends RabbitMQService
{
    public function publish($exchange, $routingKey, $message, $params = [])
    {
        // 声明交换机
        $this->channel->exchange_declare(
            $exchange,      // 交换机名称
            'direct',       // 交换机类型(根据需求修改)
            false,          // 是否被动声明(仅检查是否存在)
            true,           // 是否持久化
            false           // 是否自动删除
        );
        // 声明队列 直连模式
        $this->channel->queue_declare(
            'my_queue', // 队列名称
            false, // 是否被动声明(仅检查是否存在)
            true, // 是否持久化
            false, // 是否排他(仅对当前连接可见)
            false // 是否自动删除
        );
        $msg = new AMQPMessage(
            $message,
            array_merge([
                'content_type' => 'text/plain',
                'delivery_mode' => AMQPMessage::DELIVERY_MODE_PERSISTENT
            ], $params)
        );

        $this->channel->basic_publish($msg, $exchange, $routingKey);
    }
}
<?php
namespace app\service;
class RabbitMQConsumer extends RabbitMQService
{
    public function consume($queue, $callback, $noAck = false)
    {
        // 声明交换机(如果不存在则创建)
        $this->channel->exchange_declare(
            'my_exchange',  // 交换机名称
            'direct',       // 交换机类型(根据需求修改)
            false,          // 是否被动声明
            true,           // 是否持久化
            false           // 是否自动删除
        );
        // 声明队列(如果不存在则创建)
        $this->channel->queue_declare($queue, false, true, false, false);
        // 将队列绑定到交换机
        $this->channel->queue_bind($queue, 'my_exchange', 'my_routing_key');
        $this->channel->basic_consume(
            $queue,
            '',
            false,
            $noAck,
            false,
            false,
            $callback
        );
        while ($this->channel->is_open()) {
            $this->channel->wait();
        }
    }
}
<?php
declare (strict_types = 1);
namespace app\controller;
use app\BaseController;
use app\service\RabbitMQProducer;
class Rabbitmq extends BaseController
{
    public function sendMessage()
    {
        $producer = new RabbitMQProducer();
        $producer->publish(
            'my_exchange',      // 交换机名称
            'my_routing_key',   // 路由键
            json_encode([       // 消息内容
                'user_id' => 1234,
                'action' => 'login',
                'time' => time()
            ])
        );
        $producer->close();
        return json(['code' => 200, 'msg' => '消息发送成功']);
    }
}
<?php
declare (strict_types = 1);

namespace app\command;

use think\console\Command;
use think\console\Input;
use think\console\input\Argument;
use think\console\input\Option;
use think\console\Output;
use app\service\RabbitMQConsumer;

class ConsumeMessage extends Command
{
    protected function configure()
    {
        // 指令配置
        $this->setName('rabbitmq:consume')
             ->setDescription('消费RabbitMQ消息');
    }

    protected function execute(Input $input, Output $output)
    {
        $consumer = new RabbitMQConsumer();
        $consumer->consume('my_queue', function ($message) use ($output) {
            $data = json_decode($message->body, true);
            $output->writeln("收到消息: " . print_r($data, true));
            
            // 处理完消息后确认
            $message->ack();
        });
        $consumer->close();
    }
}
测试
http://www.thinkphp8.test/rabbitmq/sendMessage
~ php think rabbitmq:consume #可以看到投递的消息 可以supervisor 管理
消息类型

常见消息模式

  • 直连模式:消息根据路由键(routing key)直接路由到队列。生产者和消费者都需要声明队列,确保队列存在。生产者通过交换机和路由键发送消息,消费者从队列接收消息。
  • 扇形模式:消息广播到所有绑定的队列,忽略路由键。每个消费者需要声明自己的队列,并绑定到扇形交换机。生产者只需向交换机发送消息,无需关心队列。
  • 主题模式:消息根据路由键的模式(如*.error)路由到多个队列。消费者声明队列并绑定到主题交换机,指定绑定键(如order.#)。生产者根据业务逻辑设置路由键。

记录一次PHP面试题

题目1 如何设计接口可以防止重复点击,扣减库存如何保证幂等性?

简单实现

<?php
namespace app\controller;
use app\BaseController;
use think\facade\Cache;
use think\facade\Db;
use Ramsey\Uuid\Uuid;

class Idempotent extends BaseController
{
    /**
     * 幂等性:一次和多次请求某一个资源应该具有同样的副作用。
     * 幂等号:UUID、业务唯一标识组合
     * 方案1:客户端生成幂等号
     * 客户端发起扣费请求时,必须生成并携带一个全局唯一的幂等号,服务端收到某个幂等号请求时执行扣库存,将幂等号扣减结果Redis或数据库持久化。后续请求先判断幂等号是否存在,如果存在返回之前的结果。如果不存在按新请求处理。
     * 幂等号必须由客户端生成并确保唯一,对幂等号的检查、扣库存、结果存储在同一个事务,确保原子性。
     */
    public function index()
    {
        try {
            $data = $this->request->param();
            $uniqueId = $data['uniqueId'];
            $productId = $data['productId'];
            $key = 'idempotent:'.$uniqueId;
            // 检查幂等号是否存在
            if (Cache::has($key)) {
                return json(Cache::get($key));
            }
            Cache::set($key, 0, 86400);//状态0为未处理
            // 检查是否有扣减记录
            $deductRecord = Db::table('deduct_records')
                ->where('unique_id', $uniqueId)
                ->find();
            if ($deductRecord) {
                if ($deductRecord['status'] == 'success') {
                    // 已经成功扣减过库存,直接返回结果
                    return json([
                        'status' => 'success'
                    ]);
                }else if ($deductRecord['status'] == 'error'){
                    // 已经失败扣减过库存,直接返回结果
                    return json([
                        'status' =>'error',
                        'error_message' => $deductRecord['error_message']
                    ]);
                }
            }
            // 开启数据库事务
            Db::startTrans();
            // 扣减库存
            $inventory = Db::table('product_inventory')
                ->where('product_id', $productId)
                ->lock(true)
                ->find();
            if ($inventory['stock'] <= 0) {
                throw new \Exception('库存不足');
            }
            // 扣减库存操作
            Db::table('product_inventory')
                ->where('product_id', $productId)
                ->dec('stock')
                ->update();
            // 持久化结果
            $result = [
                'status' => 'success',
                'unique_id' => $uniqueId,
                'product_id' => $productId,
                'deducted' => 1,
                'timestamp' => time()
            ];
            // 记录扣减记录 方便对库存 建立定期对库存的监控兜底
            Db::table('deduct_records')->insert($result);
            // 事务提交
            Db::commit();
            // 存储到Redis(设置24小时过期)
            Cache::set($key, $result, 86400);
            return json($result);
        } catch (\Exception $e) {
            Db::rollback();
            Cache::delete($key);
            // 添加失败记录
            Db::table('deduct_records')->insert([
                'status' => 'error',
                'unique_id' => $uniqueId,
                'product_id' => $productId,
                'error_message' => $e->getMessage(),
                'timestamp' => time()
            ]);
            return json([
                'status' => 'error',
                'message' => $e->getMessage()
            ]);
        }
    }
    /**
     * 方案2:服务端生成 token
     * 客户端会先发送一个请求去获取 token,服务端会生成一个全局唯一的 ID 作为 token 保存在 redis 中,同时把这个 ID 返回给客户端
     * 客户端第二次调用业务请求的时候必须携带这个 token
     * 服务端会校验这个 token,如果校验成功,则执行业务,并删除 redis 中的 token
     * 如果校验失败,说明 redis 中已经没有对应的 token,则表示重复操作,直接返回指定的结果给客户端
     * 对 redis 中是否存在 token 以及删除的代码逻辑建议用 Lua 脚本实现,Lua脚本在Redis中执行是原子的,可以避免竞态条件
     */
    public function getToken()
    {
        try {
            $token = Uuid::uuid4()->toString();
            $key = 'idempotent_token:'. $token;
            $redis = Cache::store('redis')->handler();
            $redis->set($key, 1, 86400);
            return json([
                'code' => 200,
                'msg'  => '操作成功',
                'data' => [
                    'token' => $token
                ]
            ]);
        } catch (\Exception $e) {
            return json([
                'code' => 500,
                'msg'  => '操作失败: '.$e->getMessage()
            ]);
        }
    }
    public function checkToken()
    {
        try {
            $data = $this->request->param();
            $token = $data['token'] ?? '';
            if (empty($token)) {
                throw new \Exception('缺少必要参数: token');
            }
            $key = 'idempotent_token:' . $token;
            $lua = <<<LUA
                if redis.call("GET", KEYS[1]) then
                    redis.call("DEL", KEYS[1])
                    return 1
                else
                    return 0
                end
                LUA;
            // 执行原子性校验
            $redis = Cache::store('redis')->handler();
            $result = $redis->eval($lua, [$key], 1);
            if ($result !== 1) {
                return json([
                    'code' => 400,
                    'msg'  => '重复请求或无效Token'
                ]);
            }
            // 执行业务逻辑
            Db::name('product_inventory')
                ->where('product_id', $data['product_id'])
                ->dec('stock')
                ->update();
            return json([
                'code' => 200,
                'msg'  => '操作成功'
            ]);
        } catch (\Exception $e) {
            return json([
                'code' => 500,
                'msg'  => '操作失败: '.$e->getMessage()
            ]);
        }
    }
    /**
     * 方案3:根据带唯一索引的MySQL+状态机实现
     * 插入失败就拒绝请求
     * 插入成功就继续执行
     **/
    public function insertIndex()
    {
        try {
            $data = $this->request->param();
            $uniqueId = $data['uniqueId'];
            $productId = $data['productId'];
            // 开启数据库事务
            Db::startTrans();
            // 先尝试插入记录
            $record = [
                'unique_id' => $uniqueId,
                'product_id' => $productId,
                'status' => 'processing',
                'timestamp' => time()
            ];
            Db::table('deduct_records')
                ->strict(false)
                ->insert($record);
            // 扣减库存
            $affected = Db::table('product_inventory')
                ->where('product_id', $productId)
                ->where('stock', '>', 0)
                ->dec('stock')
                ->update();
            if ($affected === 0) {
                throw new \Exception('库存不足');
            }
            // 更新记录状态
            Db::table('deduct_records')
                ->where('unique_id', $uniqueId)
                ->update(['status' => 'success']);
            Db::commit();
            return json([
                'status' => 'success',
                'unique_id' => $uniqueId
            ]);
        } catch (\Exception $e) {
            Db::rollback();
            // 处理唯一键冲突(错误码23000)
            if ($e->getCode() == '23000') {
                return json([
                    'status' => 'error',
                    'message' => '重复请求'
                ]);
            }
            return json([
                'status' => 'error',
                'message' => $e->getMessage()
            ]);
        }
    }
    /**
     * 方案4:基于Redis 分布式锁实现
     * 使用SETNX原子操作实现分布式锁,确保只有一个请求可以执行扣减操作
     **/
    public function setNx()
    {
        try {
            $data = $this->request->param();
            $uniqueId = $data['uniqueId'];
            $productId = $data['productId'];
            $key = 'idempotent:'.$uniqueId;
            $redis = Cache::store('redis')->handler();
            // 使用SETNX原子操作
            $lockAcquired = $redis->setnx($key, 1);
            if (!$lockAcquired) {
                return json([
                    'code' => 400,
                    'msg' => '重复请求,请勿重复提交'
                ]);
            }
            $redis->expire($key, 86400);
            // 开启数据库事务
            Db::startTrans();
            // 扣减库存
            $inventory = Db::table('product_inventory')
                ->where('product_id', $productId)
                ->lock(true)
                ->find();
            if (!$inventory || $inventory['stock'] <= 0) {
                throw new \Exception('库存不足');
            }
            Db::table('product_inventory')
                ->where('product_id', $productId)
                ->dec('stock')
                ->update();
            // 保存记录
            $record = [
                'unique_id' => $uniqueId,
                'product_id' => $productId,
                'status' => 'success',
                'timestamp' => time()
            ];
            Db::table('deduct_records')->insert($record);
            Db::commit();
            return json([
                'code' => 200,
                'msg' => '操作成功',
                'data' => $record
            ]);
        } catch (\Exception $e) {
            Db::rollback();
            // 删除Redis锁
            Cache::delete($key);
            return json([
                'code' => 500,
                'msg' => '操作失败: '.$e->getMessage()
            ]);
        }
    }
    /**
     * 方案5:基于MySQL的乐观锁加版本号实现
     **/
    public function optimisticLock()
    {
        try {
            $data = $this->request->param();
            $uniqueId = $data['uniqueId'];
            $productId = $data['productId'];
            // 开启事务
            Db::startTrans();
            // 获取当前版本号
            $inventory = Db::table('product_inventory')
                ->where('product_id', $productId)
                ->lock('lock in share mode')
                ->field('stock,version')
                ->find();
            if (!$inventory || $inventory['stock'] <= 0) {
                throw new \Exception('库存不足');
            }
            // 带版本号的更新操作
            $affected = Db::table('product_inventory')
                ->where('product_id', $productId)
                ->where('version', $inventory['version'])
                ->dec('stock')
                ->inc('version')
                ->update();
            if ($affected === 0) {
                throw new \Exception('操作冲突,请重试');
            }
            // 记录扣减操作
            $record = [
                'unique_id' => $uniqueId,
                'product_id' => $productId,
                'status' => 'success',
                'timestamp' => time()
            ];
            Db::table('deduct_records')->insert($record);
            Db::commit();
            return json([
                'code' => 200,
                'msg' => '操作成功',
                'data' => $record
            ]);
        } catch (\Exception $e) {
            Db::rollback();
            return json([
                'code' => 500,
                'msg' => '操作失败: '.$e->getMessage()
            ]);
        }
    }
}
-- 商品库存表
CREATE TABLE product_inventory (
  product_id INT PRIMARY KEY COMMENT '商品ID',
  stock INT NOT NULL DEFAULT 0 COMMENT '库存数量'
  version INT NOT NULL DEFAULT 0 COMMENT '版本号'
);
-- 扣库存表
CREATE TABLE deduct_records (
  id INT AUTO_INCREMENT PRIMARY KEY,
  unique_id VARCHAR(64) NOT NULL UNIQUE,
  product_id INT NOT NULL,
  status VARCHAR(20) NOT NULL,
  deducted INT NOT NULL,
  timestamp INT NOT NULL,
  error_message VARCHAR(255) NOT NULL DEFAULT ''
);
ab -n 10 -c 10 'http://www.thinkphp8.test/idempotent/index?uniqueId=test136&productId=1' #ab模拟10次并发
#客户端uuid 包nanoid https://github.com/ai/nanoid,包uuid https://github.com/uuidjs/uuid

概念参考 https://xie.infoq.cn/article/e2752bf22f3a9e4f9ce2c404e

题目2 Redis如何保证和数据库数据一致性

  1. 旁路缓存模式:读流程:读缓存命中返回,未命中读库,写入缓存返回。写流程:方案1先更数据库再删缓存。应对更新库后和删除缓存之间读取的缓存旧数据问题,可以延迟一小时后双删。方案2先删缓存,再更新数据库。不一致问题更严重,也可以延迟双删解决。删除缓存可以交给消息队列异步操作。
  2. 写穿透模式:应用层将写操作交给一个抽象层处理。该层负责同步更新数据库、同步更新缓存。
  3. 写回模式:只更新缓存,缓存层异步更新数据到数据库。适用日志场景,容忍一部分数据丢失。
  4. 订阅数据库变更日志(Binlog/CDC),把变更事件发送到消息队列。

题目3 PHP有哪些运行模式

  1. cgi/fastcgi模式:fastcgi模式,核心思想进程常驻,fpm主进程管理一个进程池,请求交给空闲进程处理。
  2. 模块模式
  3. cli模式

题目4 中间件的架构模式或设计思想

  1. 责任链模式:一个请求经过管道的每个中间件 req next res
  2. 管道-过滤器模式:数据流经管道被每个过滤器处理
  3. 体现了解耦、可插拔性/开闭原则、可组合性、横切关注点集中处理、IOC/DI

Yaf集成think-orm4

目前最新yaf版本3.3.6,可以在PHP8.3上成功安装。

安装步骤
#下载最新代码
wget https://pecl.php.net/get/yaf-3.3.6.tgz
#解压
tar -zxvf yaf-3.3.6.tgz
#安装
cd yaf-3.3.6/
phpize
./configure --with-php-config=/www/server/php/83/bin/php-config
make && make install
#修改php.ini 添加yaf.so
#克隆yaf
git clone https://github.com/laruence/yaf.git
#生成项目skeleton
cd /www/wwwroot/yaf/tools/cg
php yaf_cg -d Sample -n
安装think-orm
sudo -u www composer require topthink/think-orm
#入口引入think-orm
require __DIR__ . '/vendor/autoload.php';
添加数据库配置
[think-orm]
db.type     = mysql
db.hostname = 127.0.0.1
db.username = yaf
db.password = myGEFhY7FKhKME3m
db.database = yaf
db.charset  = utf8mb4
db.prefix   = yaf_
db.debug    = true
初始化数据库配置
<?php
class Bootstrap extends \Yaf\Bootstrap_Abstract {
    public function _initDb(){
        $ini = new \Yaf\Config\Ini(__DIR__."/../conf/db.ini");
        $arr = $ini->toArray();
        $config = ['default'=>'mysql','connections'=>['mysql'=>$arr['think-orm']['db']]];
        (new \think\DbManager())->setConfig($config);
    }
}
配置模型
<?php
use think\Model;
/**
 * @name SampleModel
 * @desc sample模型类 可以使用模型的属性和方法
 * @author root
 */
class SampleModel extends Model{
    protected $table = "yaf_sample";
    public function selectSample() {
        return 'Hello World!';
    }

    public function insertSample($arrInfo) {
        return true;
    }
}
think-orm4新特性Enity引入
<?php
class Bootstrap extends \Yaf\Bootstrap_Abstract {
    public function _initLoader($dispatcher) {
        \Yaf\Loader::getInstance()->registerNamespace('App\Entity', realpath(APPLICATION_PATH . '/application/entity'));
    }
}
Enity sample
<?php
namespace App\Entity;
use think\Entity;
/**
 * @name SampleEntity
 * @desc sample实体类 可以扩展业务逻辑 让模型专注查询、持久化数据
 * @author root
 */
class SampleEntity extends Entity
{
    public function getOptions():array
    {
    	return [
            'modelClass'      => \SampleModel::class,
    	];
    }
}
Enity使用
$entity = new \App\Entity\SampleEntity();
$one = \App\Entity\SampleEntity::find(1);

PHP Empty函数深入理解

empty() 函数用于检查一个变量是否为空,如果变量为空,则返回 true,否则返回 false。对于对象属性,空的定义取决于属性的可见性。

在 PHP 中,对象的属性有公有(public)、私有(private)和受保护(protected)三种可见性。公有属性可以直接从对象外部访问,而私有属性和受保护属性不能直接从对象外部访问。

当使用 empty() 函数检查一个对象的属性时,如果属性是私有的或受保护的,不管在对象内部有无赋值,那么 empty() 函数会返回 true,因为从对象外部无法直接访问到这个属性,也无法确定这个属性是否有值。

class MyObject {
    private $property;
    public function __construct($value = null) {
        $this->property = $value;
    }
    public function __get($key)
    {
        if(isset($this->{$key}))
        {
            return $this->{$key};
        }
        else
        {
            throw new \Exception("Property does not exist: " . get_class($this) . "::" . $key);
        }
    }
}
$object = new MyObject('value');
$property = $object->property; // 从对象外部获取属性值
var_dump(empty($object->property),empty($property));//true,false 因为属性是私有的