Write the Code. Change the World.

分类目录
8月 28

国密 sm2,sm3,sm3 在 php 中使用。国密 sm2,sm3,sm3 在 laravel 中使用。

Github

先准备 php docker 镜像

FROM xxxx

# 其他代码

WORKDIR /www

# 编译工具链(独立成层,成功后即缓存)
RUN set -eux; \
    apt-get update; \
    apt-get install -y --no-install-recommends git ca-certificates build-essential cmake pkg-config; \
    rm -rf /var/lib/apt/lists/*

# GmSSL底层库:源码编译安装(独立成层)
RUN set -eux; \
    cd /tmp; \
    git clone https://github.com/guanzhi/GmSSL --depth=1; \
    cd GmSSL; \
    mkdir build && cd build; \
    cmake .. -DCMAKE_INSTALL_PREFIX=/usr/local/gmssl; \
    make -j$(nproc); \
    make install; \
    echo "/usr/local/gmssl/lib" > /etc/ld.so.conf.d/gmssl.conf; \
    ldconfig; \
    rm -rf /tmp/GmSSL

# GmSSL-PHP国密扩展:编译安装并启用(独立成层)
RUN set -eux; \
    cd /tmp; \
    git clone https://github.com/GmSSL/GmSSL-PHP.git --depth=1; \
    cd GmSSL-PHP; \
    phpize; \
    ./configure --with-gmssl=/usr/local/gmssl; \
    make -j$(nproc); \
    make install; \
    docker-php-ext-enable gmssl; \
    rm -rf /tmp/GmSSL-PHP

# 创建软链接,让系统可以直接找到 gmssl
RUN ln -sf /usr/local/gmssl/bin/gmssl /usr/local/bin/gmssl

# 验证安装
RUN gmssl version

环境准备好了,就可以在 laravel 项目中使用了。

使用

  1. 生成 sm4 需要的 key,iv 以及 sm2 需要的公钥私钥口令这些
#!/bin/bash

# 用来生成 GM SM4、SM2 的密钥(在已安装 GmSSL 的 Linux / docker 容器中执行)
# 使用方法: ./gmssl.sh
# 可选环境变量:
#   GMSSL_BIN  gmssl 命令路径(默认从 PATH 查找)
#   SM2_PASS   SM2 口令私钥的密码(默认随机生成24位hex,生产环境务必自行修改保管)

set -e  # 遇到错误立即退出

# ========== 配置区域 ==========
# gmssl 命令(默认安装到 /usr/local/bin/gmssl,从 PATH 查找即可)
GMSSL_BIN="${GMSSL_BIN:-gmssl}"

# 密钥存储目录(位于 storage/app 下,.env 中填相对路径即可)
KEY_DIR="./storage/app/gmssl"

# SM2 口令私钥的密码:未指定时随机生成
SM2_PASS="${SM2_PASS:-$(openssl rand -hex 12)}"

# ========== 开始生成 ==========

# 0. 检查 gmssl 命令是否可用
if ! command -v "$GMSSL_BIN" >/dev/null 2>&1; then
    echo "错误: 找不到 gmssl 命令,请确认容器内已安装 GmSSL,或通过 GMSSL_BIN 环境变量指定路径"
    exit 1
fi

# 1. 创建密钥目录
echo "创建密钥目录: $KEY_DIR"
mkdir -p "$KEY_DIR"

# 2. 生成 SM4 密钥和 IV (16字节 -> 32位十六进制)
echo "生成 SM4 密钥和 IV..."
GM_SM4_KEY=$(openssl rand -hex 16)
GM_SM4_IV=$(openssl rand -hex 16)

# 3. 生成 SM2 密钥对(口令加密私钥,公钥不加密)
echo "生成 SM2 密钥对..."
"$GMSSL_BIN" sm2keygen \
    -pass "$SM2_PASS" \
    -out "$KEY_DIR/sm2_private.pem" \
    -pubout "$KEY_DIR/sm2_public.pem"

# 4. 验证文件是否生成成功
if [ ! -f "$KEY_DIR/sm2_private.pem" ] || [ ! -f "$KEY_DIR/sm2_public.pem" ]; then
    echo "错误: SM2 密钥文件生成失败"
    exit 1
fi

# 5. 设置文件权限(安全:私钥仅属主可读写)
chmod 600 "$KEY_DIR/sm2_private.pem"
chmod 644 "$KEY_DIR/sm2_public.pem"

# 6. 输出结果(.env 中填相对于 storage/app 的路径,由 PHP 端 storage_path() 解析,避免依赖运行时工作目录)
echo ""
echo "=========================================="
echo "✅ 密钥生成完成!"
echo "=========================================="
echo ""
echo "📁 密钥文件位置:"
echo "  加密私钥: $KEY_DIR/sm2_private.pem"
echo "  公钥:     $KEY_DIR/sm2_public.pem"
echo ""
echo "📝 请将以下内容添加到 .env 文件:"
echo "----------------------------------------"
echo "GM_SM4_KEY=$GM_SM4_KEY"
echo "GM_SM4_IV=$GM_SM4_IV"
echo "GM_SM2_PRIV_KEY_FILE=gmssl/sm2_private.pem"
echo "GM_SM2_PUB_KEY_FILE=gmssl/sm2_public.pem"
echo "GM_SM2_PRIV_PASS=$SM2_PASS"
echo "----------------------------------------"
echo ""
echo "⚠️  安全提示:"
echo "  1. 请妥善保管 sm2_private.pem 及 GM_SM2_PRIV_PASS 口令,丢失后已加密数据无法恢复"
echo "  2. 不要将密钥文件和 .env 提交到 Git 仓库"
echo "  3. 生产环境请使用更强的口令,并通过密钥管理服务分发"
echo "=========================================="

# 7. 确保密钥目录被 Git 忽略
if [ -f ".gitignore" ]; then
    if ! grep -q "storage/app/gmssl" .gitignore 2>/dev/null; then
        echo "" >> .gitignore
        echo "# GMSSL 密钥文件" >> .gitignore
        echo "/storage/app/gmssl/" >> .gitignore
        echo "已添加 /storage/app/gmssl/ 到 .gitignore"
    fi
fi

echo ""
echo "🎉 完成!"

在容器内,执行 sh ./gmssl.sh 来生成上边说的这些。将上边对应的结果添加的 .env 文件中。记得在 .env.example 中增加对应的配置项。

  1. 增加 config/gm.php,填入下边的内容。
<?php

return [
    // SM4-CBC 密钥与IV:16字节原始密钥的32位hex字符串(由 gmssl.sh 生成)
    'sm4_key' => env('GM_SM4_KEY'),
    'sm4_iv'  => env('GM_SM4_IV'),

    // SM2 密钥文件路径:支持绝对路径;相对路径基于 storage/app 解析
    // 例如填 gmssl/sm2_private.pem 即 storage/app/gmssl/sm2_private.pem
    'sm2_priv_key_file' => env('GM_SM2_PRIV_KEY_FILE'),
    'sm2_pub_key_file'  => env('GM_SM2_PUB_KEY_FILE'),

    // SM2 加密私钥的口令(gmssl sm2keygen -pass 所用密码)
    'sm2_priv_pass' => env('GM_SM2_PRIV_PASS'),

    // SM2 签名/验签使用的签名者ID(与对接方约定一致,国密默认值为 1234567812345678)
    'sm2_id' => env('GM_SM2_ID', '1234567812345678'),
];
  1. 增加工具类
<?php

namespace App\Utils;

use RuntimeException;

/**
 * 国密算法工具类(SM2 / SM3 / SM4)
 *
 * 基于 gmssl PHP 扩展(https://github.com/GmSSL/GmSSL-PHP,依赖 GmSSL C 库 >= 3.1.0)
 *
 * 注意:官方扩展的函数签名与本类早期写法差异较大,以下为官方 API:
 * - gmssl_sm4_cbc_encrypt($key, $iv, $data) / gmssl_sm4_cbc_decrypt($key, $iv, $ciphertext)
 * - gmssl_sm2_sign($keypair, $id, $message),签名固定为 ASN.1 DER 编码
 * - gmssl_sm2_verify($public_key, $id, $message, $signature)
 * - gmssl_sm2_encrypt($public_key, $data) / gmssl_sm2_decrypt($keypair, $ciphertext)
 * - SM2 密钥对象必须通过文件路径加载:
 *   gmssl_sm2_private_key_info_decrypt_from_pem($file, $passphrase)
 *   gmssl_sm2_public_key_info_from_pem($file)
 */
