本文目录导读:

针对PHP项目与Dataverse(微软的云数据服务)以及存储的整合,这是一个典型的云数据库 + 对象存储(或文件存储)的混合架构需求。
鉴于你问的是“与存储”,这通常包含两个层面:
- 结构化数据存储:使用 Dataverse(通过 Dataverse Web API 或 OData 协议)。
- 非结构化/文件存储:使用 Azure Blob Storage(Dataverse 的附件/备注默认使用的后端),或直接使用本地/第三方存储(S3, MinIO)。
以下为你梳理最核心的整合方案、代码示例和关键注意事项。
与 Dataverse(结构化数据)的整合
Dataverse 本质是一个基于 Dynamics 365 和 Power Platform 的、具有强关系的商业数据库,PHP 与其交互的唯一标准方式是 RESTful OData v4 API。
核心通信方式
- 协议:HTTPS + OData v4.0 JSON。
- 认证:必须使用 OAuth 2.0(客户端凭证流 或 授权码流)。绝不能使用用户名/密码(因为它需要 Azure AD 条件访问)。
- 依赖库:
php-http/guzzle6-adapter或guzzlehttp/guzzle(HTTP 客户端)。microsoft/microsoft-graph(虽然主要是 Graph API,但可辅助认证)。- 推荐自行封装或使用轻量的 OData 客户端库,如
sainsburys/guzzle-oauth2-subscriber。
关键代码流程(创建记录示例)
<?php
require 'vendor/autoload.php';
use GuzzleHttp\Client;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Middleware;
use GuzzleHttp\Psr7\Request;
// --- 1. 获取 OAuth 2.0 Token (Client Credentials) ---
function getAccessToken($tenantId, $clientId, $clientSecret) {
$url = "https://login.microsoftonline.com/{$tenantId}/oauth2/v2.0/token";
$client = new Client();
$response = $client->post($url, [
'form_params' => [
'grant_type' => 'client_credentials',
'client_id' => $clientId,
'client_secret' => $clientSecret,
'scope' => 'https://orgxxxxxx.api.crm.dynamics.com/.default' // 替换为你的环境URL
]
]);
$data = json_decode($response->getBody(), true);
return $data['access_token'];
}
// --- 2. 配置 Dataverse 环境 ---
$orgUrl = 'https://orgxxxxxx.api.crm.dynamics.com'; // 你的 Dataverse 组织URL
$accessToken = getAccessToken('你的TenantID', '你的ClientID', '你的ClientSecret');
// --- 3. 使用 Guzzle 调用 Dataverse API ---
$client = new Client([
'base_uri' => $orgUrl,
'headers' => [
'Authorization' => 'Bearer ' . $accessToken,
'Accept' => 'application/json',
'OData-MaxVersion' => '4.0',
'OData-Version' => '4.0',
'Content-Type' => 'application/json; charset=utf-8',
]
]);
// --- 4. 示例:创建一条客户记录 (Account实体) ---
$data = [
'name' => '示例客户名称',
'telephone1' => '010-12345678',
'creditlimit' => 100000.00,
];
try {
$response = $client->post('/api/data/v9.2/accounts', [
'json' => $data
]);
if ($response->getStatusCode() == 204) {
// 成功,获取记录ID (从响应头 Location 获取)
$locationHeader = $response->getHeaderLine('Location');
preg_match('/accounts\\(([^)]+)\\)/', $locationHeader, $matches);
$accountId = $matches[1] ?? null;
echo "创建成功! Account ID: " . $accountId;
} else {
echo "创建失败: " . $response->getBody();
}
} catch (\GuzzleHttp\Exception\ClientException $e) {
echo "HTTP Error: " . $e->getResponse()->getStatusCode() . "\n";
echo $e->getResponse()->getBody(); // 通常包含更详细的错误信息
}
关键注意事项
- API 版本:使用
v9.2(当前最新稳定版)。 - 字段映射:Dataverse 的字段名通常是逻辑名(
new_fieldname),需要确认。 - 分页:使用
$top和$skip或nextLink(对于超过5000条的数据)。 - Batch 操作:对于大量写入,使用
$batch请求(支持事务)。 - 错误处理:Dataverse 返回的
@Microsoft.PowerApps.CDS.ErrorDetails非常有用。
与存储(非结构化数据/文件)的整合
Dataverse 本身不直接暴露文件存储,但有两种主要方式:
方案 A:使用 Dataverse 的“注解/附件”机制(推荐)
- 原理:通过 Dataverse API 的文件列类型(File Datatype)或 Annotation 实体。
- 优点:数据与业务记录硬关联,无需自己管理存储路径。
- 缺点:文件大小和类型有限制(128MB 以内),且需要走 Dataverse API 的中转。
上传文件到 Dataverse 实体(伪代码思路):
// 需要先创建记录,然后上传文件到该记录的指定文件列
// 1. 创建记录 (省略,同上)
// 2. 获取文件上传URL
$fileColumnName = 'myfile'; // 实体中定义的文件列名称
$recordId = 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx';
$entityName = 'accounts';
$response = $client->post("/api/data/v9.2/{$entityName}({$recordId})/{$fileColumnName}", [
'headers' => ['Content-Type' => 'application/octet-stream'],
'body' => fopen('/path/to/large/file.pdf', 'r') // 直接流式上传
]);
// 实际需要更复杂的两步操作(初始化上传+上传块),参考 Microsoft 官方文档
方案 B:直接使用外部对象存储(Azure Blob / AWS S3 / MinIO)
- 适用场景:大文件(> 128MB)、高并发、或希望存储解耦。
- 方法:
- PHP 生成签名 URL(SAS Token 或 Presigned URL)。
- 前端/客户端直接上传到存储服务。
- Dataverse 中只存储文件 URL 或路径(例如自定义字段)。
- 关键库:
- Azure Blob:
microsoft/azure-storage-blob - AWS S3:
aws/aws-sdk-php - MinIO:
aws/aws-sdk-php(兼容 S3 API)
- Azure Blob:
PHP 生成 Azure Blob SAS Token 示例:
use MicrosoftAzure\Storage\Blob\BlobRestProxy;
use MicrosoftAzure\Storage\Common\SharedAccessSignatureHelper;
use MicrosoftAzure\Storage\Blob\Models\CreateContainerOptions;
use MicrosoftAzure\Storage\Blob\Models\PublicAccessType;
// 1. 获取 Blob 客户端
$connectionString = "DefaultEndpointsProtocol=https;AccountName=你的存储账户;AccountKey=你的密钥";
$blobClient = BlobRestProxy::createBlobService($connectionString);
// 2. 生成 SAS Token (1 小时有效)
$sasHelper = new SharedAccessSignatureHelper(
'你的存储账户',
'你的密钥'
);
$sasToken = $sasHelper->generateAccountSharedAccessSignatureToken(
'2023-01-01T00:00:00Z', // 起始时间
'2023-12-31T23:59:59Z', // 过期时间
'rw', // 权限 (read, write)
'hb', // 资源类型 (blob, container)
'https' // 允许协议
);
// 3. 生成可上传的完整 URL
$containerName = 'mycontainer';
$blobName = 'user_upload_' . time() . '.pdf';
$uploadUrl = "https://你的存储账户.blob.core.windows.net/{$containerName}/{$blobName}?{$sasToken}";
// 返回给前端:前端直接使用这个URL进行PUT上传
架构模式建议:Dataverse + 存储的联动
在实际项目中,典型的“Dataverse 与存储”协作模式如下:
- 关键数据:客户信息、订单、产品定义 -> 存储于 Dataverse(结构化)。
- 用户文件:合同、PDF、图片 -> 存储于 Azure Blob(或 S3/MinIO)。
- 关联方式:在 Dataverse 的实体中(如合同实体)添加一个
CustomURLField或FileUrl字段,指向 Blob 中的文件。 - 安全控制:
- 直接访问存储由 SAS Token 控制失效时间。
- 读取业务数据由 Dataverse OAuth Token 控制。
环境考虑与部署注意事项
- PHP 版本:推荐 PHP 8.1+,因为 Dataverse API 返回强类型 JSON 和自定义 OData 序列化。
- 时区:Dataverse 存储的是 UTC 时间,PHP 端需要处理
DateTimeZone转换。 - 性能:避免在 PHP 中循环调用 Dataverse API,尽量使用
$batch批量操作。 - 存储优化:如果使用 Dataverse 的附件功能,务必定期清理
annotationbase表,避免 Dataverse 数据库膨胀。
- 已包含的存储(Dataverse 自带的附件/文件列):通过标准的 REST API + OAuth 认证,适合<128MB的文件,与业务记录强绑定。
- 独立的存储(外部 Blob/S3):适用于大型文件、高并发,PHP 负责生成凭证(SAS/Pre-signed URL),并负责更新 Dataverse 中的引用字段。
建议优先使用外部存储(方案 B),因为 Dataverse 的 I/O 性能不是为高吞吐文件服务设计的,且成本较高,如果你的场景是记录几十MB的PDF合同,且数量不大,可以使用方案 A(注解),如果涉及大文件、视频或海量小文件,必选方案 B。