stechstudio / laravel-bref-bridge
Bref,Laravel的方式。
Requires
- php: ^7.2
- ext-curl: *
- ext-fileinfo: *
- ext-iconv: *
- ext-json: *
- ext-mbstring: *
- ext-openssl: *
- ext-parallel: *
- ext-pcre: *
- ext-simplexml: *
- ext-tokenizer: *
- bref/bref: ^0.3.4
- gisostallenberg/file-permission-calculator: ^1.0
- illuminate/http: ^6.0|^7.0
- illuminate/support: ^6.0|^7.0
- laravel/framework: ^6.0|^7.0
- psr/http-server-handler: ^1.0
- stechstudio/aws-events: ^1.0
- symfony/psr-http-message-bridge: ^1.1
- symfony/yaml: ^4.2
Requires (Dev)
- phpstan/phpstan: ^0.11.2
- phpstan/phpstan-doctrine: ^0.11.1
This package is auto-updated.
Last update: 2020-09-10 13:19:02 UTC
README
如果您正在寻找一种轻松部署Laravel项目到AWS Lambda的方法,那么您就找到了正确的位置!
基于出色的bref项目,我们提供了一种简单便捷的方式进入AWS的Serverless Laravel世界。
安装
假设您已经在现有的Laravel项目中,让我们通过Composer安装桥接器。
$ composer require stechstudio/laravel-bref-bridge
需要发布一个路由、配置文件和AWS SAM模板。
$ artisan vendor:publish --tag=bref-routes --tag=bref-sam-template --tag=bref-configuration
配置
TL;DR
编辑.env
BREF_NAME="<my-lambdas-name>" BREF_S3_BUCKET="<bucket-name>"
$ artisan vendor:publish --tag=bref-sam-template --tag=bref-configuration $ artisan bref:config-sam $ artisan vendor:publish --tag=bref-routes $ mv routes/lambda.example.php routes/lambda.php $ artisan bref:package $ artisan bref:deploy
AWS
您需要一个S3存储桶来发送函数包,以便Cloudformation可以消费它。您可以使用现有的存储桶或创建一个新的存储桶。您可以使用AWS CLI轻松创建一个新的存储桶,如下所示。
$ aws s3 mb s3://<bucket-name>
.env
修改您的.env文件并添加以下内容:
BREF_NAME="<my-lambdas-name>" BREF_S3_BUCKET="<bucket-name>"
区域层
虽然已经为您配置了默认的us-east-1层,但最好参考https://bref.sh/docs/runtimes/,并找到您打算部署Lambda函数的区域中最新的bref层的ARN。
请注意,当您选择基本层时,我们需要一个php-??-fpm层。此桥接器不兼容php-??或console层类型。
确保区域和层匹配,如下所示
BREF_DEFAULT_REGION=ap-southeast-1 BREF_FUNCTION_LAYER_1=arn:aws:lambda:ap-southeast-1:209497400698:layer:php-73-fpm:6
SQS作业队列
我们将报告创建的默认作业队列。函数将配置为接收来自它的事件以及写入它。这意味着当您向默认队列发送作业时,它将触发相同的Lambda函数来处理作业。
SAM模板
$ artisan vendor:publish --tag=bref-sam-template
您现在可以在您的根目录中找到template.yml文件,您可以打开它,查看它,编辑它,或者现在先忽略它。完成之后,让我们运行配置命令。这将根据您的.env文件生成最终的模板。如果您在.env中进行了任何修改,您应该再次运行此命令以更新模板。
$ artisan bref:config-sam
Lambda路由
什么是Lambda路由?很高兴您问!许多人只关注来自API Gateway和/或AWS SQS的事件来触发他们的Lambda作业。然而,还有一系列可能配置为触发您的Lambda函数的事件。
我们为Laravel实现了一个路由器,它使得您的应用程序能够轻松消费和响应多个触发器的事件,全部在单个Lambda函数中完成。我们使用AWS Events Package将传入的事件转换为适当的PHP对象,然后确定将事件发送到哪个控制器。
API网关
所有API网关代理请求事件都被硬编码为任何正常网络请求的处理方式。事件将被转换为HTTP请求,并像nginx或apache一样传递给PHP-FPM。然后,结果将转换回适当的API网关代理响应,并送回网关。对于这种情况,您只需像为任何传统的Laravel应用程序一样编写HTTP路由和控制器即可,如果我们的工作做得正确,它应该会顺利运行!
其他事件
除了API网关之外,我们目前支持路由所有(十六个)其他可能的事件。如果您没有使用其他事件,您可以简单地忽略这一部分。然而,对于那些超越API网关的人来说,让我们发布示例路由文件。
$ artisan vendor:publish --tag=bref-routes
这将导致在您的项目中放置一个routes/lambda.example.php文件。在使用之前,您需要将其手动重命名为routes/lambda.php。当您查看它时,您会注意到它遵循与HTTP路由相同的范式。您可以映射一个回调或映射一个Lambda控制器。
然后,路由器将确保当出现您正在路由的事件类型时,它会被传递到适当的callable以处理该事件。您只需在完成后返回一个array即可。为了帮助您测试各种事件的路线,这里有一些示例。
AWS CloudFormation创建请求示例事件
{
"StackId": "arn:aws:cloudformation:us-west-2:EXAMPLE/stack-name/guid",
"ResponseURL": "http://pre-signed-S3-url-for-response",
"ResourceProperties": {
"StackName": "stack-name",
"List": [
"1",
"2",
"3"
]
},
"RequestType": "Create",
"ResourceType": "Custom::TestResource",
"RequestId": "unique id for this create request",
"LogicalResourceId": "MyTestResource"
}
Amazon SES接收电子邮件示例事件
{
"Records": [
{
"eventVersion": "1.0",
"ses": {
"mail": {
"commonHeaders": {
"from": [
"Jane Doe <janedoe@example.com>"
],
"to": [
"johndoe@example.com"
],
"returnPath": "janedoe@example.com",
"messageId": "<0123456789example.com>",
"date": "Wed, 7 Oct 2015 12:34:56 -0700",
"subject": "Test Subject"
},
"source": "janedoe@example.com",
"timestamp": "1970-01-01T00:00:00.000Z",
"destination": [
"johndoe@example.com"
],
"headers": [
{
"name": "Return-Path",
"value": "<janedoe@example.com>"
},
{
"name": "Received",
"value": "from mailer.example.com (mailer.example.com [203.0.113.1]) by inbound-smtp.us-west-2.amazonaws.com with SMTP id o3vrnil0e2ic for johndoe@example.com; Wed, 07 Oct 2015 12:34:56 +0000 (UTC)"
},
{
"name": "DKIM-Signature",
"value": "v=1; a=rsa-sha256; c=relaxed/relaxed; d=example.com; s=example; h=mime-version:from:date:message-id:subject:to:content-type; bh=jX3F0bCAI7sIbkHyy3mLYO28ieDQz2R0P8HwQkklFj4=; b=sQwJ+LMe9RjkesGu+vqU56asvMhrLRRYrWCbV"
},
{
"name": "MIME-Version",
"value": "1.0"
},
{
"name": "From",
"value": "Jane Doe <janedoe@example.com>"
},
{
"name": "Date",
"value": "Wed, 7 Oct 2015 12:34:56 -0700"
},
{
"name": "Message-ID",
"value": "<0123456789example.com>"
},
{
"name": "Subject",
"value": "Test Subject"
},
{
"name": "To",
"value": "johndoe@example.com"
},
{
"name": "Content-Type",
"value": "text/plain; charset=UTF-8"
}
],
"headersTruncated": false,
"messageId": "o3vrnil0e2ic28tr"
},
"receipt": {
"recipients": [
"johndoe@example.com"
],
"timestamp": "1970-01-01T00:00:00.000Z",
"spamVerdict": {
"status": "PASS"
},
"dkimVerdict": {
"status": "PASS"
},
"processingTimeMillis": 574,
"action": {
"type": "Lambda",
"invocationType": "Event",
"functionArn": "arn:aws:lambda:us-west-2:012345678912:function:Example"
},
"spfVerdict": {
"status": "PASS"
},
"virusVerdict": {
"status": "PASS"
}
}
},
"eventSource": "aws:ses"
}
]
}
计划事件示例事件
{
"account": "123456789012",
"region": "us-east-1",
"detail": {},
"detail-type": "Scheduled Event",
"source": "aws.events",
"time": "1970-01-01T00:00:00Z",
"id": "cdc73f9d-aea9-11e3-9d5a-835b769c0d9c",
"resources": [
"arn:aws:events:us-east-1:123456789012:rule/my-schedule"
]
}
Amazon CloudWatch日志示例事件
{
"awslogs": {
"data": "H4sIAAAAAAAAAHWPwQqCQBCGX0Xm7EFtK+smZBEUgXoLCdMhFtKV3akI8d0bLYmibvPPN3wz00CJxmQnTO41whwWQRIctmEcB6sQbFC3CjW3XW8kxpOpP+OC22d1Wml1qZkQGtoMsScxaczKN3plG8zlaHIta5KqWsozoTYw3/djzwhpLwivWFGHGpAFe7DL68JlBUk+l7KSN7tCOEJ4M3/qOI49vMHj+zCKdlFqLaU2ZHV2a4Ct/an0/ivdX8oYc1UVX860fQDQiMdxRQEAAA=="
}
}
Amazon SNS示例事件
{
"Records": [
{
"EventVersion": "1.0",
"EventSubscriptionArn": eventsubscriptionarn,
"EventSource": "aws:sns",
"Sns": {
"SignatureVersion": "1",
"Timestamp": "1970-01-01T00:00:00.000Z",
"Signature": "EXAMPLE",
"SigningCertUrl": "EXAMPLE",
"MessageId": "95df01b4-ee98-5cb9-9903-4c221d41eb5e",
"Message": "Hello from SNS!",
"MessageAttributes": {
"Test": {
"Type": "String",
"Value": "TestString"
},
"TestBinary": {
"Type": "Binary",
"Value": "TestBinary"
}
},
"Type": "Notification",
"UnsubscribeUrl": "EXAMPLE",
"TopicArn": topicarn,
"Subject": "TestInvoke"
}
}
]
}
Amazon DynamoDB更新示例事件
{
"Records": [
{
"eventID": "1",
"eventVersion": "1.0",
"dynamodb": {
"Keys": {
"Id": {
"N": "101"
}
},
"NewImage": {
"Message": {
"S": "New item!"
},
"Id": {
"N": "101"
}
},
"StreamViewType": "NEW_AND_OLD_IMAGES",
"SequenceNumber": "111",
"SizeBytes": 26
},
"awsRegion": "us-west-2",
"eventName": "INSERT",
"eventSourceARN": eventsourcearn,
"eventSource": "aws:dynamodb"
},
{
"eventID": "2",
"eventVersion": "1.0",
"dynamodb": {
"OldImage": {
"Message": {
"S": "New item!"
},
"Id": {
"N": "101"
}
},
"SequenceNumber": "222",
"Keys": {
"Id": {
"N": "101"
}
},
"SizeBytes": 59,
"NewImage": {
"Message": {
"S": "This item has changed"
},
"Id": {
"N": "101"
}
},
"StreamViewType": "NEW_AND_OLD_IMAGES"
},
"awsRegion": "us-west-2",
"eventName": "MODIFY",
"eventSourceARN": sourcearn,
"eventSource": "aws:dynamodb"
},
{
"eventID": "3",
"eventVersion": "1.0",
"dynamodb": {
"Keys": {
"Id": {
"N": "101"
}
},
"SizeBytes": 38,
"SequenceNumber": "333",
"OldImage": {
"Message": {
"S": "This item has changed"
},
"Id": {
"N": "101"
}
},
"StreamViewType": "NEW_AND_OLD_IMAGES"
},
"awsRegion": "us-west-2",
"eventName": "REMOVE",
"eventSourceARN": sourcearn,
"eventSource": "aws:dynamodb"
}
]
}
Amazon Cognito Sync触发器示例事件
{
"datasetName": "datasetName",
"eventType": "SyncTrigger",
"region": "us-east-1",
"identityId": "identityId",
"datasetRecords": {
"SampleKey2": {
"newValue": "newValue2",
"oldValue": "oldValue2",
"op": "replace"
},
"SampleKey1": {
"newValue": "newValue1",
"oldValue": "oldValue1",
"op": "replace"
}
},
"identityPoolId": "identityPoolId",
"version": 2
}
Amazon Kinesis数据流示例事件
{
"Records": [
{
"eventID": "shardId-000000000000:49545115243490985018280067714973144582180062593244200961",
"eventVersion": "1.0",
"kinesis": {
"partitionKey": "partitionKey-3",
"data": "SGVsbG8sIHRoaXMgaXMgYSB0ZXN0IDEyMy4=",
"kinesisSchemaVersion": "1.0",
"sequenceNumber": "49545115243490985018280067714973144582180062593244200961"
},
"invokeIdentityArn": identityarn,
"eventName": "aws:kinesis:record",
"eventSourceARN": eventsourcearn,
"eventSource": "aws:kinesis",
"awsRegion": "us-east-1"
}
]
}
Amazon S3放置示例事件
{
"Records": [
{
"eventVersion": "2.0",
"eventTime": "1970-01-01T00:00:00.000Z",
"requestParameters": {
"sourceIPAddress": "127.0.0.1"
},
"s3": {
"configurationId": "testConfigRule",
"object": {
"eTag": "0123456789abcdef0123456789abcdef",
"sequencer": "0A1B2C3D4E5F678901",
"key": "HappyFace.jpg",
"size": 1024
},
"bucket": {
"arn": bucketarn,
"name": "sourcebucket",
"ownerIdentity": {
"principalId": "EXAMPLE"
}
},
"s3SchemaVersion": "1.0"
},
"responseElements": {
"x-amz-id-2": "EXAMPLE123/5678abcdefghijklambdaisawesome/mnopqrstuvwxyzABCDEFGH",
"x-amz-request-id": "EXAMPLE123456789"
},
"awsRegion": "us-east-1",
"eventName": "ObjectCreated:Put",
"userIdentity": {
"principalId": "EXAMPLE"
},
"eventSource": "aws:s3"
}
]
}
Amazon S3删除示例事件
{
"Records": [
{
"eventVersion": "2.0",
"eventTime": "1970-01-01T00:00:00.000Z",
"requestParameters": {
"sourceIPAddress": "127.0.0.1"
},
"s3": {
"configurationId": "testConfigRule",
"object": {
"sequencer": "0A1B2C3D4E5F678901",
"key": "HappyFace.jpg"
},
"bucket": {
"arn": bucketarn,
"name": "sourcebucket",
"ownerIdentity": {
"principalId": "EXAMPLE"
}
},
"s3SchemaVersion": "1.0"
},
"responseElements": {
"x-amz-id-2": "EXAMPLE123/5678abcdefghijklambdaisawesome/mnopqrstuvwxyzABCDEFGH",
"x-amz-request-id": "EXAMPLE123456789"
},
"awsRegion": "us-east-1",
"eventName": "ObjectRemoved:Delete",
"userIdentity": {
"principalId": "EXAMPLE"
},
"eventSource": "aws:s3"
}
]
}
Amazon Lex示例事件
{
"messageVersion": "1.0",
"invocationSource": "FulfillmentCodeHook or DialogCodeHook",
"userId": "user-id specified in the POST request to Amazon Lex.",
"sessionAttributes": {
"key1": "value1",
"key2": "value2",
},
"bot": {
"name": "bot-name",
"alias": "bot-alias",
"version": "bot-version"
},
"outputDialogMode": "Text or Voice, based on ContentType request header in runtime API request",
"currentIntent": {
"name": "intent-name",
"slots": {
"slot-name": "value",
"slot-name": "value",
"slot-name": "value"
},
"confirmationStatus": "None, Confirmed, or Denied
(intent confirmation, if configured)"
}
}
Amazon SQS事件
{
"Records": [
{
"messageId": "c80e8021-a70a-42c7-a470-796e1186f753",
"receiptHandle": "AQEBJQ+/u6NsnT5t8Q/VbVxgdUl4TMKZ5FqhksRdIQvLBhwNvADoBxYSOVeCBXdnS9P+erlTtwEALHsnBXynkfPLH3BOUqmgzP25U8kl8eHzq6RAlzrSOfTO8ox9dcp6GLmW33YjO3zkq5VRYyQlJgLCiAZUpY2D4UQcE5D1Vm8RoKfbE+xtVaOctYeINjaQJ1u3mWx9T7tork3uAlOe1uyFjCWU5aPX/1OHhWCGi2EPPZj6vchNqDOJC/Y2k1gkivqCjz1CZl6FlZ7UVPOx3AMoszPuOYZ+Nuqpx2uCE2MHTtMHD8PVjlsWirt56oUr6JPp9aRGo6bitPIOmi4dX0FmuMKD6u/JnuZCp+AXtJVTmSHS8IXt/twsKU7A+fiMK01NtD5msNgVPoe9JbFtlGwvTQ==",
"body": "{\"foo\":\"bar\"}",
"attributes": {
"ApproximateReceiveCount": "3",
"SentTimestamp": "1529104986221",
"SenderId": "594035263019",
"ApproximateFirstReceiveTimestamp": "1529104986230"
},
"messageAttributes": {},
"md5OfBody": "9bb58f26192e4ba00f01e2e7b136bbd8",
"eventSource": "aws:sqs",
"eventSourceARN": "arn:aws:sqs:us-west-2:594035263019:NOTFIFOQUEUE",
"awsRegion": "us-west-2"
}
]
}
CloudFront事件
{
"Records": [
{
"cf": {
"config": {
"distributionId": "EDFDVBD6EXAMPLE"
},
"request": {
"clientIp": "2001:0db8:85a3:0:0:8a2e:0370:7334",
"method": "GET",
"uri": "/picture.jpg",
"headers": {
"host": [
{
"key": "Host",
"value": "d111111abcdef8.cloudfront.net"
}
],
"user-agent": [
{
"key": "User-Agent",
"value": "curl/7.51.0"
}
]
}
}
}
}
]
}
AWS Config事件
{
"invokingEvent": "{\"configurationItem\":{\"configurationItemCaptureTime\":\"2016-02-17T01:36:34.043Z\",\"awsAccountId\":\"000000000000\",\"configurationItemStatus\":\"OK\",\"resourceId\":\"i-00000000\",\"ARN\":\"arn:aws:ec2:us-east-1:000000000000:instance/i-00000000\",\"awsRegion\":\"us-east-1\",\"availabilityZone\":\"us-east-1a\",\"resourceType\":\"AWS::EC2::Instance\",\"tags\":{\"Foo\":\"Bar\"},\"relationships\":[{\"resourceId\":\"eipalloc-00000000\",\"resourceType\":\"AWS::EC2::EIP\",\"name\":\"Is attached to ElasticIp\"}],\"configuration\":{\"foo\":\"bar\"}},\"messageType\":\"ConfigurationItemChangeNotification\"}",
"ruleParameters": "{\"myParameterKey\":\"myParameterValue\"}",
"resultToken": "myResultToken",
"eventLeftScope": false,
"executionRoleArn": "arn:aws:iam::012345678912:role/config-role",
"configRuleArn": "arn:aws:config:us-east-1:012345678912:config-rule/config-rule-0123456",
"configRuleName": "change-triggered-config-rule",
"configRuleId": "config-rule-0123456",
"accountId": "012345678912",
"version": "1.0"
}
AWS IoT按钮事件
{
"serialNumber": "ABCDEFG12345",
"clickType": "SINGLE",
"batteryVoltage": "2000 mV"
}
Kinesis数据Firehose事件
{
"invocationId": "invoked123",
"deliveryStreamArn": "aws:lambda:events",
"region": "us-west-2",
"records": [
{
"data": "SGVsbG8gV29ybGQ=",
"recordId": "record1",
"approximateArrivalTimestamp": 1510772160000,
"kinesisRecordMetadata": {
"shardId": "shardId-000000000000",
"partitionKey": "4d1ad2b9-24f8-4b9d-a088-76e9947c317a",
"approximateArrivalTimestamp": "2012-04-23T18:25:43.511Z",
"sequenceNumber": "49546986683135544286507457936321625675700192471156785154",
"subsequenceNumber": ""
}
},
{
"data": "SGVsbG8gV29ybGQ=",
"recordId": "record2",
"approximateArrivalTimestamp": 151077216000,
"kinesisRecordMetadata": {
"shardId": "shardId-000000000001",
"partitionKey": "4d1ad2b9-24f8-4b9d-a088-76e9947c318a",
"approximateArrivalTimestamp": "2012-04-23T19:25:43.511Z",
"sequenceNumber": "49546986683135544286507457936321625675700192471156785155",
"subsequenceNumber": ""
}
}
]
}
在您将包发布到lambda之后,您可以前往AWS控制台,将各种示例复制粘贴到测试中并手动运行它们。如果您想从AWS CLI中进行测试,也可以这样做。
打包与部署
我们使这一过程尽可能简单。
$ artisan bref:package
这将生成一个storage/latest.zip包,包含您的当前代码。这里不会有来自composer的开发包,所以如果您需要这些包,您需要将它们移动到所需的段落。
$ artisan bref:deploy
几分钟后,任务将完成,您可以前往AWS控制台查看您的新Lambda作业。
就这样,恭喜您!
配置选项
config/bref.php文件包含我们的配置选项。这是可用设置的概述。有关更多详细信息和默认值,请参阅文件中的注释。
- 名称 - 此值是您的 Lambda 名称。当框架需要生成 Lambda 函数名称时使用此值。
- 描述 - 此值是您的 Lambda 描述。当框架需要生成 Lambda 函数描述时使用此值。
- 区域 - 此值是您的 Lambda 区域。当框架需要生成 Lambda 函数区域时使用此值。
- 超时时间 - 此值是配置 Lambda 函数的超时时间(以秒为单位)。API Gateway 超时时间为 30 秒,因此这是默认值。最大超时时间为 900 秒(15 分钟)。
- 内存大小 - 函数可访问的内存量。增加函数的内存也会增加其 CPU 配额。默认值为 128 MB,最大值为 3,008 MB。此值必须是 64 MB 的整数倍。
- 层 - 要添加到函数执行环境的函数层列表。指定每个层时,请按它们应堆叠的顺序,使用 ARN(包括版本),最多五层。
- 保留 - 在文件系统中保留的最新(zip)包的数量。
- SQS - Lambda 消耗作业队列在此配置。向队列发布作业仍然按正常工作,所以这里没有变化。只需更新 .env 即可。
- 打包 - 此数组配置在打包您的应用程序时应忽略的文件,以及标识可执行文件。
- 环境变量 - 此数组配置传递给函数代码的环境变量,并列出那些被忽略的变量。
.env 变量
以下 .env 变量可供使用。有关更多详细信息,请参考 config/bref.php。
- BREF_NAME - 此值是您的 Lambda 名称。当框架需要生成 Lambda 函数名称时使用此值。
- BREF_DESCRIPTION - 此值是您的 Lambda 描述。当框架需要生成 Lambda 函数描述时使用此值。
- BREF_DEFAULT_REGION - 此值是您的 Lambda 区域。当框架需要生成 Lambda 函数区域时使用此值。
- BREF_FUNCTION_TIMEOUT - 此值是配置 Lambda 函数的超时时间(以秒为单位)。API Gateway 超时时间为 30 秒,因此这是默认值。最大超时时间为 900 秒(15 分钟)。
- BREF_FUNCTION_MEMORY_SIZE - 函数可访问的内存量。增加函数的内存也会增加其 CPU 配额。默认值为 128 MB,最大值为 3,008 MB。此值必须是 64 MB 的整数倍。
- BREF_FUNCTION_LAYER_1 - 第一个层的 ARN(包括版本)。这将覆盖默认层。
- BREF_FUNCTION_LAYER_2 - 第二个层的 ARN(包括版本),如果使用的话。
- BREF_FUNCTION_LAYER_3 - 第三个层的 ARN(包括版本),如果使用的话。
- BREF_FUNCTION_LAYER_4 - 第四个层的 ARN(包括版本),如果使用的话。
- BREF_FUNCTION_LAYER_5 - 第五个层的 ARN(包括版本),如果使用的话。
- BREF_PACKAGE_KEEP - 在文件系统中保留的最新(zip)包的数量。
- BREF_APP_STORAGE - Laravel 应用存储的位置。默认为
/tmp/storage。 - BREF_LOG_CHANNEL - 在 Lambda 中处理日志的方式。默认为
stderr。 - BREF_CACHE_DRIVER - 在 Lambda 中使用的缓存驱动程序。默认为
file。 - BREF_SESSION_DRIVER - 在 Lambda 中使用的 Session 驱动程序。默认为
array。 - BREF_QUEUE_CONNECTION - 在 Lambda 中使用的队列连接。默认为
sqs。
启用扩展
如果您想启用扩展或修改php.ini指令,可以通过创建一个./storage/php/conf.d目录来实现。您放入该目录中的任何内容都将被打包到/var/task/php/config.d,这是bref的默认设置。
例如,要启用来自基础bref层的pdo_mysql,只需创建一个类似的./storage/php/conf.d/mysql.ini文件
extension=pdo_mysql
参见:https://bref.sh/docs/environment/php.html#customizing-phpini https://bref.sh/docs/environment/php.html#extensions-installed-but-disabled-by-default