class GmUtils
{
    /** SM4 二进制密钥(16字节),进程内缓存 */
    protected static ?string $sm4Key = null;

    /** SM4 二进制IV(16字节),进程内缓存 */
    protected static ?string $sm4Iv = null;

    /** SM2 私钥对象(96字节,含口令解密结果),进程内缓存 */
    protected static ?string $sm2PrivKey = null;

    /** SM2 公钥对象(96字节),进程内缓存 */
    protected static ?string $sm2PubKey = null;

    /**
     * SM3 摘要
     * @param string $data 原始数据
     * @return string 小写hex摘要(64位)
     */
    public static function sm3(string $data): string
    {
        return bin2hex(gmssl_sm3($data));
    }

    /**
     * SM4‑CBC 加密(带PKCS填充)
     * @param string $plainText 明文
     * @param string|null $keyHex 自定义密钥(32位hex),不传则用 GM_SM4_KEY
     * @param string|null $ivHex 自定义IV(32位hex),不传则用 GM_SM4_IV
     * @return string base64密文(适合http/json传输)
     */
    public static function sm4Encrypt(string $plainText, ?string $keyHex = null, ?string $ivHex = null): string
    {
        [$key, $iv] = self::sm4Keys($keyHex, $ivHex);
        return base64_encode(gmssl_sm4_cbc_encrypt($key, $iv, $plainText));
    }

