Permalink
Browse files

Add support for pagination on /top-sellers

- Use a manual LengthAwarePaginator to add support for pagination.
- Move top sellers queries to a Query Object class.
- Add unit test for TopSellersQuery class.
  • Loading branch information...
MitchellMcKenna committed Sep 2, 2017
1 parent c1eee2e commit 97d3be3170a0f6ba07e5eaeb92e4637f6d670ba9
@@ -4,25 +4,23 @@
use App\Http\Requests\TopSellersRequest;
use App\Http\Responses\ProductCollectionResponse;
use App\Product;
use Illuminate\Database\DatabaseManager;
use App\Queries\TopSellersQuery;
use Illuminate\Pagination\LengthAwarePaginator;
class TopSellersController extends Controller
{
public function index(TopSellersRequest $request, DatabaseManager $db)
public function index(TopSellersRequest $request, TopSellersQuery $query)
{
$topSellers = $db->table('orders')
->leftJoin('products', 'orders.product_id', '=', 'products.id')
->selectRaw('products.*, SUM(orders.quantity) as quantity')
->orderBy('quantity', 'desc')
->groupBy('orders.product_id')
->whereBetween('orders.created_at', [$request->getBegin(), $request->getEnd()])
->limit($request->getLimit())
->offset($request->getLimit() * ($request->getPage() - 1))
->get();
$products = $query->get($request->getBegin(), $request->getEnd(), $request->getPage(), $request->getLimit());
$products = (new Product())->hydrate($topSellers->toArray());
$paginator = (new LengthAwarePaginator(
$products,
$query->total($request->getBegin(), $request->getEnd()),
$request->getLimit(),
$request->getPage(),
['path' => $request->url()]
))->appends($request->query());
return new ProductCollectionResponse($products);
return new ProductCollectionResponse($products, $paginator);
}
}
@@ -30,21 +30,33 @@ public function rules()
];
}
/**
* @return DateTime
*/
public function getBegin()
{
return (new DateTime)->setTimestamp($this->input('begin', Carbon::now()->subHours(24)->timestamp));
}
/**
* @return DateTime
*/
public function getEnd()
{
return (new DateTime)->setTimestamp($this->input('end', Carbon::now()->timestamp));
}
/**
* @return int
*/
public function getPage()
{
return $this->input('page', 1);
}
/**
* @return int
*/
public function getLimit()
{
return $this->input('limit', 15);
@@ -3,6 +3,8 @@
namespace App\Http\Responses;
use App\Http\Responses\Transformers\OrderTransformer;
use App\Order;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Http\Response;
use League\Fractal\Manager;
use League\Fractal\Pagination\IlluminatePaginatorAdapter;
@@ -11,14 +13,15 @@
class OrderCollectionResponse extends Response
{
public function __construct($orders, $paginator = null)
/**
* @param Order[]|\Illuminate\Support\Collection $orders
* @param LengthAwarePaginator $paginator
*/
public function __construct($orders, $paginator)
{
$fractal = (new Manager())->setSerializer(new JsonApiSerializer());
$resource = new Collection($orders, new OrderTransformer(), 'order');
if ($paginator) {
$resource->setPaginator(new IlluminatePaginatorAdapter($paginator));
}
$resource->setPaginator(new IlluminatePaginatorAdapter($paginator));
return parent::__construct($fractal->createData($resource)->toArray());
}
@@ -3,6 +3,8 @@
namespace App\Http\Responses;
use App\Http\Responses\Transformers\ProductTransformer;
use App\Product;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Http\Response;
use League\Fractal\Manager;
use League\Fractal\Pagination\IlluminatePaginatorAdapter;
@@ -11,14 +13,15 @@
class ProductCollectionResponse extends Response
{
public function __construct($products, $paginator = null)
/**
* @param Product[]|\Illuminate\Support\Collection $products
* @param LengthAwarePaginator $paginator
*/
public function __construct($products, $paginator)
{
$fractal = (new Manager())->setSerializer(new JsonApiSerializer());
$resource = new Collection($products, new ProductTransformer(), 'product');
if ($paginator) {
$resource->setPaginator(new IlluminatePaginatorAdapter($paginator));
}
$resource->setPaginator(new IlluminatePaginatorAdapter($paginator));
return parent::__construct($fractal->createData($resource)->toArray());
}
@@ -0,0 +1,60 @@
<?php
namespace App\Queries;
use App\Product;
use DateTime;
use Illuminate\Database\DatabaseManager;
class TopSellersQuery
{
/** @var DatabaseManager */
private $db;
/**
* @param DatabaseManager $db
*/
public function __construct(DatabaseManager $db)
{
$this->db = $db;
}
/**
* Get Products ordered by the highest quantity of orders in a given time span.
*
* @param DateTime $begin
* @param DateTime $end
* @param int $limit
* @param int $page
* @return Product[]|\Illuminate\Database\Eloquent\Collection
*/
public function get(DateTime $begin, DateTime $end, $page = 1, $limit = 15)
{
$topSellers = $this->db->table('orders')
->leftJoin('products', 'orders.product_id', '=', 'products.id')
->selectRaw('products.*, SUM(orders.quantity) as quantity')
->orderBy('quantity', 'desc')
->groupBy('orders.product_id')
->whereBetween('orders.created_at', [$begin, $end])
->limit($limit)
->offset($limit * ($page - 1))
->get();
return (new Product())->hydrate($topSellers->toArray());
}
/**
* Get total number of products that have sales in a given time span.
*
* @param DateTime $begin
* @param DateTime $end
* @return int
*/
public function total(DateTime $begin, DateTime $end)
{
return $this->db->table('orders')
->selectRaw('count(DISTINCT orders.product_id) as total')
->whereBetween('orders.created_at', [$begin, $end])
->value('total');
}
}
@@ -6,6 +6,7 @@
use App\Http\Requests\TopSellersRequest;
use App\Order;
use App\Product;
use App\Queries\TopSellersQuery;
use Carbon\Carbon;
use Illuminate\Database\DatabaseManager;
use Illuminate\Foundation\Testing\RefreshDatabase;
@@ -18,14 +19,14 @@ class TopSellersControllerTest extends TestCase
/** @var TopSellersController */
protected $controller;
/** @var DatabaseManager */
protected $db;
/** @var TopSellersQuery */
protected $query;
protected function setUp()
{
parent::setUp();
$this->controller = new TopSellersController();
$this->db = $this->app->make(DatabaseManager::class);
$this->query = new TopSellersQuery($this->app->make(DatabaseManager::class));
}
public function testIndexDefaultsToOnlyIncludeOrdersInLast24Hours()
@@ -35,41 +36,19 @@ public function testIndexDefaultsToOnlyIncludeOrdersInLast24Hours()
// Create 1 before
factory(Order::class)->create(['product_id' => $topSeller->id, 'created_at' => Carbon::now()->subHours(25)]);
// Create 2 during
factory(Order::class, 2)->create(
['product_id' => $topSeller->id, 'quantity' => 4]
);
factory(Order::class, 2)->create(['product_id' => $topSeller->id, 'quantity' => 4]);
$response = $this->controller->index(new TopSellersRequest(), $this->db);
$response = $this->controller->index(new TopSellersRequest(), $this->query);
$this->assertEquals(8, $response->getOriginalContent()['data'][0]['meta']['quantity']);
}
public function testIndexOnlyIncludesOrdersBetweenBeginAndEndQueryParams()
{
$begin = Carbon::create(2010);
$end = Carbon::create(2012);
$topSeller = factory(Product::class)->create();
// Create 1 before
factory(Order::class)->create(['product_id' => $topSeller->id, 'created_at' => Carbon::create(2009)]);
// Create 2 during
factory(Order::class, 2)->create(
['product_id' => $topSeller->id, 'quantity' => 5, 'created_at' => Carbon::create(2011)]
);
// Create 1 after
factory(Order::class)->create(['product_id' => $topSeller->id, 'created_at' => Carbon::create(2013)]);
$request = new TopSellersRequest(['begin' => $begin->timestamp, 'end' => $end->timestamp]);
$response = $this->controller->index($request, $this->db);
$this->assertEquals(10, $response->getOriginalContent()['data'][0]['meta']['quantity']);
}
public function testIndexPagination()
{
factory(Order::class)->create(['quantity' => 100]);
$secondPlace = factory(Order::class)->create(['quantity' => 50]);
$request = new TopSellersRequest(['page' => 2, 'limit' => 1]);
$response = $this->controller->index($request, $this->db);
$response = $this->controller->index($request, $this->query);
$this->assertEquals($secondPlace->product->id, $response->getOriginalContent()['data'][0]['id']);
}
}
@@ -0,0 +1,76 @@
<?php
namespace Tests\Unit\App\Queries;
use App\Http\Requests\TopSellersRequest;
use App\Order;
use App\Product;
use App\Queries\TopSellersQuery;
use Carbon\Carbon;
use Illuminate\Database\DatabaseManager;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class TopSellersQueryTest extends TestCase
{
use RefreshDatabase;
/** @var TopSellersQuery */
protected $query;
protected function setUp()
{
parent::setUp();
$this->query = new TopSellersQuery($this->app->make(DatabaseManager::class));
}
public function testGetOnlyIncludesOrdersBetweenBeginAndEndQueryParams()
{
$begin = Carbon::create(2010);
$end = Carbon::create(2012);
$topSeller = factory(Product::class)->create();
// Create 1 before
factory(Order::class)->create(['product_id' => $topSeller->id, 'created_at' => Carbon::create(2009)]);
// Create 2 during
factory(Order::class, 2)->create(
['product_id' => $topSeller->id, 'quantity' => 5, 'created_at' => Carbon::create(2011)]
);
// Create 1 after
factory(Order::class)->create(['product_id' => $topSeller->id, 'created_at' => Carbon::create(2013)]);
$topSellers = $this->query->get($begin, $end);
$this->assertEquals(10, $topSellers->first()->quantity);
}
public function testGetLimitAndPageParams()
{
$begin = Carbon::create(2010);
$end = Carbon::create(2012);
// Factory will create 3 orders; 1 for each new product.
factory(Order::class, 3)->create(['created_at' => Carbon::create(2011)]);
$this->assertEquals(2, count($this->query->get($begin, $end, 1, 2)));
$this->assertEquals(1, count($this->query->get($begin, $end, 2, 2)));
}
public function testTotal()
{
$begin = Carbon::create(2010);
$end = Carbon::create(2012);
$nonTopSeller = factory(Product::class)->create();
$topSeller = factory(Product::class)->create();
$topSeller2 = factory(Product::class)->create();
// Create 1 before and 1 after for $nonTopSeller which shouldn't be included
factory(Order::class)->create(['product_id' => $nonTopSeller->id, 'created_at' => Carbon::create(2009)]);
factory(Order::class)->create(['product_id' => $nonTopSeller->id, 'created_at' => Carbon::create(2013)]);
// Create 1 during for $topSeller and 1 for $topSeller2 which should be included
factory(Order::class)->create(['product_id' => $topSeller->id, 'created_at' => Carbon::create(2011)]);
factory(Order::class)->create(['product_id' => $topSeller2->id, 'created_at' => Carbon::create(2011)]);
$this->assertEquals(2, $this->query->total($begin, $end));
}
}

0 comments on commit 97d3be3

Please sign in to comment.