我目前正在开发私有(private) rest api,在发布以创建父级资源时遇到了子资源相互依赖的问题。
例如..
我有以下父端点..
/products
以及以下与/products 相关的子端点..
/products/[PRODUCT_ID]/categories
/products/[PRODUCT_ID]/media
/products/[PRODUCT_ID]/shippingZones
/products/[PRODUCT_ID]/variants
使用此 rest api,/products 还能够接受以下键的发布有效负载:'categories'、'media'、'shippingZones'、'variants'。
如果设置了这些键中的任何一个,/products 端点将向下钻取,并根据负载键分别为/products 上的当前发布请求创建和关联子资源。
下面是针对/products 的 POST 请求执行的代码,显示了目前的处理方式。在您花点时间浏览下面的内容后,我会着手解决问题,也许您会在我解释之前看到问题?
protected function post()
{
if (!$this->validatePermissions()) {
return;
}
$productsM = new productsModel();
$filesM = new filesModel();
$userId = $this->controller->user['id'];
$productId = $this->getResourceIdByName('products');
$productCategories = $this->controller->payload['productCategories'];
$productMedia = $this->controller->payload['productMedia'];
$productShippingZones = $this->controller->payload['productShippingZones'];
$productVariants = $this->controller->payload['productVariants'];
$existingProduct = ($productId) ? $productsM->getSingle(array( 'id' => $productId, 'userId' => $userId )) : array();
$product = array_merge($existingProduct, $this->controller->getFilteredPayload(array(
'title',
'description',
'shippingType',
'fileId',
'hasVariants',
'isHidden'
)));
$this->validateParameters(array( 'title' => $product['title'] ));
if ($productId && !$existingProduct) {
$this->addResponseError('productId');
}
if ($product['shippingType'] && !in_array($product['shippingType'], array( 'free', 'flat', 'calculated' ))) {
$this->addResponseError('shippingType');
}
if ($product['fileId'] && !$filesM->getNumRows(array( 'id' => $product['fileId'], 'userId' => $userId ))) {
$this->addResponseError('fileId');
}
if ($this->hasResponseErrors()) {
return;
}
$lastCreatedProduct = (!$existingProduct) ? $productsM->getSingle(array( 'userId' => $userId ), array( 'publicId' => 'DESC' )) : array();
$product = $productsM->upsert(array( 'id' => $productId, 'userId' => $userId ), array_merge($product, array(
'publicId' => $lastCreatedProduct['publicId'] + 1,
'userId' => $userId,
'isActive' => 1,
'modified' => time(),
'created' => time()
)), array( 'publicId' ));
// product categories subresource
if (is_array($productCategories)) {
foreach ($productCategories as $index => $productCategory) {
$endpoint = "/products/{$product['id']}/categories/{$productCategory['id']}";
$requestMethod = ($productCategory['isActive'] !== '0') ? endpoint::REQUEST_METHOD_POST : endpoint::REQUEST_METHOD_DELETE;
$productCategory = $this->executeEndpointByPath($endpoint, $requestMethod, $productCategory);
foreach ($productCategory['errors'] as $error) {
$this->addResponseError($error['parameter'], $error['message'], array( 'productCategories', $index, $error['parameter'] ));
}
$product['productCategories'][$index] = $productCategory['data'];
}
}
// product media subresource
if (is_array($productMedia)) {
foreach ($productMedia as $index => $media) {
$endpoint = "/products/{$product['id']}/media/{$media['id']}";
$requestMethod = ($media['isActive'] !== '0') ? endpoint::REQUEST_METHOD_POST : endpoint::REQUEST_METHOD_DELETE;
$media = $this->executeEndpointByPath($endpoint, $requestMethod, $media);
foreach ($media['errors'] as $error) {
$this->addResponseError($error['parameter'], $error['message'], array( 'productMedia', $index, $error['parameter'] ));
}
$product['productMedia'][$index] = $media['data'];
}
}
// product shipping zones subresource
if (is_array($productShippingZones)) {
foreach ($productShippingZones as $index => $productShippingZone) {
$endpoint = "/products/{$product['id']}/shippingZones/{$productShippingZone['id']}";
$requestMethod = ($productShippingZone['isActive'] !== '0') ? endpoint::REQUEST_METHOD_POST : endpoint::REQUEST_METHOD_DELETE;
$productShippingZone = $this->executeEndpointByPath($endpoint, $requestMethod, $productShippingZone);
foreach ($productShippingZone['errors'] as $error) {
$this->addResponseError($error['parameter'], $error['message'], array( 'productShippingZones', $index, $error['parameter'] ));
}
$product['productShippingZones'][$index] = $productShippingZone['data'];
}
}
// product variants subresource
if (is_array($productVariants)) {
foreach ($productVariants as $index => $productVariant) {
$endpoint = "/products/{$product['id']}/variants/{$productVariant['id']}";
$requestMethod = ($productVariant['isActive'] !== '0') ? endpoint::REQUEST_METHOD_POST : endpoint::REQUEST_METHOD_DELETE;
$productVariant = $this->executeEndpointByPath($endpoint, $requestMethod, $productVariant);
foreach ($productVariant['errors'] as $error) {
$this->addResponseError($error['parameter'], $error['message'], array( 'productVariants', $index, $error['parameter'] ));
}
$product['productVariants'][$index] = $productVariant['data'];
}
}
return $product;
}
好的!现在问题来了。通过此流程,在新创建的产品上为/products 创建子资源取决于插入到产品数据库表中的新行并在向下迭代和创建子资源之前返回产品 ID,因为子资源将抛出错误如果没有在他们的端点 uri 中传递 productId。
这会产生相互依赖和维护 ALL or NOTHING 原则的问题。
如果创建新产品并完成初始/products 错误检查,产品将在产品数据库表中获得一个新行。但是,如果在完成此操作后继续创建子资源并由于初始请求中传递的数据而导致任何子资源创建失败,初始请求仅部分成功,因为来自这些子资源的错误将阻止创建特定错误的子资源并将其与最初创建的产品相关联。
所以这是我的一些想法..
我想潜在地实现一种试运行方法,该方法完全忽略插入/更新并通过所有父/子端点错误处理运行数据以查看数据是否干净。我不完全确定如何将其合并到端点流中而不会使代码流过于复杂和破坏代码流的可读性。
任何其他可以解决此问题的想法或对执行流程的更改都将非常感激,因为它们可以让我指出最佳方法的正确方向。
谢谢!
最佳答案
您遇到的问题是事务处理。最终必须有一些东西来处理事务和回滚。是的,这在您的 PHP 程序中可能很痛苦,因为您必须取回 ID,但如果这些是真正的交易,则当产品未首先插入时您一定会失败。
如果对您来说更容易,一个选择是将事务处理推送到持久层(数据库?)。存储过程是这里的一个选项,因为可以将过程设置为通过或失败,然后发送回正确的代码以指示发生了什么(如果需要,可能还有错误信息)。关系数据库通常有相当简单的方法来建立这种类型的处理,而 NoSQL 则被击中或失败。
在这种情况下,PHP 变得更简单,但您开始维护一些数据库代码。
您可以设置试运行,取消任何约束并运行数据。这样做可能对您有一定的值(value)。但这并不能解决您仍将面临的“系统能否正常工作”的问题。该方法是否有值(value)取决于您将从练习中获得哪些信息。如果做得好,您将创建一个准单元测试,它应该会产生一些值(value)。
关于php - REST API - 相互依赖的端点试运行流程?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/37754944/