    /**
     * SM4‑CBC 解密
     * @param string $base64Cipher base64密文
     * @param string|null $keyHex 自定义密钥(32位hex),不传则用 GM_SM4_KEY
     * @param string|null $ivHex 自定义IV(32位hex),不传则用 GM_SM4_IV
     * @return string 原始明文
     */
    public static function sm4Decrypt(string $base64Cipher, ?string $keyHex = null, ?string $ivHex = null): string
    {
        $cipher = base64_decode($base64Cipher, true);
        if ($cipher === false) {
            throw new RuntimeException('SM4解密失败: 密文不是合法的base64');
        }
        [$key, $iv] = self::sm4Keys($keyHex, $ivHex);
        return gmssl_sm4_cbc_decrypt($key, $iv, $cipher);
    }

    /**
     * SM4‑CBC 加密,返回hex密文(与小程序/前端 JS 库如 sm-crypto 对接用)
     */
    public static function sm4EncryptHex(string $plainText, ?string $keyHex = null, ?string $ivHex = null): string
    {
        [$key, $iv] = self::sm4Keys($keyHex, $ivHex);
        return bin2hex(gmssl_sm4_cbc_encrypt($key, $iv, $plainText));
    }

    /**
     * SM4‑CBC 解密,入参为hex密文(与小程序/前端 JS 库如 sm-crypto 对接用)
     */
    public static function sm4DecryptHex(string $hexCipher, ?string $keyHex = null, ?string $ivHex = null): string
    {
        $hex = trim($hexCipher);
        if ($hex === '' || strlen($hex) % 2 !== 0 || !ctype_xdigit($hex)) {
            throw new RuntimeException('SM4解密失败: 密文不是合法的hex字符串');
        }
        [$key, $iv] = self::sm4Keys($keyHex, $ivHex);
        return gmssl_sm4_cbc_decrypt($key, $iv, hex2bin($hex));
    }

    /**
     * SM2签名(SM3withSM2),返回 base64 的 ASN.1 DER 签名
     * @param string $data 待签名原文
     * @param string|null $id 签名者ID,默认取 GM_SM2_ID 配置(1234567812345678)
     * @return string base64签名
     */
    public static function sm2Sign(string $data, ?string $id = null): string
    {
        $sig = gmssl_sm2_sign(self::getSm2PrivKey(), self::sm2Id($id), $data);
        return base64_encode($sig);
    }

    /**
     * SM2验签
     * @param string $data 原文
     * @param string $base64Sign base64编码的DER签名
     * @param string|null $id 签名者ID,需与签名时一致
     * @return bool
     */
    public static function sm2Verify(string $data, string $base64Sign, ?string $id = null): bool
    {
        $sig = base64_decode($base64Sign, true);
        if ($sig === false || $sig === '') {
            return false;
        }
        return gmssl_sm2_verify(self::getSm2PubKey(), self::sm2Id($id), $data, $sig);
    }

