Skip to content

Instantly share code, notes, and snippets.

@Wiejeben
Created December 19, 2017 15:39
Show Gist options
  • Save Wiejeben/5938e29900cacfead6753ba2888353eb to your computer and use it in GitHub Desktop.
Save Wiejeben/5938e29900cacfead6753ba2888353eb to your computer and use it in GitHub Desktop.
Skip form validation for disabled variants. Also skip form validation for the default variant if the product is disabled.
<?php
/**
* User: Maarten de Graaf
* Date: 19/12/2017
* Time: 15:36
*/
namespace Craft;
use Commerce\Interfaces\Purchasable;
/**
* This class overwrites the saving behaviour for the default variant of a product.
* Normally when saving an enabled element, form validation is being accounted for.
* However when an element is supposedly disabled, it should skip validation.
* Default variants cannot be disabled, therefore we need to overwrite this behaviour.
*/
class Exact_PurchasablesService extends BaseApplicationComponent
{
/**
* Saves product and bypass form validation for the default variant if the product is disabled.
*
* @param Commerce_ProductModel $product
*
* @return bool
* @throws Exception
* @throws \Exception
*/
public function saveProduct(Commerce_ProductModel $product)
{
$productsService = craft()->commerce_products;
$isNewProduct = !$product->id;
if (!$product->id) {
$record = new Commerce_ProductRecord();
} else {
$record = Commerce_ProductRecord::model()->findById($product->id);
if (!$record) {
throw new Exception(Craft::t('No product exists with the ID “{id}”',
['id' => $product->id]));
}
}
// Fire an 'onBeforeSaveProduct' event
$event = new Event($productsService, [
'product' => $product,
'isNewProduct' => $isNewProduct
]);
$productsService->onBeforeSaveProduct($event);
$record->postDate = $product->postDate;
$record->expiryDate = $product->expiryDate;
$record->typeId = $product->typeId;
$record->promotable = $product->promotable;
$record->freeShipping = $product->freeShipping;
$record->taxCategoryId = $product->taxCategoryId;
$record->shippingCategoryId = $product->shippingCategoryId;
$record->validate();
$product->addErrors($record->getErrors());
$productType = craft()->commerce_productTypes->getProductTypeById($product->typeId);
if(!$productType){
throw new Exception(Craft::t('No product type exists with the ID “{id}”',
['id' => $product->typeId]));
}
$taxCategoryIds = array_keys($productType->getTaxCategories());
if (!in_array($product->taxCategoryId, $taxCategoryIds))
{
$record->taxCategoryId = $product->taxCategoryId = $taxCategoryIds[0];
}
$shippingCategoryIds = array_keys($productType->getShippingCategories());
if (!in_array($product->shippingCategoryId, $shippingCategoryIds))
{
$record->shippingCategoryId = $product->shippingCategoryId = $shippingCategoryIds[0];
}
// Final prep of variants and validation
$variantsValid = true;
$defaultVariant = null;
foreach ($product->getVariants() as $variant) {
// Use the product type's titleFormat if the title field is not shown
if (!$productType->hasVariantTitleField && $productType->hasVariants)
{
try
{
$variant->getContent()->title = craft()->templates->renderObjectTemplate($productType->titleFormat, $variant);
}catch(\Exception $e){
$variant->getContent()->title = "";
}
}
if(!$productType->hasVariants)
{
// Since VariantModel::getTitle() returns the parent products title when the product has
// no variants, lets save the products title as the variant title anyway.
$variant->getContent()->title = $product->getTitle();
}
// If we have a blank SKU, generate from product type's skuFormat
if(!$variant->sku){
try
{
if (!$productType->hasVariants)
{
$variant->sku = craft()->templates->renderObjectTemplate($productType->skuFormat, $product);
}
else
{
$variant->sku = craft()->templates->renderObjectTemplate($productType->skuFormat, $variant);
}
}catch(\Exception $e){
CommercePlugin::log("Could not generate SKU format: ".$e->getMessage(), LogLevel::Warning, true);
$variant->sku = "";
}
}
// Make the first variant (or the last one that says it isDefault) the default.
if ($defaultVariant === null || $variant->isDefault)
{
$defaultVariant = $variant;
}
if ($this->shouldValidateContent($variant) && !craft()->commerce_variants->validateVariant($variant)) {
$variantsValid = false;
// If we have a title error but hide the title field, put the error onto the sku.
if($variant->getError('title') && !$productType->hasVariantTitleField && $productType->hasVariants){
$variant->addError('sku',Craft::t('Could not generate the variant title from product type’s title format.'));
}
if($variant->getError('title') && !$productType->hasVariants){
$product->addError('title',Craft::t('Title cannot be blank.'));
}
}
}
if ($product->hasErrors() || !$variantsValid)
{
return false;
}
$transaction = craft()->db->getCurrentTransaction() === null ? craft()->db->beginTransaction() : null;
try {
$record->defaultVariantId = $product->defaultVariantId = $defaultVariant->getPurchasableId();
$record->defaultSku = $product->defaultSku = $defaultVariant->getSku();
$record->defaultPrice = $product->defaultPrice = (float) $defaultVariant->price;
$record->defaultHeight = $product->defaultHeight = (float) $defaultVariant->height;
$record->defaultLength = $product->defaultLength = (float) $defaultVariant->length;
$record->defaultWidth = $product->defaultWidth = (float) $defaultVariant->width;
$record->defaultWeight = $product->defaultWeight = (float) $defaultVariant->weight;
if ($event->performAction)
{
$success = craft()->elements->saveElement($product);
if ($success)
{
// Now that we have an element ID, save it on the other stuff
if ($isNewProduct)
{
$record->id = $product->id;
}
$record->save(false);
$keepVariantIds = [];
$oldVariantIds = craft()->db->createCommand()
->select('id')
->from('commerce_variants')
->where('productId = :productId', [':productId' => $product->id])
->queryColumn();
foreach ($product->getVariants() as $variant)
{
if ($defaultVariant === $variant)
{
$variant->isDefault = true;
$variant->enabled = true; // default must always be enabled.
}
else
{
$variant->isDefault = false;
}
$variant->setProduct($product);
$this->saveVariant($variant);
// Need to manually update the product's default variant ID now that we have a saved ID
if ($product->defaultVariantId === null && $defaultVariant === $variant)
{
$product->defaultVariantId = $variant->id;
craft()->db->createCommand()->update('commerce_products', ['defaultVariantId' => $variant->id], ['id' => $product->id]);
}
$keepVariantIds[] = $variant->id;
}
foreach (array_diff($oldVariantIds, $keepVariantIds) as $deleteId)
{
craft()->commerce_variants->deleteVariantById($deleteId);
}
if ($transaction !== null)
{
$transaction->commit();
}
}
}else{
$success = false;
}
} catch (\Exception $e) {
if ($transaction !== null)
{
$transaction->rollback();
}
throw $e;
}
if ($success)
{
// Fire an 'onSaveProduct' event
$productsService->onSaveProduct(new Event($productsService, [
'product' => $product,
'isNewProduct' => $isNewProduct
]));
}
return $success;
}
/**
* @param Commerce_VariantModel $variant
*
* @return bool
*/
public function validateVariant(Commerce_VariantModel $variant)
{
$variant->clearErrors();
$record = $this->_getVariantRecord($variant);
$this->_populateVariantRecord($record, $variant);
$record->validate();
$variant->addErrors($record->getErrors());
if (!craft()->content->validateContent($variant))
{
$variant->addErrors($variant->getContent()->getErrors());
}
// If variant validation has not already found a clash check all purchasables
if (!$variant->getError('sku'))
{
$existing = craft()->commerce_purchasables->getPurchasableBySku($variant->sku);
if ($existing)
{
if ($existing->id != $variant->id)
{
$variant->addError('sku', Craft::t('SKU has already been taken by another purchasable.'));
}
}
}
return !$variant->hasErrors();
}
/**
* Persists a variant.
*
* @param BaseElementModel $model
*
* @return bool
* @throws \CDbException
* @throws \Exception
*/
public function saveVariant(BaseElementModel $model)
{
$record = $this->_getVariantRecord($model);
$this->_populateVariantRecord($record, $model);
$record->validate();
$model->addErrors($record->getErrors());
$transaction = craft()->db->getCurrentTransaction() === null ? craft()->db->beginTransaction() : null;
try
{
if (!$model->hasErrors())
{
if ($this->saveElement($model))
{
$record->id = $model->id;
$record->save(false);
if ($transaction !== null)
{
$transaction->commit();
}
return true;
}
}
}
catch (\Exception $e)
{
if ($transaction !== null)
{
$transaction->rollback();
}
throw $e;
}
if ($transaction !== null)
{
$transaction->rollback();
}
return false;
}
/**
* Saves the element and the purchasable. Use this function where you would usually
* use `craft()->elements->saveElement()`
*
* @param BaseElementModel $model
*
* @return bool
* @throws \Exception
*/
public function saveElement(BaseElementModel $model)
{
if (!$model instanceof Purchasable) {
throw new Exception('Trying to save a purchasable element that is not a purchasable.');
}
$transaction = craft()->db->getCurrentTransaction() === null ? craft()->db->beginTransaction() : null;
try {
if ($success = craft()->elements->saveElement($model, $this->shouldValidateContent($model))) {
$id = $model->getPurchasableId();
$price = $model->getPrice();
$sku = $model->getSku();
$purchasable = Commerce_PurchasableRecord::model()->findById($id);
if (!$purchasable) {
$purchasable = new Commerce_PurchasableRecord();
}
$purchasable->id = $id;
$purchasable->price = $price;
$purchasable->sku = $sku;
$success = $purchasable->save();
if (!$success) {
$model->addErrors($purchasable->getErrors());
if ($transaction !== null)
{
$transaction->rollback();
}
return $success;
}
if ($transaction !== null)
{
$transaction->commit();
}
}
} catch (\Exception $e) {
if ($transaction !== null)
{
$transaction->rollback();
}
throw $e;
}
return $success;
}
/**
* Whether or not the contents have to be validated in order to be saved.
*
* @param BaseElementModel $model
* @return bool|null
*/
public function shouldValidateContent(BaseElementModel $model)
{
if ($model instanceof Commerce_VariantModel && $model->isDefault) {
return $model->getProduct()->enabled;
}
return $model->enabled;
}
/**
* @param BaseElementModel $model
*
* @return BaseRecord|Commerce_VariantRecord
* @throws HttpException
*/
private function _getVariantRecord(BaseElementModel $model)
{
if ($model->id)
{
$record = Commerce_VariantRecord::model()->findById($model->id);
if (!$record)
{
throw new HttpException(404);
}
}
else
{
$record = new Commerce_VariantRecord();
}
return $record;
}
/**
* @param $record
* @param Commerce_VariantModel $model
*/
private function _populateVariantRecord($record, Commerce_VariantModel $model)
{
$record->productId = $model->productId;
$record->sku = $model->sku;
$record->price = $model->price;
$record->width = (float) $model->width;
$record->height = (float) $model->height;
$record->length = (float) $model->length;
$record->weight = (float) $model->weight;
$record->minQty = $model->minQty;
$record->maxQty = $model->maxQty;
$record->stock = $model->stock;
$record->isDefault = $model->isDefault;
$record->sortOrder = $model->sortOrder;
$record->unlimitedStock = $model->unlimitedStock;
if (!$model->getProduct()->getType()->hasDimensions)
{
$record->width = $model->width = 0;
$record->height = $model->height = 0;
$record->length = $model->length = 0;
$record->weight = $model->weight = 0;
}
if ($model->unlimitedStock && $record->stock == "")
{
$model->stock = 0;
$record->stock = 0;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment