Skip to content

Commit

Permalink
chore: updated code for fetching api data, updated unit test
Browse files Browse the repository at this point in the history
  • Loading branch information
benborla committed Nov 17, 2023
1 parent 0192804 commit bf99f10
Show file tree
Hide file tree
Showing 6 changed files with 139 additions and 54 deletions.
3 changes: 3 additions & 0 deletions api/.env.test
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,6 @@ SYMFONY_DEPRECATIONS_HELPER=999999
# DATABASE_URL="sqlite:///%kernel.project_dir%/var/data.db"
DATABASE_URL="mysql://[email protected]:3306/fruity_vice?serverVersion=8.0.32&charset=utf8mb4"
###< doctrine/doctrine-bundle ###
#
API_ENDPOINT=https://fruityvice.com/api/fruit
APP_CACHE=1
27 changes: 18 additions & 9 deletions api/src/Command/FruitsFetchCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,25 @@

namespace App\Command;

use App\Entity\Fruit;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use App\Service\FruitAggregator;
use App\Service\FruitDecomulator;

#[AsCommand(
name: 'fruits:fetch',
description: 'Fetch data from api https://fruityvice.com/api/fruit/*',
)]
class FruitsFetchCommand extends Command
{
public function __construct(private EntityManagerInterface $em)
{
public function __construct(
private FruitAggregator $aggregator,
private FruitDecomulator $decomulator,
) {
parent::__construct();
}

Expand All @@ -40,14 +42,21 @@ protected function execute(InputInterface $input, OutputInterface $output): int

if ($truncate) {
$io->info('Clearing Fruit table');
// @TODO: Improve this, or move this to a service
$connection = $this->em->getConnection();
$platform = $connection->getDatabasePlatform();
$table = $this->em->getClassMetadata(Fruit::class)->getTableName();
$connection->executeStatement($platform->getTruncateTableSQL($table, true));
$this->decomulator->__invoke();
}

$io->comment('Processing...');
// @INFO: Fake step loading
$io->createProgressBar(100);
$io->progressStart();
$io->progressAdvance(50);

// @INFO: Process the synchronization of API data to DB
$this->aggregator->sync();

// @INFO: Fake progress iterate
$io->progressAdvance(50);
$io->progressFinish();

$io->success('Done!');

Expand Down
4 changes: 2 additions & 2 deletions api/src/Repository/FruitRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,9 @@ public function __construct(ManagerRegistry $registry)
* @param string $field The name of the field that you would like to refer to
* @param string $value The value of the field that you would like to refer to
*
* @return Fruit[] Returns an array of Fruit objects
* @return \App\Entity\Fruit Returns an instance of Fruit
*/
public function findByField(string $field, string $value): null|array
public function findByField(string $field, string $value): null|Fruit
{
return $this->createQueryBuilder('f')
->andWhere("f.{$field} = :val")
Expand Down
47 changes: 43 additions & 4 deletions api/src/Service/FruitAggregator.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,24 +12,63 @@ class FruitAggregator
{
public function __construct(
private FruitsApiClient $api,
private EntityManagerInterface $em
private EntityManagerInterface $em,
private FruitRepository $fruitRepository,
) {
}

public function save()
/**
* This will fetch data from the API endpoint and will do an upsert in the
* database `fruits` table
*
* @return self
*/
public function sync(): self
{
$fruitsFromApi = $this->api->get();

// @INFO: Throw an exception if the data is invalid
if (! $fruitsFromApi instanceof FruitApiEntity) {
if (!$fruitsFromApi instanceof FruitApiEntity) {
throw new \Exception('Invalid response from the API endpoint');
}

// @INFO: Do nothing if nothing is returned from the API call
if (0 === $fruitsFromApi->getItems()) {
return;
return $this;
}

// @INFO: Check if the fetched data from API already exist in the database table
$items = $fruitsFromApi->getItems();
foreach ($items as $item) {
$isNew = false;
// @INFO: Check if data already exist
$fruit = $this->fruitRepository->findByField('name', $item['name']);

// @INFO: Create new instance for new data if `fruit` is not existing
if (!$fruit) {
$fruit = new Fruit();
$isNew = true;
}

$fruit->setName($item['name'])
->setFamily($item['family'])
->setFruitOrder($item['order'])
->setGenus($item['genus'])
->setCalories($item['nutritions']['calories'])
->setFat($item['nutritions']['fat'])
->setSugar($item['nutritions']['sugar'])
->setCarbohydrates($item['nutritions']['carbohydrates'])
->setProtein($item['nutritions']['protein'])
->setSource(Fruit::SOURCE_FETCHED_API);

// @INFO: Persist non-existent data
if ($isNew) {
$this->em->persist($fruit);
}

$this->em->flush();
}

return $this;
}
}
58 changes: 19 additions & 39 deletions api/tests/Command/FruitsFetchCommandTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,56 +5,36 @@
namespace App\Tests\Command;

use App\Command\FruitsFetchCommand;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Mapping\ClassMetadata;
use App\Service\FruitAggregator;
use App\Service\FruitDecomulator;
use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\Console\Tester\CommandTester;

class FruitsFetchCommandTest extends KernelTestCase
{
private const TABLE_NAME = 'fruits';
private $entityManager;
private $aggregator;
private $decomulator;
private $application;
private $command;

/**
* @TODO: For improvement
*
* @return void
*/
protected function setUp(): void
public function setUp(): void
{
$kernel = self::bootKernel();
$this->application = new Application($kernel);

$this->entityManager = $this->createMock(EntityManagerInterface::class);

$classMetadata = $this->createMock(ClassMetadata::class);
$classMetadata->expects($this->any())
->method('getTableName')
->willReturn(self::TABLE_NAME);

$this->entityManager->expects($this->any())
->method('getClassMetadata')
->willReturn($classMetadata);

$connection = $this->createMock(\Doctrine\DBAL\Connection::class);
$connection->expects($this->any())
->method('getDatabasePlatform')
->willReturn($this->createMock(\Doctrine\DBAL\Platforms\AbstractPlatform::class));

$this->entityManager->expects($this->any())
->method('getConnection')
->willReturn($connection);

$this->application->add(new FruitsFetchCommand($this->entityManager));
// Mock the services
$this->aggregator = $this->createMock(FruitAggregator::class);
$this->decomulator = $this->createMock(FruitDecomulator::class);
$this->command = new FruitsFetchCommand($this->aggregator, $this->decomulator);

$this->application = new Application(self::bootKernel());
$this->application->add($this->command);
$this->command = $this->application->find('fruits:fetch');
}

public function testFruitsFetchCommandDefault()
{
$command = $this->application->find('fruits:fetch');
$commandTester = new CommandTester($command);
$commandTester = new CommandTester($this->command);

// @INFO: Run the command with the `--truncate` option
// @INFO: Run the command with default option
$commandTester->execute([]);

// @INFO: Run assertions
Expand Down Expand Up @@ -85,7 +65,7 @@ protected function tearDown(): void
parent::tearDown();

// @INFO: doing this is recommended to avoid memory leaks
$this->entityManager->close();
$this->entityManager = null;
$this->aggregator = null;
$this->decomulator = null;
}
}
54 changes: 54 additions & 0 deletions api/tests/Service/FruitDecomulatorTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php

// tests/Service/FruitDecomulatorTest.php

namespace App\Tests\Service;

use App\Service\FruitDecomulator;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\Cache\Adapter\FilesystemAdapter;
use Doctrine\ORM\Mapping\ClassMetadata;

class FruitDecomulatorTest extends KernelTestCase
{
private const TABLE_NAME = 'fruits';

public function testInvoke()
{
$entityManager = $this->createMock(EntityManagerInterface::class);

$classMetadata = $this->createMock(ClassMetadata::class);
$classMetadata->expects($this->any())
->method('getTableName')
->willReturn(self::TABLE_NAME);

$entityManager->expects($this->any())
->method('getClassMetadata')
->willReturn($classMetadata);

$connection = $this->createMock(\Doctrine\DBAL\Connection::class);
$connection->expects($this->any())
->method('getDatabasePlatform')
->willReturn($this->createMock(\Doctrine\DBAL\Platforms\AbstractPlatform::class));

$entityManager->expects($this->any())
->method('getConnection')
->willReturn($connection);

$cache = $this->createMock(FilesystemAdapter::class);

// Set up an expectation that the deleteItem method will be called at least once
$cache->expects($this->any())
->method('deleteItem')
->with('api.fruits.*');

$fruitDecomulator = new FruitDecomulator($entityManager, $cache);

// Call the __invoke method
$fruitDecomulator->__invoke();

// @INFO: Make sure that it has cleared the cache
$this->assertEquals($cache->hasItem('api.fruits.*'), false);
}
}

0 comments on commit bf99f10

Please sign in to comment.