    /**
     * SM2公钥加密,返回base64(密文为ASN.1 DER,内部C1C3C2)
     * @param string $plain 明文(SM2加密适用于短数据/密钥材料保护)
     * @return string base64密文
     */
    public static function sm2Encrypt(string $plain): string
    {
        $cipher = gmssl_sm2_encrypt(self::getSm2PubKey(), $plain);
        return base64_encode($cipher);
    }

    /**
     * SM2私钥解密
     * @param string $base64Cipher base64密文
     * @return string 明文
     */
    public static function sm2Decrypt(string $base64Cipher): string
    {
        $cipher = base64_decode($base64Cipher, true);
        if ($cipher === false || $cipher === '') {
            throw new RuntimeException('SM2解密失败: 密文不是合法的base64');
        }
        return gmssl_sm2_decrypt(self::getSm2PrivKey(), $cipher);
    }

    /**
     * SM2解密兼容方法:解密第三方客户端(小程序/前端 sm-crypto 等)传来的裸 C1C3C2 hex 密文
     * 密文布局:[04] || C1(X 32字节 || Y 32字节) || C3(32字节SM3摘要) || C2
     * @param string $hexCipher hex密文,带不带 04 前缀均可,要求客户端使用 C1C3C2 顺序
     * @return string 明文
     */
    public static function sm2DecryptHex(string $hexCipher): string
    {
        $hex = trim($hexCipher);
        if ($hex === '' || strlen($hex) % 2 !== 0 || !ctype_xdigit($hex)) {
            throw new RuntimeException('SM2解密失败: 密文不是合法的hex字符串');
        }
        $bin = hex2bin($hex);
        // 兼容带 04 非压缩点前缀的写法
        if ($bin[0] === "\x04") {
            $bin = substr($bin, 1);
        }
        if (strlen($bin) < 97) {
            throw new RuntimeException('SM2解密失败: 密文长度不足,请确认客户端使用 C1C3C2 顺序');
        }
        $x  = substr($bin, 0, 32);
        $y  = substr($bin, 32, 32);
        $c3 = substr($bin, 64, 32);
        $c2 = substr($bin, 96);

        // gmssl 扩展只接受 ASN.1 DER 密文,需把裸密文组装成:
        // SEQUENCE { xCoordinate INTEGER, yCoordinate INTEGER, hash OCTET STRING, cipherText OCTET STRING }
        $body = self::derInt($x) . self::derInt($y) . self::derOctetString($c3) . self::derOctetString($c2);
        $der  = "\x30" . self::derLength(strlen($body)) . $body;
        return gmssl_sm2_decrypt(self::getSm2PrivKey(), $der);
    }

    /**
     * 导出SM2公钥为非压缩点hex(04||X||Y,130位字符),用于下发给客户端 JS 库(如 sm-crypto)
     * @return string
     */
    public static function sm2PubKeyHex(): string
    {
        $pub = self::getSm2PubKey();
        // 公钥对象为96字节,前64字节即 X||Y,后32字节为零填充的私钥位,不取
        return '04' . bin2hex(substr($pub, 0, 64));
    }

    /**
     * 从SM2私钥导出公钥PEM(公钥丢失时可随时由私钥重新导出,密钥对不变)
     * @param string $targetFile 目标公钥PEM路径
     * @return string 导出的公钥非压缩点hex(04||X||Y,130位)
     */
    public static function exportSm2PubPem(string $targetFile): string
    {
        $keypair = self::getSm2PrivKey();
        if (gmssl_sm2_public_key_info_to_pem($keypair, $targetFile) !== true) {
            throw new RuntimeException('SM2公钥导出失败: 请检查目标路径是否可写');
        }
        // 私钥对象前64字节即公钥点 X||Y,直接推导,无需再读公钥文件
        return '04' . bin2hex(substr($keypair, 0, 64));
    }

