I have got a technical challenge from a company but they reject my code and I'm not sure how to do this challenge in a more efficient way so I'm here to get some guidance.
The Technical challenge was:
Manage a list of products that have prices.
- Enable the administrator to set concrete prices (such as 10EUR) and discounts to prices either by a concrete amount (-1 EUR) or by percentage (-10%).
- Enable the administrator to group products together to form bundles (which is also a product) that have independent prices.
- Enable customers to get the list of products and respective prices.
- Enable customers to place an order for one or more products, and provide customers with the list of products and the total price.
I have used Symfony framework to build this API and I'm writing the company response here:
SingleProduct
andBundleProduct
should be polymorphic.ConcretePrice
,DiscountedPriceByAmount
,DiscountedPriceByPercentage
should be polymorphic.- The computation of the overall sum of prices for the order should make no assumption about how the individual price was calculated (fix price, or discounted).
- The response should provide a deep object structure (as opposed to a flat list) that preserves the semantics of the model and is suitable for rendering.
/src/Entity/Product.php
<?php
// src/Entity/Product.php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
/**
* @ORM\Entity(repositoryClass="App\Repository\ProductRepository")
* @ORM\Table(name="products")
*/
class Product {
/**
* @ORM\Column(type="integer")
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* @Assert\NotBlank()
* @ORM\Column(type="string", length=150)
*/
private $title;
/**
* @ORM\Column(type="text", length=150, nullable=true)
*/
private $slug;
/**
* @Assert\NotBlank()
* @ORM\Column(type="float", scale=2)
*/
private $price;
/**
* @ORM\Column(type="string", length=5)
*/
private $currency = '€';
/**
* @ORM\Column(type="string", length=3, options={"comment":"Yes, No"})
*/
private $isDiscount = 'No';
/**
* @ORM\Column(type="string", length=10, options={"comment":"Concrete amount (-1 EUR) or by Percentage (-10%)"}, nullable=true)
*/
private $discountType;
/**
* @ORM\Column(type="integer", length=5, options={"comment":"1 or 10"})
*/
private $discount = 0;
/**
* @ORM\Column(type="string", length=5, options={"comment":"No, Yes, if yes then save product ids in product bundle items"})
*/
private $isProductBundle = 'No';
/**
* @ORM\Column(type="text", length=150, nullable=true)
*/
private $sku;
/**
* @ORM\Column(type="string", length=15, options={"comment":"Active or Pending , only Active products will display to customers"})
*/
private $status = 'Active';
/**
* @ORM\Column(type="string", length=150, options={"comment":"Upload or Link of image"})
*/
private $imageType = 'Link';
/**
* @ORM\Column(type="text")
*/
private $image = 'https://via.placeholder.com/400x300.png';
/**
* @ORM\Column(type="text", nullable=true)
*/
private $description;
/**
* @ORM\Column(type="datetime", nullable=true)
*/
private $createdAt;
/**
* @ORM\Column(type="datetime", nullable=true)
*/
private $updatedAt;
//Getters and Setters
}
/src/Entity/ProductBundleItem.php
<?php
// src/Entity/ProductBundleItem.php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
/**
* @ORM\Entity(repositoryClass="App\Repository\ProductBundleItemRepository")
* @ORM\Table(name="product_bundle_items")
*/
class ProductBundleItem {
/**
* @ORM\Column(type="integer")
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* @ORM\Column(type="integer")
*/
private $productBundleId;
/**
* @ORM\Column(type="integer")
*/
private $productId;
//Getters and Setters
/src/Repository/ProductRepository.php
<?php
namespace App\Repository;
use App\Entity\Product;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Symfony\Bridge\Doctrine\RegistryInterface;
/**
* @method Product|null find($id, $lockMode = null, $lockVersion = null)
* @method Product|null findOneBy(array $criteria, array $orderBy = null)
* @method Product[] findAll()
* @method Product[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class ProductRepository extends ServiceEntityRepository
{
public function __construct(RegistryInterface $registry)
{
parent::__construct($registry, Product::class);
}
public function findAllQueryBuilder()
{
return $this->createQueryBuilder('products');
}
// /**
// * @return Product[] Returns an array of Product objects
// */
/*
public function findByExampleField($value)
{
return $this->createQueryBuilder('t')
->andWhere('t.exampleField = :val')
->setParameter('val', $value)
->orderBy('t.id', 'ASC')
->setMaxResults(10)
->getQuery()
->getResult()
;
}
*/
/*
public function findOneBySomeField($value): ?Product
{
return $this->createQueryBuilder('t')
->andWhere('t.exampleField = :val')
->setParameter('val', $value)
->getQuery()
->getOneOrNullResult()
;
}
*/
}
/src/Repository/ProductBundleItemRepository.php
<?php
namespace App\Repository;
use App\Entity\ProductBundleItem;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Symfony\Bridge\Doctrine\RegistryInterface;
/**
* @method ProductBundleItem|null find($id, $lockMode = null, $lockVersion = null)
* @method ProductBundleItem|null findOneBy(array $criteria, array $orderBy = null)
* @method ProductBundleItem[] findAll()
* @method ProductBundleItem[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class ProductBundleItemRepository extends ServiceEntityRepository
{
public function __construct(RegistryInterface $registry)
{
parent::__construct($registry, ProductBundleItem::class);
}
public function findByProductBundleIdJoinedToProduct($productBundleId)
{
return $this->createQueryBuilder('pbi')
->select('p.id','p.title', 'p.price', 'p.currency')
->leftJoin('App\Entity\Product', 'p', 'WITH', 'p.id = pbi.productId')
->where('pbi.productBundleId = :productBundleIdParam')
->setParameter('productBundleIdParam', $productBundleId)
->getQuery()
->getResult();
}
// /**
// * @return ProductBundleItem[] Returns an array of ProductBundleItem objects
// */
/*
public function findByExampleField($value)
{
return $this->createQueryBuilder('t')
->andWhere('t.exampleField = :val')
->setParameter('val', $value)
->orderBy('t.id', 'ASC')
->setMaxResults(10)
->getQuery()
->getResult()
;
}
*/
/*
public function findOneBySomeField($value): ?ProductBundleItem
{
return $this->createQueryBuilder('t')
->andWhere('t.exampleField = :val')
->setParameter('val', $value)
->getQuery()
->getOneOrNullResult()
;
}
*/
}
/src/Controller/Api/ProductController.php
<?php
// src/Controller/Api/ProductController.php
namespace App\Controller\Api;
use FOS\RestBundle\Controller\Annotations as Rest;
use FOS\RestBundle\Controller\AbstractFOSRestController;
use FOS\RestBundle\View\View;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use App\Service\ProductService;
class ProductController extends AbstractFOSRestController
{
private $productService;
public function __construct(ProductService $productService){
$this->productService = $productService;
}
/**
* Retrieves a collection of Product resource
* @Rest\Get("/products")
*/
public function getProducts(Request $request): View
{
$params['page'] = $request->query->getInt('page', 1);
$params['limit'] = $request->query->getInt('limit', 10);
$products = $this->productService->getProducts($params);
return View::create($products, Response::HTTP_OK);
}
/**
* Retrieves a Product resource
* @Rest\Get("/products/{slug}")
*/
public function getProduct(Request $request, $slug): View
{
$product = $this->productService->getProduct($slug);
return View::create($product, Response::HTTP_OK);
}
/**
* Creates an Product resource
* @Rest\Post("/products")
* @param Request $request
* @return View
*/
public function addProduct(Request $request): View
{
$user = $this->getUser();
if(!in_array('ROLE_ADMIN',$user->getRoles())){
return View::create([], Response::HTTP_UNAUTHORIZED);
}
$params = json_decode($request->getContent(), true);
$product = $this->productService->addProduct($params);
return View::create($product, Response::HTTP_OK);
}
/**
* Creates an Product resource
* @Rest\Put("/products/{id}")
* @param Request $request
* @return View
*/
public function updateProduct(Request $request, $id): View
{
$user = $this->getUser();
if(!in_array('ROLE_ADMIN',$user->getRoles())){
return View::create([], Response::HTTP_UNAUTHORIZED);
}
$params = json_decode($request->getContent(), true);
$product = $this->productService->updateProduct($params, $id);
return View::create($product, Response::HTTP_OK);
}
/**
* Removes the Product resource
* @Rest\Delete("/products/{id}")
*/
public function deleteProduct($id): View
{
$user = $this->getUser();
if(!in_array('ROLE_ADMIN',$user->getRoles())){
return View::create([], Response::HTTP_UNAUTHORIZED);
}
$this->productService->deleteProduct($id);
return View::create([], Response::HTTP_NO_CONTENT);
}
}
/src/Controller/Api/ProductBundleController.php
<?php
// src/Controller/Api/ProductController.php
namespace App\Controller\Api;
use FOS\RestBundle\Controller\Annotations as Rest;
use FOS\RestBundle\Controller\AbstractFOSRestController;
use FOS\RestBundle\View\View;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use App\Service\ProductBundleService;
class ProductBundleController extends AbstractFOSRestController
{
private $productBundleService;
public function __construct(ProductBundleService $productBundleService){
$this->productBundleService = $productBundleService;
}
/**
* Retrieves a collection of Product bundle resource
* @Rest\Get("/products-not-bundles")
*/
public function getProductsIsNotBundles(): View
{
$products = $this->productBundleService->getProductsIsNotBundles();
return View::create($products, Response::HTTP_OK);
}
/**
* Retrieves a collection of Product bundle resource
* @Rest\Get("/product-bundles")
*/
public function getProductBundles(): View
{
$products = $this->productBundleService->getProducts();
return View::create($products, Response::HTTP_OK);
}
/**
* Retrieves a Product bundle resource
* @Rest\Get("/product-bundles/{id}")
*/
public function getProduct(Request $request, $id): View
{
$product = $this->productBundleService->getProduct($id);
return View::create($product, Response::HTTP_OK);
}
/**
* Creates an Product bundle resource
* @Rest\Post("/product-bundles")
* @param Request $request
* @return View
*/
public function addProduct(Request $request): View
{
$user = $this->getUser();
if(!in_array('ROLE_ADMIN',$user->getRoles())){
return View::create([], Response::HTTP_UNAUTHORIZED);
}
$params = json_decode($request->getContent(), true);
$product = $this->productBundleService->addProduct($params);
return View::create($product, Response::HTTP_OK);
}
/**
* Update an Product bundle resource
* @Rest\Put("/product-bundles/{id}")
* @param Request $request
* @return View
*/
public function updateProduct(Request $request, $id): View
{
$user = $this->getUser();
if(!in_array('ROLE_ADMIN',$user->getRoles())){
return View::create([], Response::HTTP_UNAUTHORIZED);
}
$params = json_decode($request->getContent(), true);
$product = $this->productBundleService->updateProduct($params, $id);
return View::create($product, Response::HTTP_OK);
}
/**
* Removes the Product bundle resource
* @Rest\Delete("/product-bundles/{id}")
*/
public function deleteProduct($id): View
{
$user = $this->getUser();
if(!in_array('ROLE_ADMIN',$user->getRoles())){
return View::create([], Response::HTTP_UNAUTHORIZED);
}
$this->productBundleService->deleteProduct($id);
return View::create([], Response::HTTP_NO_CONTENT);
}
}
/src/Service/ProductService.php
<?php
namespace App\Service;
use Doctrine\ORM\EntityManagerInterface;
use Pagerfanta\Adapter\DoctrineORMAdapter;
use Pagerfanta\Pagerfanta;
use App\Repository\ProductRepository;
use App\Utils\Slugger;
use App\Entity\Product;
final class ProductService
{
/**
* @var ProductRepository
*/
private $productRepository;
private $slugger;
private $em;
public function __construct(ProductRepository $productRepository, Slugger $slugger, EntityManagerInterface $em){
$this->productRepository = $productRepository;
$this->slugger = $slugger;
$this->em = $em;
}
public function getProducts($params): ?array
{
$qb = $this->productRepository->findAllQueryBuilder();
$adapter = new DoctrineORMAdapter($qb);
$pagerfanta = new Pagerfanta($adapter);
$pagerfanta->setMaxPerPage($params['limit']);
$pagerfanta->setCurrentPage($params['page']);
$products = [];
foreach ($pagerfanta->getCurrentPageResults() as $result) {
$products[] = $result;
}
$response =[
'total' => $pagerfanta->getNbResults(),
'count' => count($products),
'products' => $products,
];
return $response;
}
public function getProduct($slug){
#Find by id
//return $this->productRepository->find($id);
#Or find by slug
return $this->productRepository->findBy(['slug'=>$slug]);
}
public function addProduct($params){
$product = new Product();
foreach($params as $key=>$val){
$property = 'set'.strtoupper($key);
if(property_exists('App\Entity\Product',$key)){
$product->$property($val);
}
}
$slug = $this->slugger->slugify($product->getTitle());
$product->setSlug($slug);
$product->setCreatedAt(date("Y-m-d H:i:s"));
$product->setUpdatedAt(date("Y-m-d H:i:s"));
$this->em->persist($product);
$this->em->flush();
return $product;
}
public function updateProduct($params, $id){
if(empty($id))
return [];
$product = $this->productRepository->find($id);
if(!$product){
return [];
}
foreach($params as $key=>$val){
if($key=='id')
continue;
$property = 'set'.ucfirst($key);
if(property_exists('App\Entity\Product',$key)){
$product->$property($val);
}
}
$slug = $this->slugger->slugify($product->getTitle());
$product->setSlug($slug);
$product->setUpdatedAt(date("Y-m-d H:i:s"));
$this->em->persist($product);
$this->em->flush();
return $product;
}
public function deleteProduct($id){
$product = $this->productRepository->find($id);
if($product){
$this->em->remove($product);
$this->em->flush();
}
}
}
/src/Service/ProductBundleService.php
<?php
namespace App\Service;
use Doctrine\ORM\EntityManagerInterface;
use App\Repository\ProductRepository;
use App\Repository\ProductBundleItemRepository;
use App\Utils\Slugger;
use App\Entity\Product;
use App\Entity\ProductBundleItem;
final class ProductBundleService
{
/**
* @var ProductRepository
*/
private $productRepository;
private $productBundleItemRepository;
private $slugger;
private $em;
public function __construct(ProductRepository $productRepository, Slugger $slugger, EntityManagerInterface $em, ProductBundleItemRepository $productBundleItemRepository){
$this->productRepository = $productRepository;
$this->productBundleItemRepository = $productBundleItemRepository;
$this->slugger = $slugger;
$this->em = $em;
}
public function getProductsIsNotBundles(): ?array
{
return $this->productRepository->findBy(['status'=>'Active', 'isProductBundle'=>'No']);
}
public function getProducts(): ?array
{
return $this->productRepository->findBy(['isProductBundle'=>'Yes'],['id'=>'DESC']);
}
public function getProduct($id){
#Find by id
//return $this->productRepository->find($id);
#Or find by slug
$product = $this->productRepository->findBy(['id'=>$id,'isProductBundle'=>'Yes']);
$bunleItems = $this->productBundleItemRepository->findByProductBundleIdJoinedToProduct($product[0]->getId());
$returnData['product'] = $product;
$returnData['bunleItems'] = $bunleItems;
return $returnData;
}
public function addProduct($params){
$product = new Product();
foreach($params as $key=>$val){
$property = 'set'.strtoupper($key);
if(property_exists('App\Entity\Product',$key)){
$product->$property($val);
}
}
$product->setIsProductBundle("Yes");
$slug = $this->slugger->slugify($product->getTitle());
$product->setSlug($slug);
$product->setCreatedAt(date("Y-m-d H:i:s"));
$product->setUpdatedAt(date("Y-m-d H:i:s"));
$this->em->persist($product);
$this->em->flush();
$productsArr = $params['productsArr'];
if(count($productsArr)>0){
foreach($productsArr as $productId){
$productBundleItem = new ProductBundleItem();
$productBundleItem->setProductBundleId($product->getId());
$productBundleItem->setProductId($productId);
$this->em->persist($productBundleItem);
$this->em->flush();
}
}
$returnData['product'] = $product;
$returnData['productsArr'] = $productsArr;
return $returnData;
}
public function updateProduct($params, $id){
if(empty($id))
return [];
$product = $this->productRepository->find($id);
if(!$product){
return [];
}
foreach($params as $key=>$val){
if($key=='id')
continue;
$property = 'set'.ucfirst($key);
if(property_exists('App\Entity\Product',$key)){
$product->$property($val);
}
}
$product->setIsProductBundle("Yes");
$slug = $this->slugger->slugify($product->getTitle());
$product->setSlug($slug);
$product->setUpdatedAt(date("Y-m-d H:i:s"));
$this->em->persist($product);
$this->em->flush();
$productsArr = $params['productsArr'];
if(count($productsArr)>0){
foreach($productsArr as $productId){
$isExist = $this->productBundleItemRepository->findBy(['productId'=>$productId]);
if(!$isExist){
$productBundleItem = new ProductBundleItem();
$productBundleItem->setProductBundleId($product->getId());
$productBundleItem->setProductId($productId);
$this->em->persist($productBundleItem);
$this->em->flush();
}
}
}
$returnData['product'] = $product;
$returnData['productsArr'] = $productsArr;
return $returnData;
}
public function deleteProduct($id){
$product = $this->productRepository->find($id);
if($product){
$productBundleItems = $this->productBundleItemRepository->findBy(['productBundleId'=>$product->getId()]);
$this->em->remove($product);
foreach($productBundleItems as $item){
$this->em->remove($item);
}
$this->em->flush();
}
}
}
The whole code can be view here
I'm writing the company response here
". Does that mean that is the response from the company about the code you submitted? If so, I would expect to see those class names likeBundleProduct
in your code and/or repo but I don't... \$\endgroup\$isProductBundle
column in Product entity but I have not made a separate class. \$\endgroup\$