Skip to content

Commit

Permalink
Add extended query / collate job
Browse files Browse the repository at this point in the history
  • Loading branch information
Tam committed Jun 17, 2019
1 parent 0e66d59 commit ff40dba
Show file tree
Hide file tree
Showing 9 changed files with 230 additions and 16 deletions.
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
## [Unreleased] 1.1.0
## 1.1.0 - 2019-06-17
### Added
- Added "Extended" product query (requires fresh install)
- Added job to collate purchase pattern data on plugin install
- Added "Purchased With" section to product edit sidebar

## 1.0.0 - 2018-04-18
Expand Down
16 changes: 13 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@ Purchasable via the Plugin Store.
Use the `craft.purchasePatterns.related` function in your templates to get related products that customers also bought.

```php
ProductQuery related ( Product|Order $target [, int $limit = 8 [, ProductQuery $paddingQuery = null ] ] )
ProductQueryExtended related ( Product|Order $target [, int $limit = 8 [, ProductQuery $paddingQuery = null ] ] )
```

The function returns a `ProductQuery`, so you can include additional query parameters as needed. The `id` parameter is already set and shouldn't be overridden.
The function returns a `ProductQueryExtended`, so you can include additional query parameters as needed. The `id` parameter is already set and shouldn't be overridden.

```twig
{% set customersAlsoBought = craft.purchasePatterns.related(
Expand All @@ -40,4 +40,14 @@ The `paddingQuery` allows you to specify a `ProductQuery` that will be used to p
).orderBy('random()').all() %}
```

**Editor's Note:** `random()` is Postgres specific. Use `RAND()` for MySQL.
**Editor's Note:** `random()` is Postgres specific. Use `RAND()` for MySQL.

### Extended Product Query

The extended product query allows you to sort the products by `qtyCount` and `orderCount`.
It extends the base product query, to you can use it the same way as you would `craft.products`.
The `related` query above returns an extended query, as does:

```twig
{% set products = craft.purchasePatterns.extended().orderBy('orderCount DESC').all() %}
```
8 changes: 8 additions & 0 deletions src/PurchasePatterns.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
use craft\events\RegisterComponentTypesEvent;
use craft\services\Dashboard;
use craft\web\twig\variables\CraftVariable;
use ether\purchasePatterns\jobs\PopulateDataJob;
use ether\purchasePatterns\widgets\BoughtTogether;
use Twig\Error\LoaderError;
use Twig\Error\RuntimeError;
Expand Down Expand Up @@ -98,6 +99,13 @@ public function getService (): Service
// Events
// =========================================================================

protected function afterInstall ()
{
parent::afterInstall();

Craft::$app->getQueue()->push(new PopulateDataJob());
}