    /**
     * 解析 SM4 密钥/IV:未传自定义值时用配置默认值,传了则现场 hex 解码(不走缓存)
     * @return array [二进制key, 二进制iv]
     */
    protected static function sm4Keys(?string $keyHex, ?string $ivHex): array
    {
        $key = $keyHex === null ? self::getSm4Key() : self::hexToBin($keyHex, 'SM4自定义密钥');
        $iv  = $ivHex === null ? self::getSm4Iv() : self::hexToBin($ivHex, 'SM4自定义IV');
        return [$key, $iv];
    }

    /**
     * 获取SM4二进制key(16字节)
     */
    protected static function getSm4Key(): string
    {
        if (self::$sm4Key === null) {
            self::$sm4Key = self::hexToBin((string) config('gm.sm4_key'), 'GM_SM4_KEY');
        }
        return self::$sm4Key;
    }

    /**
     * 获取SM4二进制iv(16字节)
     */
    protected static function getSm4Iv(): string
    {
        if (self::$sm4Iv === null) {
            self::$sm4Iv = self::hexToBin((string) config('gm.sm4_iv'), 'GM_SM4_IV');
        }
        return self::$sm4Iv;
    }

    /**
     * 加载SM2私钥对象(从口令加密的PEM文件解密导入)
     */
    protected static function getSm2PrivKey(): string
    {
        if (self::$sm2PrivKey === null) {
            $file = self::resolveKeyPath((string) config('gm.sm2_priv_key_file'), 'GM_SM2_PRIV_KEY_FILE');
            $pass = (string) config('gm.sm2_priv_pass');
            if ($pass === '') {
                throw new RuntimeException('国密配置错误: 未配置 GM_SM2_PRIV_PASS(SM2私钥口令)');
            }
            self::$sm2PrivKey = gmssl_sm2_private_key_info_decrypt_from_pem($file, $pass);
        }
        return self::$sm2PrivKey;
    }

    /**
     * 加载SM2公钥对象
     */
    protected static function getSm2PubKey(): string
    {
        if (self::$sm2PubKey === null) {
            $file = self::resolveKeyPath((string) config('gm.sm2_pub_key_file'), 'GM_SM2_PUB_KEY_FILE');
            self::$sm2PubKey = gmssl_sm2_public_key_info_from_pem($file);
        }
        return self::$sm2PubKey;
    }

    /**
     * 解析密钥文件路径:绝对路径直接使用,相对路径基于 storage/app 解析
     */
    protected static function resolveKeyPath(string $path, string $envName): string
    {
        $path = trim($path);
        if ($path === '') {
            throw new RuntimeException("国密配置错误: 未配置 {$envName}");
        }
        if (!str_starts_with($path, '/') && !preg_match('/^[A-Za-z]:[\\\\\/]/', $path)) {
            $path = storage_path('app/' . ltrim($path, '/'));
        }
        if (!is_file($path)) {
            throw new RuntimeException("国密配置错误: {$envName} 指向的文件不存在: {$path}");
        }
        return $path;
    }

    /**
     * hex字符串转二进制,并校验长度为16字节
     */
    protected static function hexToBin(string $hex, string $envName): string
    {
        $hex = trim($hex);
        if ($hex === '' || !ctype_xdigit($hex) || strlen($hex) % 2 !== 0) {
            throw new RuntimeException("国密配置错误: {$envName} 必须是合法的hex字符串");
        }
        $bin = hex2bin($hex);
        if (strlen($bin) !== 16) {
            throw new RuntimeException("国密配置错误: {$envName} 必须是16字节(32位hex)");
        }
        return $bin;
    }

    /**
     * SM2签名/验签使用的签名者ID
     */
    protected static function sm2Id(?string $id): string
    {
        if ($id !== null && $id !== '') {
            return $id;
        }
        $configId = (string) config('gm.sm2_id', '');
        return $configId !== '' ? $configId : GMSSL_SM2_DEFAULT_ID;
    }

    /**
     * DER 长度编码
     */
    protected static function derLength(int $len): string
    {
        if ($len < 0x80) {
            return chr($len);
        }
        $bytes = '';
        while ($len > 0) {
            $bytes = chr($len & 0xFF) . $bytes;
            $len >>= 8;
        }
        return chr(0x80 | strlen($bytes)) . $bytes;
    }

