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:
SingleProductandBundleProductshould be polymorphic.ConcretePrice,DiscountedPriceByAmount,DiscountedPriceByPercentageshould 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 likeBundleProductin your code and/or repo but I don't... \$\endgroup\$isProductBundlecolumn in Product entity but I have not made a separate class. \$\endgroup\$