/**
* @param Event $event
*
Expand Down
49 changes: 41 additions & 8 deletions src/Service.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
use craft\commerce\elements\Order;
use craft\commerce\elements\Product;
use craft\commerce\elements\Variant;
use ether\purchasePatterns\elements\db\ProductQueryExtended;
use yii\db\Exception;
use yii\db\Expression;

Expand All @@ -35,13 +36,16 @@ class Service extends Component
*/
public function tallyProducts (Order $order)
{
$db = Craft::$app->getDb();

$productIds = [];
$productQtys = [];
$variantIds = [];

foreach ($order->lineItems as $item)
foreach ($order->getLineItems() as $item)
{
/** @var Variant $variant */
$variant = $item->purchasable;
$variant = $item->getPurchasable();

if (!$variant || !($variant instanceof Variant))
continue;
Expand All @@ -53,17 +57,38 @@ public function tallyProducts (Order $order)
$variantId = $variant->id;
if (!in_array($variantId, $variantIds))
$variantIds[] = $variantId;

if (!in_array($productId, $productQtys))
$productQtys[$productId] = 0;
$productQtys[$productId] += $item->qty;
}

sort($productIds);

try {
foreach ($productIds as $idA) {
$qty = $productQtys[$idA];
$db->createCommand()->upsert(
'{{%purchase_counts}}', [
'product_id' => $idA,
'order_count' => 1,
'qty_count' => $qty,
], [
'order_count' => new Expression(
'{{%purchase_counts}}.order_count + 1'
),
'qty_count' => new Expression(
'{{%purchase_counts}}.qty_count + ' . $qty
),
],
[], false
)->execute();

foreach ($productIds as $idB) {
if ($idA >= $idB)
continue;

Craft::$app->db->createCommand()->upsert(
$db->createCommand()->upsert(
'{{%purchase_patterns}}', [
'product_a' => $idA,
'product_b' => $idB,
Expand Down Expand Up @@ -93,7 +118,7 @@ public function tallyProducts (Order $order)
*
* @param ProductQuery|null $paddingQuery
*
* @return ProductQuery
* @return ProductQueryExtended
* @throws Exception
*/
public function getRelatedToProductCriteria (Product $product, $limit = 8, ProductQuery $paddingQuery = null)
Expand Down Expand Up @@ -124,7 +149,7 @@ public function getRelatedToProductCriteria (Product $product, $limit = 8, Produ
$productIds = array_merge($productIds, $paddingIds);
}

return Product::find()->id($productIds);
return $this->_getQuery()->id($productIds);
}

/**
Expand All @@ -134,7 +159,7 @@ public function getRelatedToProductCriteria (Product $product, $limit = 8, Produ
* @param int $limit
* @param ProductQuery|null $paddingQuery
*
* @return ProductQuery
* @return ProductQueryExtended
* @throws Exception
*/
public function getRelatedToOrderCriteria (Order $order, $limit = 8, ProductQuery $paddingQuery = null)
Expand All @@ -147,7 +172,7 @@ public function getRelatedToOrderCriteria (Order $order, $limit = 8, ProductQuer


if (empty($orderProductIds))
return Product::find()->limit($limit);
return $this->_getQuery()->limit($limit);

$idString = '(' . implode(',', $orderProductIds) . ')';

Expand Down Expand Up @@ -181,7 +206,7 @@ public function getRelatedToOrderCriteria (Order $order, $limit = 8, ProductQuer
$productIds = array_merge($productIds, $paddingIds);
}

return Product::find()->id($productIds);
return $this->_getQuery()->id($productIds);
}

/**
Expand Down Expand Up @@ -285,4 +310,12 @@ public function getBoughtTogetherMeta (Product $product)
}, $products);
}

/**
* @return ProductQueryExtended
*/
private function _getQuery ()
{
return new ProductQueryExtended(Product::class);
}

}
19 changes: 19 additions & 0 deletions src/Variable.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@

namespace ether\purchasePatterns;

use Craft;
use craft\commerce\elements\db\ProductQuery;
use craft\commerce\elements\Order;
use craft\commerce\elements\Product;
use ether\purchasePatterns\elements\db\ProductQueryExtended;
use yii\base\InvalidConfigException;
use yii\db\Exception;

Expand Down Expand Up @@ -68,4 +70,21 @@ public function related (
);
}

/**
* Returns an extended product query
*
* @param mixed|null $criteria
*
* @return ProductQueryExtended
*/
public function extended ($criteria = null)
{
$query = new ProductQueryExtended(Product::class);

if ($criteria)
Craft::configure($query, $criteria);

return $query;
}

}
40 changes: 40 additions & 0 deletions src/elements/db/ProductQueryExtended.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php
/**
* Purchase Patterns
*
* @link https://ethercreative.co.uk
* @copyright Copyright (c) 2019 Ether Creative
*/

namespace ether\purchasePatterns\elements\db;

use craft\commerce\elements\db\ProductQuery;

/**
* Class ProductQueryExtended
*
* @author Ether Creative
* @package ether\purchasePatterns\elements\db
*/
class ProductQueryExtended extends ProductQuery
{

protected function beforePrepare (): bool
{
$this->innerJoin(
'{{%purchase_counts}}',
'[[commerce_products.id]] = [[purchase_counts.product_id]]'
);

$select = [
'[[purchase_counts.order_count]] AS orderCount',
'[[purchase_counts.qty_count]] AS qtyCount',
];

$this->query->select($select);
$this->subQuery->select($select);

return parent::beforePrepare();
}

}
56 changes: 56 additions & 0 deletions src/jobs/PopulateDataJob.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?php
/**
* Purchase Patterns
*
* @link https://ethercreative.co.uk
* @copyright Copyright (c) 2019 Ether Creative
*/

namespace ether\purchasePatterns\jobs;

use Craft;
use craft\commerce\elements\Order;
use craft\commerce\elements\Variant;
use craft\queue\BaseJob;
use craft\queue\QueueInterface;
use ether\purchasePatterns\PurchasePatterns;
use yii\base\InvalidConfigException;
use yii\queue\Queue;

/**
* Class PopulateDataJob
*
* @author Ether Creative
* @package ether\purchasePatterns\jobs
*/
class PopulateDataJob extends BaseJob
{

/**
* @param Queue|QueueInterface $queue The queue the job belongs to
*
* @throws InvalidConfigException
*/
public function execute ($queue)
{
$completeOrders = Order::find()->isCompleted(true);
$total = $completeOrders->count();
$service = PurchasePatterns::getInstance()->getService();
$loop = 0;

// TODO: Clear tables if we allow users to run this later, after install

/** @var Order $order */
foreach ($completeOrders->each() as $order)
{
$this->setProgress($queue, $loop++ / $total);
$service->tallyProducts($order);
}
}

protected function defaultDescription ()
{
return Craft::t('purchase-patterns', 'Collating purchase pattern data');
}

}
Loading

0 comments on commit ff40dba

Please sign in to comment.