    /**
     * DER INTEGER 编码(去前导零,高位为1时补 0x00)
     */
    protected static function derInt(string $bin): string
    {
        $bin = ltrim($bin, "\x00");
        if ($bin === '') {
            $bin = "\x00";
        }
        if (ord($bin[0]) & 0x80) {
            $bin = "\x00" . $bin;
        }
        return "\x02" . self::derLength(strlen($bin)) . $bin;
    }

    /**
     * DER OCTET STRING 编码
     */
    protected static function derOctetString(string $bin): string
    {
        return "\x04" . self::derLength(strlen($bin)) . $bin;
    }
}
  1. 在全局辅助函数 helpers.php 中增加函数。
use App\Utils\GmUtils;

/**
 * SM2签名,返回base64 der签名
 */
function sm2_sign(string $data): string
{
    return GmUtils::sm2Sign($data);
}

/**
 * SM2验签
 */
function sm2_verify(string $data, string $base64Sign): bool
{
    return GmUtils::sm2Verify($data, $base64Sign);
}

/**
 * SM2公钥加密 返回base64密文
 */
function sm2_encrypt(string $plain): string
{
    return GmUtils::sm2Encrypt($plain);
}

/**
 * SM2私钥解密
 */
function sm2_decrypt(string $base64Cipher): string
{
    return GmUtils::sm2Decrypt($base64Cipher);
}

/**
 * SM2解密:客户端(小程序/前端 sm-crypto)传来的裸 C1C3C2 hex 密文
 */
function sm2_decrypt_hex(string $hexCipher): string
{
    return GmUtils::sm2DecryptHex($hexCipher);
}

/**
 * SM3哈希摘要 返回小写hex
 */
function sm3_hash(string $data): string
{
    return GmUtils::sm3($data);
}

/**
 * SM4‑CBC加密 返回base64密文;可选自定义密钥/IV(32位hex),不传用配置默认值
 */
function sm4_encrypt(string $plainText, ?string $keyHex = null, ?string $ivHex = null): string
{
    return GmUtils::sm4Encrypt($plainText, $keyHex, $ivHex);
}

/**
 * SM4‑CBC解密;可选自定义密钥/IV(32位hex),不传用配置默认值
 */
function sm4_decrypt(string $base64Cipher, ?string $keyHex = null, ?string $ivHex = null): string
{
    return GmUtils::sm4Decrypt($base64Cipher, $keyHex, $ivHex);
}

/**
 * SM4‑CBC加密 返回hex密文(与小程序/前端对接用)
 */
function sm4_encrypt_hex(string $plainText, ?string $keyHex = null, ?string $ivHex = null): string
{
    return GmUtils::sm4EncryptHex($plainText, $keyHex, $ivHex);
}

/**
 * SM4‑CBC解密 入参为hex密文(与小程序/前端对接用)
 */
function sm4_decrypt_hex(string $hexCipher, ?string $keyHex = null, ?string $ivHex = null): string
{
    return GmUtils::sm4DecryptHex($hexCipher, $keyHex, $ivHex);
}
  1. 到此,算是都准备好了。进行测试和具体的业务使用了。
# 在容器中,laravel 项目根目录,执行下边的命令,测试一下

php artisan tinker --execute="var_dump(sm4_decrypt(sm4_encrypt('国密测试')), sm2_verify('hello', sm2_sign('hello')), sm2_decrypt(sm2_encrypt('hello')), sm3_hash('abc'));"

# 输出
string(12) "国密测试"
bool(true)
string(5) "hello"
string(64) "66c7f0f462eeedd9d1f2d46bdc10e4e24167c4875cf2f7a2297da02b8f4ba8e0"
  1. 其他端,要使用非对称加密 sm2 的时候,可以把公钥给过去,签名者ID 约定好就可以。其他的东西不要给过去。

sm2 只能前端加密,后端加密。没办法反着来。如果想要双向的,只能结合 sm4。

  • 前端随机生成一个 16 字节 SM4 密钥(hex)
  • 用 SM2 公钥加密这个 SM4 密钥,随请求上传(服务端 sm2_decrypt_hex 解开)
  • 之后双方用这个 SM4 密钥走 GM_SM4_KEY/IV 相同模式的 CBC 加解密(服务端已有 sm4_encrypt/sm4_decrypt)
8月 03

php artisan tinker; 使用时,报错 Error Class "Psy\Readline\Interactive\Helper\TokenHelper" not found. 处理。

