Quentin Ferrer
Posted on December 21, 2020
- Generating entities
- Migrating the Database
- Setting the default Order status
- Avoiding adding duplicate item
- Removing all items from the Order
- Calculating the Order totals
- Creating the Factory
Throughout the tutorial, we will talk about a cart as an Order. It's an Order that is in progress, not placed yet.
Actually, a cart is an Order in a
cart
status.
Generating entities
The Order
entity contains:
- a collection of
OrderItem
entity: it represents products as its physical copies, with chosen quantities, - a status: it will be initialized to
cart
, - a creation date: the creation date of the order,
- a modification date: the date the order was last modified.
Generating the OrderItem
entity
Use the Maker bundle to generate the OrderItem
entity:
$ symfony console make:entity OrderItem
Add the fields that we need:
- product, relation, related to the Product entity, ManyToOne, no, no
- quantity, integer, no
<?php
namespace App\Entity;
use App\Repository\OrderItemRepository;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity(repositoryClass=OrderItemRepository::class)
*/
class OrderItem
{
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
private $id;
/**
* @ORM\ManyToOne(targetEntity=Product::class)
* @ORM\JoinColumn(nullable=false)
*/
private $product;
/**
* @ORM\Column(type="integer")
*/
private $quantity;
public function getId(): ?int
{
return $this->id;
}
public function getProduct(): ?Product
{
return $this->product;
}
public function setProduct(?Product $product): self
{
$this->product = $product;
return $this;
}
public function getQuantity(): ?int
{
return $this->quantity;
}
public function setQuantity(int $quantity): self
{
$this->quantity = $quantity;
return $this;
}
}
Generating the Order
entity
One more time, use Maker bundle to generate the Order
entity:
$ symfony console make:entity Order
Add the fields that we need:
- items, relation, related to the
OrderItem
entity, OneToMany, orderRef (order is a reserved word in Mysql), no, yes - status, string, 255, no
- createdAt, datetime, no
- updatedAt, datetime, no
<?php
namespace App\Entity;
use App\Repository\OrderRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity(repositoryClass=OrderRepository::class)
* @ORM\Table(name="`order`")
*/
class Order
{
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
private $id;
/**
* @ORM\OneToMany(targetEntity=OrderItem::class, mappedBy="orderRef", orphanRemoval=true)
*/
private $items;
/**
* @ORM\Column(type="string", length=255)
*/
private $status;
/**
* @ORM\Column(type="datetime")
*/
private $createdAt;
/**
* @ORM\Column(type="datetime")
*/
private $updatedAt;
public function __construct()
{
$this->items = new ArrayCollection();
}
public function getId(): ?int
{
return $this->id;
}
/**
* @return Collection|OrderItem[]
*/
public function getItems(): Collection
{
return $this->items;
}
public function addItem(OrderItem $item): self
{
if (!$this->items->contains($item)) {
$this->items[] = $item;
$item->setOrderRef($this);
}
return $this;
}
public function removeItem(OrderItem $item): self
{
if ($this->items->removeElement($item)) {
// set the owning side to null (unless already changed)
if ($item->getOrderRef() === $this) {
$item->setOrderRef(null);
}
}
return $this;
}
public function getStatus(): ?string
{
return $this->status;
}
public function setStatus(string $status): self
{
$this->status = $status;
return $this;
}
public function getCreatedAt(): ?\DateTimeInterface
{
return $this->createdAt;
}
public function setCreatedAt(\DateTimeInterface $createdAt): self
{
$this->createdAt = $createdAt;
return $this;
}
public function getUpdatedAt(): ?\DateTimeInterface
{
return $this->updatedAt;
}
public function setUpdatedAt(\DateTimeInterface $updatedAt): self
{
$this->updatedAt = $updatedAt;
return $this;
}
}
The OrderItem
has been updated with the following changes:
<?php
namespace App\Entity;
use App\Repository\OrderItemRepository;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity(repositoryClass=OrderItemRepository::class)
*/
class OrderItem
{
// ...
/**
* @ORM\ManyToOne(targetEntity=Order::class, inversedBy="items")
* @ORM\JoinColumn(nullable=false)
*/
private $orderRef;
// ...
public function getOrderRef(): ?Order
{
return $this->orderRef;
}
public function setOrderRef(?Order $orderRef): self
{
$this->orderRef = $orderRef;
return $this;
}
}
Setting Cascade Operations
When we add a new OrderItem
entity to an Order
entity, we should persist it before persisting the Order
entity. Also, when we will want to remove an Order
entity, we should remove all OrderItem
entities before removing the Order
entity.
It can be really boring, so we will let Doctrine take care of this internally by using the cascade operations. To do that, add a new cascade option on the items
property of the Order
entity:
/**
* @ORM\OneToMany(targetEntity=OrderItem::class, mappedBy="orderRef", cascade={"persist", "remove"}, orphanRemoval=true)
*/
private $items;
Therefore, when we will persist/remove an Order
entity, Doctrine will automatically call persist/remove on each of the OrderItem
objects associated with the Order
entity.
Migrating the Database
Create a migration file via the Maker bundle:
$ symfony console make:migration
A migration file has been stored under the migrations/
directory:
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20201215150141 extends AbstractMigration
{
public function getDescription() : string
{
return '';
}
public function up(Schema $schema) : void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE TABLE `order` (id INT AUTO_INCREMENT NOT NULL, status VARCHAR(255) NOT NULL, created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
$this->addSql('CREATE TABLE order_item (id INT AUTO_INCREMENT NOT NULL, product_id INT NOT NULL, order_ref_id INT NOT NULL, quantity INT NOT NULL, INDEX IDX_52EA1F094584665A (product_id), INDEX IDX_52EA1F09E238517C (order_ref_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
$this->addSql('ALTER TABLE order_item ADD CONSTRAINT FK_52EA1F094584665A FOREIGN KEY (product_id) REFERENCES product (id)');
$this->addSql('ALTER TABLE order_item ADD CONSTRAINT FK_52EA1F09E238517C FOREIGN KEY (order_ref_id) REFERENCES `order` (id)');
}
public function down(Schema $schema) : void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE order_item DROP FOREIGN KEY FK_52EA1F09E238517C');
$this->addSql('DROP TABLE `order`');
$this->addSql('DROP TABLE order_item');
}
}
Now, we can update the local database schema:
$ symfony console doctrine:migrations:migrate
Setting the default Order status
All new orders must be in the default cart
state. In the generated Order
entity, initialize the status using a STATUS_CART
constant:
<?php
namespace App\Entity;
// ...
/**
* @ORM\Entity(repositoryClass=OrderRepository::class)
* @ORM\Table(name="`order`")
*/
class Order
{
// ...
/**
* @ORM\Column(type="string", length=255)
*/
private $status = self::STATUS_CART;
/**
* An order that is in progress, not placed yet.
*
* @var string
*/
const STATUS_CART = 'cart';
// ...
}
Avoiding adding duplicate OrderItem
Currently, it's possible to add duplicate OrderItem
entities (with the same Product) to the Order
entity. Let's fix it.
Add a equals()
method to the OrderItem
class to know if the item corresponds to an item given as an argument.
/**
* Tests if the given item given corresponds to the same order item.
*
* @param OrderItem $item
*
* @return bool
*/
public function equals(OrderItem $item): bool
{
return $this->getProduct()->getId() === $item->getProduct()->getId();
}
Update the addItem()
method of the Order
entity to sum the quantity if the item already exists:
public function addItem(OrderItem $item): self
{
foreach ($this->getItems() as $existingItem) {
// The item already exists, update the quantity
if ($existingItem->equals($item)) {
$existingItem->setQuantity(
$existingItem->getQuantity() + $item->getQuantity()
);
return $this;
}
}
$this->items[] = $item;
$item->setOrderRef($this);
return $this;
}
Removing all items from the Order
Add a removeItems()
method to the Order
class to remove all items from the Order:
/**
* Removes all items from the order.
*
* @return $this
*/
public function removeItems(): self
{
foreach ($this->getItems() as $item) {
$this->removeItem($item);
}
return $this;
}
Calculating the Order totals
We will need to show the cart summary. Note that we ignore adjustments that order could have such as shipping cost, promo code, etc.
First, add a getTotal()
method to the OrderItem
entity to calculate the item total:
/**
* Calculates the item total.
*
* @return float|int
*/
public function getTotal(): float
{
return $this->getProduct()->getPrice() * $this->getQuantity();
}
Finally, add a getTotal()
method to the Order
entity to calculate the order total:
/**
* Calculates the order total.
*
* @return float
*/
public function getTotal(): float
{
$total = 0;
foreach ($this->getItems() as $item) {
$total += $item->getTotal();
}
return $total;
}
Creating the Factory
The OrderFactory
factory will help us to create Order
and OrderItem
entities with default data. It also allows you to change the Order
entity easily.
<?php
namespace App\Factory;
use App\Entity\Order;
use App\Entity\OrderItem;
use App\Entity\Product;
/**
* Class OrderFactory
* @package App\Factory
*/
class OrderFactory
{
/**
* Creates an order.
*
* @return Order
*/
public function create(): Order
{
$order = new Order();
$order
->setStatus(Order::STATUS_CART)
->setCreatedAt(new \DateTime())
->setUpdatedAt(new \DateTime());
return $order;
}
/**
* Creates an item for a product.
*
* @param Product $product
*
* @return OrderItem
*/
public function createItem(Product $product): OrderItem
{
$item = new OrderItem();
$item->setProduct($product);
$item->setQuantity(1);
return $item;
}
}
Let's go to the next step to manage cart storage.
Posted on December 21, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.