The carts are saved in the database. It is, therefore, necessary to purge the expired carts in order to avoid keeping old carts that are no longer used due to the expiration of the session. To do that, we will create a CLI command with the goal of running it every day in production automatically using a cron job.
Writing the Command
Generating the command
Use the Maker bundle to generate the command:
$ symfony console make:command RemoveExpiredCartsCommand
The command creates a RemoveExpiredCartsCommand
class under the src/Command/
directory.
<?php
namespace App\Command;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
class RemoveExpiredCartsCommand extends Command
{
protected static $defaultName = 'RemoveExpiredCartsCommand';
protected function configure()
{
$this
->setDescription('Add a short description for your command')
->addArgument('arg1', InputArgument::OPTIONAL, 'Argument description')
->addOption('option1', null, InputOption::VALUE_NONE, 'Option description')
;
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$arg1 = $input->getArgument('arg1');
if ($arg1) {
$io->note(sprintf('You passed an argument: %s', $arg1));
}
if ($input->getOption('option1')) {
// ...
}
$io->success('You have a new command! Now make it your own! Pass --help to see your options.');
return Command::SUCCESS;
}
}
Configuring the Command
In the RemoveExpiredCartsCommand
class, set the default command name:
protected static $defaultName = 'app:remove-expired-carts';
And update the configure()
method to:
- define a command description and,
- add an optional input argument to define the number of days a cart can remain inactive. By default, a cart will be deleted after 2 days of inactivity.
protected function configure()
{
$this
->setDescription('Removes carts that have been inactive for a defined period')
->addArgument(
'days',
InputArgument::OPTIONAL,
'The number of days a cart can remain inactive',
2
)
;
}
Writing the Logic of the Command
Finding Expired Carts
When we created the OrderEntity
entity, a OrderRepository
class was generated.
A repository helps you fetch entities of a certain class.
Doctrine recommends centralizing all queries in this repository. So, let's add a method findCartsNotModifiedSince
to find inactive cards since a period.
<?php
namespace App\Repository;
use App\Entity\Order;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @method Order|null find($id, $lockMode = null, $lockVersion = null)
* @method Order|null findOneBy(array $criteria, array $orderBy = null)
* @method Order[] findAll()
* @method Order[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class OrderRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Order::class);
}
/**
* Finds carts that have not been modified since the given date.
*
* @param \DateTime $limitDate
* @param int $limit
*
* @return int|mixed|string
*/
public function findCartsNotModifiedSince(\DateTime $limitDate, int $limit = 10): array
{
return $this->createQueryBuilder('o')
->andWhere('o.status = :status')
->andWhere('o.updatedAt < :date')
->setParameter('status', Order::STATUS_CART)
->setParameter('date', $limitDate)
->setMaxResults($limit)
->getQuery()
->getResult()
;
}
}
Doctrine provides a Query Builder object to help you building a DQL query.
First, we add a filter to find the carts that have not been modified since the given limit date.
Then, as a cart is an Order
in acart
status, we don't forget to filter by status.
Finally, we might have a lot of expired carts, so we add a limit filter to avoid running out of memory.
Deleting Expired Carts
The execute()
method must contain the logic we want the command to execute. It must return an integer to inform the command status. Implement it to bulk delete expired carts based on the input argument value:
<?php
namespace App\Command;
use App\Repository\OrderRepository;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class RemoveExpiredCartsCommand extends Command
{
/**
* @var EntityManagerInterface
*/
private $entityManager;
/**
* @var OrderRepository
*/
private $orderRepository;
protected static $defaultName = 'app:remove-expired-carts';
/**
* RemoveExpiredCartsCommand constructor.
*
* @param EntityManagerInterface $entityManager
* @param OrderRepository $orderRepository
*/
public function __construct(EntityManagerInterface $entityManager, OrderRepository $orderRepository)
{
parent::__construct();
$this->entityManager = $entityManager;
$this->orderRepository = $orderRepository;
}
/**
* @inheritDoc
*/
protected function configure()
{
$this
->setDescription('Removes carts that have been inactive for a defined period')
->addArgument(
'days',
InputArgument::OPTIONAL,
'The number of days a cart can remain inactive',
2
)
;
}
/**
* @inheritDoc
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$days = $input->getArgument('days');
if ($days <= 0) {
$io->error('The number of days should be greater than 0.');
return Command::FAILURE;
}
// Subtracts the number of days from the current date.
$limitDate = new \DateTime("- $days days");
$expiredCartsCount = 0;
while($carts = $this->orderRepository->findCartsNotModifiedSince($limitDate)) {
foreach ($carts as $cart) {
// Items will be deleted on cascade
$this->entityManager->remove($cart);
}
$this->entityManager->flush(); // Executes all deletions
$this->entityManager->clear(); // Detaches all object from Doctrine
$expiredCartsCount += count($carts);
};
if ($expiredCartsCount) {
$io->success("$expiredCartsCount cart(s) have been deleted.");
} else {
$io->info('No expired carts.');
}
return Command::SUCCESS;
}
}
We set the date limit by subtracting the number of days from the current date.
Next, we iterate and handle batches of carts instead of loading all the expired carts into memory. It's a good way to avoid running out of memory with bulk operations like this. The items of the cart have been deleted on cascade.
Then, we write the number of expired carts we deleted to the console and return the command status.
Executing the Command
After configuring and handling the command, you can run it in the terminal:
$ symfony console app:remove-expired-carts
[OK] 1 cart(s) have been deleted.
If there are no expired carts, the command should write the following message in the output:
$ symfony console app:remove-expired-carts
[INFO] No expired carts.
Now you know how to create a CLI command in Symfony. Of course, this command should be executed automatically in production using a cron job but that is not the purpose of this tutorial.
There is one last step left to complete this tutorial. Let's test the cart in the final step.
Top comments (2)
Hello Quentin.
Thank you for an excellent tutorial.
This is realy good intermediate content from great symfony developer.
I can't wait for more. Good Job
Btw I 've just noticed small mistake.
You forgot to add parent::__construct() in RemoveExpiredCartsCommand constructor.
Glad you like it! Thanks a lot, I fixed it! I also checked the source code on github, it's correct. Enjoy the rest of this tutorial.