https://packagist.org/packages/psy/psysh

psy 版本:Psy Shell v0.12.24

root@a2129439b046:/www/tcrj.laravel# php artisan tinker;
Psy Shell v0.12.24 (PHP 8.5.5 — cli) by Justin Hileman
New PHP manual is available (latest: 3.1.1). Update with `doc --update-manual`

tinker 版本:

root@a2129439b046:/www/tcrj.laravel# composer show laravel/tinker
name : laravel/tinker
descrip. : Powerful REPL for the Laravel framework.
keywords : REPL, Tinker, laravel, psysh
versions : * v3.0.2
released : 2026-03-17, 4 months ago
type : library
license : MIT License (MIT) (OSI approved) https://spdx.org/licenses/MIT.html#licenseText
homepage :
source : [git] https://github.com/laravel/tinker.git 4faba77764bd33411735936acdf30446d058c78b
dist : [zip] https://api.github.com/repos/laravel/tinker/zipball/4faba77764bd33411735936acdf30446d058c78b 4faba77764bd33411735936acdf30446d058c78b
path : /www/tcrj.laravel/vendor/laravel/tinker
names : laravel/tinker

进入 vendor 中,确实没看到 TokenHelper 这个类。手动去官网复制一份下来。
https://github.com/bobthecow/psysh/blob/main/src/Readline/Interactive/Helper/TokenHelper.php

进入到 vendor 目录,创建 TokenHelper.php 文件,复制代码进去。

<?php

/*
 * This file is part of Psy Shell.
 *
 * (c) 2012-2026 Justin Hileman
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Psy\Readline\Interactive\Helper;

/**
 * Shared token analysis utilities.
 */
class TokenHelper
{
    /** @var int[] Token types that indicate a statement continues on the next line */
    private const TRAILING_TOKEN_OPS = [
        \T_OBJECT_OPERATOR, \T_PAAMAYIM_NEKUDOTAYIM,
        \T_BOOLEAN_AND, \T_BOOLEAN_OR, \T_LOGICAL_AND, \T_LOGICAL_OR,
        \T_DOUBLE_ARROW, \T_COALESCE, \T_SPACESHIP,
    ];

    /** @var string[] Single-character operators that indicate continuation */
    private const TRAILING_CHAR_OPS = ['+', '-', '*', '/', '%', '.', '=', '&', '|', '^', '<', '>', ','];

    /**
     * Check whether the last non-whitespace token is a trailing operator.
     *
     * Used to determine whether a statement continues onto the next line.
     *
     * @param array $tokens token_get_all tokens
     */
    public static function hasTrailingOperator(array $tokens): bool
    {
        if (empty($tokens)) {
            return false;
        }

        $lastToken = null;
        for ($i = \count($tokens) - 1; $i >= 0; $i--) {
            $token = $tokens[$i];
            if (\is_array($token) && $token[0] === \T_WHITESPACE) {
                continue;
            }

            $lastToken = $token;
            break;
        }

        if ($lastToken === null) {
            return false;
        }

        if (\is_array($lastToken)) {
            return \in_array($lastToken[0], self::TRAILING_TOKEN_OPS, true);
        }

        return \in_array($lastToken, self::TRAILING_CHAR_OPS, true);
    }
}

再重启 composer。

composer dump-autoload -o

再使用 tinker 就可以了。

5月 08

Laravel 11Laravel Reverb 现已发布。Reverb 是 Laravel 生态系统的最新成员,是第一方、可扩展的 WebSocket 服务器,旨在为用户的应用程序提供强大的实时功能。

Laravel 11 引入了:极简应用结构、默认使用 SQLite、实现 health routing、提供每秒速率限制、支持优雅的加密密钥轮换、改进队列测试、引入新的 Artisan 命令、添加 Resend 邮件传输、集成 Prompt validator、新的 Artisan commands、Model Casts 改进、The once function、改进了使用内存数据库进行测试时的性能、改进了对 MariaDB 的支持等等,

Laravel 11 使用的 PHP 版本最低要求是 PHP 8.2。

继续阅读

4月 08

https://www.meilisearch.com/docs/learn/cookbooks/laravel_scout

构建 laravel 项目使用的工具不是 Sail 时。按照官方文档 https://laravel.com/docs/11.x/scout 来安装和使用 Scout 时,发现会报错。

cURL error 7: Failed to connect to 127.0.0.1 port 7700 after 0 ms: Couldn't connect to server (see https://curl.haxx.se/libcurl/c/libcurl-errors.html) for http://127.0.0.1:7700/indexes/resources/documents?primaryKey=id"

我这里使用的是 docker 环境。出现这个的原因是没有启动 meilisearch 服务。如果使用 sail 构建时,其会包含 meilisearch 服务。所以在这里,自己手动构建好 meilisearch 服务就可以了。docker-compose.yaml 配置如下:

  meilisearch:  
    image: getmeili/meilisearch:latest
    container_name: meilisearch_latest
    ports:  
      - "7700:7700"  
    environment:  
      - MEILI_MASTER_KEY=masterKey  
      - MEILI_NO_ANALYTICS=true  
    volumes:  
      - ./data/meilisearch:/var/lib/meilisearch  
    restart: always

其中,masterKey 为秘钥,可以自定义。

然后 laravel 的 .env 文件中,可以增加以下配置。

SCOUT_DRIVER=meilisearch  
MEILISEARCH_HOST=http://meilisearch:7700  
MEILISEARCH_KEY=masterKey

其他按照官方文档来就可以了。

12月 11

docker 的确是个好东西。该早一点就用起来。当下还在熟悉和练习中。

渐渐去丰富和完善。

.env

# 时区
TZ=Asia/Shanghai

# 项目对应文件夹
SOURCE_DIR=./www

# nginx https://nginx.org/
NGINX_NAME=nginx
NGINX_VERSION=1.25.3
NGINX_HTTP_HOST_PORT=80
NGINX_HTTPS_HOST_PORT=443
NGINX_CONFD_DIR=./conf/nginx/conf.d
NGINX_CONF_FILE=./conf/nginx/nginx.conf
NGINX_SSL_CERTIFICATE_DIR=./conf/nginx/ssl
NGINX_LOG_DIR=./logs/nginx

# php8.3
PHP_LOG_DIR=./logs/php8.3

# mysql
MYSQL_DATA=./data/mysql8.2-data
MYSQL_PORT=3306

# redis
REDIS_PORT=6379

继续阅读

10月 13

很久很久以前,第三方授权登录就开始流行。比如 qq、微博、github、微信这些。如果有一个大的平台来做这个服务的确是很方便的一个事情。在这些授权后都会有一个回调页面,就是从自己的网页跳转到授权页面,再跳转回来。但是,小程序是没提供这个服务的。但是自己可以构造类似的方式。
整个环节,代码都是由自己控制,可以很灵活的视线需求。仅仅是生成小程序码和扫码登录上小程序是腾讯那边做的。

示例:https://www.zeipan.com/admin

步骤

关键点:这里以 jwt 的认证方式来实现授权登录。

  • 登录页获取并展示生成携带参数的小程序码
  • 登录页面轮询获取登录状态(这里是 token)
  • 手机微信扫描小程序码并授权同意登录

继续阅读

9月 10

该后台使用 vite + ts + pnpm + vue3 + element-plus + tailwindcss 等技术栈构成。没有添加任意可视化图标等插件。以最小功能,最基础功能展现。用户可以额外添加可使用的插件逻辑。

该后台后端使用 php8.2 + laravel 10 + mysql

该后台后端 go 语言版本开发中。将使用 gframe2.5.2

源码: https://github.com/vini123/simpleAdmin

在线体验: https://www.zeipan.com/admin

权限以及密码一键复位: https://v3test.yuepaibao.com/admin/api/reset

测试账号以及密码: zhoulin@xiangrong.pro、 111111 (如果发现登录不了,可一键复位谢谢)

继续阅读

9月 06

既然 go 的迁移、seeder 等不好用,还是用 laravel 来搞吧。

开始

composer create-project laravel/laravel youme.com --prefer-dist

这样就创建了一个最新的 laravel 项目,项目文件在 youme.com 里。在本地,做了 host 映射。通过 youme.com 就可以打开。

上边就是默认的。

创建 git 版本控制,提交。

git init -b main
git add .
git commit -m 'laravel initialize'

继续阅读