Refactor Code and write Unit test

This commit is contained in:
Thang Tran 2023-03-01 18:50:56 +07:00
parent 31b91e2980
commit b0b603fdc1
17 changed files with 684 additions and 50 deletions

2
.gitignore vendored
View File

@ -5,3 +5,5 @@ obj
vendor
.idea
*.iml
.history
.vscode

168
php/.docker/php/Dockerfile Normal file
View File

@ -0,0 +1,168 @@
FROM composer as composer
FROM php:8.2-fpm-bullseye as php-base
RUN set -xe; \
apt-get update -yqq && \
pecl channel-update pecl.php.net && \
apt-get install -yqq \
apt-utils \
gnupg2 \
git
###########################################################################
# Zip:
###########################################################################
RUN apt-get install -yqq \
libzip-dev zip unzip && \
docker-php-ext-configure zip && \
# Install the zip extension
docker-php-ext-install zip && \
php -m | grep -q 'zip'
###########################################################################
# GD:
###########################################################################
RUN apt-get install -yqq libpng-dev && \
docker-php-ext-configure gd \
--prefix=/usr \
--with-jpeg \
--with-webp \
--with-xpm \
--with-freetype; \
docker-php-ext-install gd
###########################################################################
# EXIF:
###########################################################################
RUN docker-php-ext-install exif;
RUN docker-php-ext-enable exif;
###########################################################################
# Imagick:
###########################################################################
RUN apt-get install -y libmagickwand-dev; \
pecl install imagick; \
docker-php-ext-enable imagick;
###########################################################################
# PHP GETTEXT EXTENTION:
###########################################################################
RUN docker-php-ext-install gettext;
RUN docker-php-ext-enable gettext;
###########################################################################
# PHP GETTEXT EXTENTION:
###########################################################################
RUN docker-php-ext-install calendar && docker-php-ext-configure calendar
###########################################################################
# Human Language and Character Encoding Support:
###########################################################################
RUN apt-get install -yqq zlib1g-dev libicu-dev g++ && docker-php-ext-configure intl && docker-php-ext-install intl && docker-php-ext-enable intl
###########################################################################
# APCU:
###########################################################################
ARG INSTALL_APCU=false
RUN if [ ${INSTALL_APCU} = true]; then \
pecl install apcu \
&& docker-php-ext-enable apcu \
;fi
###########################################################################
# OPCACHE:
###########################################################################
ARG INSTALL_OPCACHE=false
RUN if [ ${INSTALL_OPCACHE} = true ]; then \
docker-php-ext-install opcache \
;fi
# Copy opcache configration
COPY ./conf.d/opcache.ini /usr/local/etc/php/conf.d/opcache.ini
###########################################################################
# PHP REDIS EXTENSION
###########################################################################
ARG INSTALL_PHPREDIS=false
RUN if [ ${INSTALL_PHPREDIS} = true ]; then \
pecl install -o -f redis \
&& rm -rf /tmp/pear \
&& docker-php-ext-enable redis \
;fi
############################################################################
# Xdebug
# NOTE: Please do not install xdebug and PCOV at the same time
############################################################################
ARG INSTALL_XDEBUG=false
RUN if [ ${INSTALL_XDEBUG} = true ]; then \
pecl install -o -f xdebug \
;fi
# Copy xdebug configration
COPY ./conf.d/xdebug.ini /usr/local/etc/php/conf.d/xdebug.ini
###########################################################################
# MySQL
###########################################################################
ARG INSTALL_MYSQL=false
RUN if [ ${INSTALL_MYSQL} = true ]; then \
docker-php-ext-install pdo_mysql \
;fi
###########################################################################
# RABBITMQ:
###########################################################################
ARG INSTALL_AMQP=false
RUN if [ ${INSTALL_AMQP} = true ]; then \
docker-php-ext-install sockets \
&& apt-get install -yqq librabbitmq-dev \
&& pecl install amqp-1.11.0 \
&& docker-php-ext-enable amqp \
&& php -m | grep -oiE '^amqp$' \
;fi
ARG INSTALL_YAML=false
RUN if [ ${INSTALL_YAML} = true ]; then \
apt-get install -yqq libyaml-dev \
&& pecl install yaml \
&& docker-php-ext-enable yaml \
;fi
COPY ./conf.d/core.ini /usr/local/etc/php/conf.d/core.ini
# Configure non-root user.
ARG PUID=1000
ENV PUID ${PUID}
ARG PGID=1000
ENV PGID ${PGID}
RUN groupmod -o -g ${PGID} www-data && \
usermod -o -u ${PUID} -g www-data www-data
WORKDIR /var/www
FROM php-base AS php-fpm
USER www-data
EXPOSE 9000
CMD ["php-fpm"]
FROM php-base AS php-cli
COPY --from=composer /usr/bin/composer /usr/bin/composer
###########################################################################
# PCOV
###########################################################################
ARG INSTALL_PCOV=false
RUN if [ ${INSTALL_PCOV} = true ]; then \
pecl install pcov \
&& docker-php-ext-enable pcov \
;fi
USER www-data
CMD ["php-fpm"]

View File

@ -0,0 +1,7 @@
[PHP]
file_uploads = On
upload_max_filesize = 256M
post_max_size = 256M
memory_limit=256M
[Date]
date.timezone="UTC"

View File

@ -0,0 +1,9 @@
; NOTE: The actual opcache.so extention is NOT SET HERE but rather (/usr/local/etc/php/conf.d/docker-php-ext-opcache.ini)
opcache.enable=1
opcache.memory_consumption=128
opcache.max_accelerated_files=30000
opcache.revalidate_freq=0
# JIT
opcache.jit_buffer_size=64M
opcache.jit=tracing

View File

@ -0,0 +1,6 @@
;default deactivated
zend_extension=xdebug.so
xdebug.mode=debug,coverage
xdebug.start_with_request=yes
xdebug.client_host=host.docker.internal
xdebug.client_port=9003

4
php/.gitignore vendored
View File

@ -4,3 +4,7 @@
/.editorconfig
/.phpunit.result.cache
/composer.lock
.history
.vscode
.bash_history
.composer

20
php/docker-compose.yml Normal file
View File

@ -0,0 +1,20 @@
version: "3.9"
services:
php-cli:
container_name: php-cli
image: customer-core-api-php-cli
build:
context: .docker/php
args:
- INSTALL_INTL=true
- INSTALL_MYSQL=true
- INSTALL_APCU=true
- INSTALL_OPCACHE=true
- INSTALL_AMQP=false
- INSTALL_PHPREDIS=false
- INSTALL_PCOV=true
- INSTALL_YAML=true
volumes:
- .docker/php/conf.d/xdebug.ini:/usr/local/etc/php/conf.d/xdebug.ini
- ./:/var/www

View File

@ -14,3 +14,5 @@ parameters:
checkGenericClassInNonGenericObjectType: false
checkMissingIterableValueType: false
treatPhpDocTypesAsCertain: false

View File

@ -4,6 +4,13 @@ declare(strict_types=1);
namespace GildedRose;
use GildedRose\ItemUpdater\Interface\ItemUpdaterInterface;
use GildedRose\ItemUpdater\AgedBrieItemUpdater;
use GildedRose\ItemUpdater\BackstagePassItemUpdater;
use GildedRose\ItemUpdater\ConjuredItemUpdater;
use GildedRose\ItemUpdater\NormalItemUpdater;
use GildedRose\ItemUpdater\SulfurasItemUpdater;
final class GildedRose
{
/**
@ -14,54 +21,31 @@ final class GildedRose
) {
}
/**
* @return void
*/
public function updateQuality(): void
{
foreach ($this->items as $item) {
if ($item->name != 'Aged Brie' and $item->name != 'Backstage passes to a TAFKAL80ETC concert') {
if ($item->quality > 0) {
if ($item->name != 'Sulfuras, Hand of Ragnaros') {
$item->quality = $item->quality - 1;
}
}
} else {
if ($item->quality < 50) {
$item->quality = $item->quality + 1;
if ($item->name == 'Backstage passes to a TAFKAL80ETC concert') {
if ($item->sellIn < 11) {
if ($item->quality < 50) {
$item->quality = $item->quality + 1;
}
}
if ($item->sellIn < 6) {
if ($item->quality < 50) {
$item->quality = $item->quality + 1;
}
}
}
}
}
if ($item->name != 'Sulfuras, Hand of Ragnaros') {
$item->sellIn = $item->sellIn - 1;
}
if ($item->sellIn < 0) {
if ($item->name != 'Aged Brie') {
if ($item->name != 'Backstage passes to a TAFKAL80ETC concert') {
if ($item->quality > 0) {
if ($item->name != 'Sulfuras, Hand of Ragnaros') {
$item->quality = $item->quality - 1;
}
}
} else {
$item->quality = $item->quality - $item->quality;
}
} else {
if ($item->quality < 50) {
$item->quality = $item->quality + 1;
}
}
}
$itemUpdater = $this->getItemUpdater($item);
$itemUpdater->updateSellIn($item);
$itemUpdater->updateQuality($item);
}
}
/**
* @param Item $item
*
* @return ItemUpdaterInterface
*/
private function getItemUpdater(Item $item): ItemUpdaterInterface
{
return match(true) {
strpos($item->name, 'Aged Brie') !== false => new AgedBrieItemUpdater(),
strpos($item->name, 'Backstage') !== false => new BackstagePassItemUpdater(),
strpos($item->name, 'Sulfuras') !== false => new SulfurasItemUpdater(),
strpos($item->name, 'Conjured') !== false => new ConjuredItemUpdater(),
default => new NormalItemUpdater()
};
}
}

View File

@ -0,0 +1,73 @@
<?php
declare(strict_types=1);
namespace GildedRose\ItemUpdater\Abstract;
use GildedRose\Item;
use GildedRose\ItemUpdater\Interface\ItemUpdaterInterface;
abstract class AbstractItemUpdater implements ItemUpdaterInterface
{
private const MIN_QUALITY = 0;
private const MAX_QUALITY = 50;
/**
* @param Item $item
*
* @return void
*/
abstract public function updateQuality(Item $item): void;
/**
* @param Item $item
*
* @return void
*/
public function updateSellIn(Item $item): void
{
$this->decreaseSellIn($item);
}
/**
* @param Item $item
*
* @return integer
*/
protected function getQualityDecrease(Item $item): int
{
return $item->sellIn < 0 ? 2 : 1;
}
/**
* @param Item $item
*
* @return void
*/
protected function decreaseSellIn(Item $item): void
{
$item->sellIn--;
}
/**
* @param Item $item
* @param integer $amount
*
* @return void
*/
protected function decreaseQuality(Item $item, int $amount = 1): void
{
$item->quality = max(self::MIN_QUALITY, $item->quality - $amount);
}
/**
* @param Item $item
* @param integer $amount
*
* @return void
*/
protected function increaseQuality(Item $item, int $amount = 1): void
{
$item->quality = min(self::MAX_QUALITY, $item->quality + $amount);
}
}

View File

@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace GildedRose\ItemUpdater;
use GildedRose\Item;
use GildedRose\ItemUpdater\Abstract\AbstractItemUpdater;
use GildedRose\ItemUpdater\Interface\ItemUpdaterInterface;
class AgedBrieItemUpdater extends AbstractItemUpdater implements ItemUpdaterInterface
{
/**
* @param Item $item
*
* @return void
*/
public function updateQuality(Item $item): void
{
// "Aged Brie" actually increases in Quality the older it gets
$this->increaseQuality($item, $this->getQualityDecrease($item));
}
}

View File

@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace GildedRose\ItemUpdater;
use GildedRose\Item;
use GildedRose\ItemUpdater\Abstract\AbstractItemUpdater;
use GildedRose\ItemUpdater\Interface\ItemUpdaterInterface;
class BackstagePassItemUpdater extends AbstractItemUpdater implements ItemUpdaterInterface
{
/**
* @param Item $item
*
* @return void
*/
public function updateQuality(Item $item): void
{
match(true) {
$item->sellIn <= 0 => $this->decreaseQuality($item, $item->quality),
$item->sellIn <= 5 => $this->increaseQuality($item, 3),
$item->sellIn <= 10 => $this->increaseQuality($item, 2),
default => $this->increaseQuality($item)
};
}
}

View File

@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace GildedRose\ItemUpdater;
use GildedRose\Item;
use GildedRose\ItemUpdater\Abstract\AbstractItemUpdater;
use GildedRose\ItemUpdater\Interface\ItemUpdaterInterface;
class ConjuredItemUpdater extends AbstractItemUpdater implements ItemUpdaterInterface
{
/**
* @param Item $item
*
* @return void
*/
public function updateQuality(Item $item): void
{
// "Conjured" items degrade in Quality twice as fast as normal items
$this->decreaseQuality($item, $this->getQualityDecrease($item) * 2);
}
}

View File

@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace GildedRose\ItemUpdater\Interface;
use GildedRose\Item;
interface ItemUpdaterInterface
{
/**
* @param Item $item
*
* @return void
*/
public function updateQuality(Item $item): void;
/**
* @param Item $item
*
* @return void
*/
public function updateSellIn(Item $item): void;
}

View File

@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace GildedRose\ItemUpdater;
use GildedRose\Item;
use GildedRose\ItemUpdater\Abstract\AbstractItemUpdater;
use GildedRose\ItemUpdater\Interface\ItemUpdaterInterface;
class NormalItemUpdater extends AbstractItemUpdater implements ItemUpdaterInterface
{
/**
* @param Item $item
*
* @return void
*/
public function updateQuality(Item $item): void
{
$this->decreaseQuality($item, $this->getQualityDecrease($item));
}
}

View File

@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace GildedRose\ItemUpdater;
use GildedRose\Item;
use GildedRose\ItemUpdater\Abstract\AbstractItemUpdater;
use GildedRose\ItemUpdater\Interface\ItemUpdaterInterface;
class SulfurasItemUpdater extends AbstractItemUpdater implements ItemUpdaterInterface
{
/**
* @param Item $item
*
* @return void
*/
public function updateQuality(Item $item): void
{
// Do nothing, Sulfuras never changes in quality
}
}

View File

@ -10,11 +10,229 @@ use PHPUnit\Framework\TestCase;
class GildedRoseTest extends TestCase
{
public function testFoo(): void
/**
* @return array
*/
private function getPayloadItems(): array
{
$items = [new Item('foo', 0, 0)];
$gildedRose = new GildedRose($items);
$gildedRose->updateQuality();
$this->assertSame('fixme', $items[0]->name);
return [
new Item('+5 Dexterity Vest', 10, 20),
new Item('Aged Brie', 2, 0),
new Item('Elixir of the Mongoose', 5, 7),
new Item('Sulfuras, Hand of Ragnaros', 0, 80),
new Item('Sulfuras, Hand of Ragnaros', -1, 80),
new Item('Backstage passes to a TAFKAL80ETC concert', 15, 20),
new Item('Backstage passes to a TAFKAL80ETC concert', 10, 49),
new Item('Backstage passes to a TAFKAL80ETC concert', 5, 49),
new Item('Conjured Mana Cake', 3, 6),
];
}
/**
* @return void
*/
public function testQualityDegradesTwiceWhenSellInNegative(): void
{
$items = [
new Item('Normal Item', 0, 12)
];
$GildedRose = new GildedRose($items);
$GildedRose->updateQuality();
$this->assertEquals(-1, $items[0]->sellIn);
$this->assertEquals(10, $items[0]->quality);
}
/**
* @return void
*/
public function testWithQualityNeverNagative(): void
{
$items = [
new Item('Normal Item', 9, 0)
];
$GildedRose = new GildedRose($items);
$GildedRose->updateQuality();
$this->assertEquals(8, $items[0]->sellIn);
$this->assertEquals(0, $items[0]->quality);
}
/**
* @return void
*/
public function testWithQualityNeverGreaterThan50(): void
{
$items = [
new Item('Aged Brie', 9, 50)
];
$GildedRose = new GildedRose($items);
$GildedRose->updateQuality();
$this->assertEquals(8, $items[0]->sellIn);
$this->assertEquals(50, $items[0]->quality);
}
/**
* @return void
*/
public function testAgedBrieIncreaseQualityWhenOlder(): void
{
$items = [
new Item('Aged Brie', 10, 10)
];
$GildedRose = new GildedRose($items);
$GildedRose->updateQuality();
$this->assertEquals(9, $items[0]->sellIn);
$this->assertEquals(11, $items[0]->quality);
}
/**
* @return void
*/
public function testAgedBrieIncraseQualityTwiceWhenSellInNagative(): void
{
$items = [
new Item('Aged Brie', -1, 10)
];
$GildedRose = new GildedRose($items);
$GildedRose->updateQuality();
$this->assertEquals(-2, $items[0]->sellIn);
$this->assertEquals(12, $items[0]->quality);
}
/**
* @return void
*/
public function testSulfurasNeverChangeQuality(): void
{
$items = [
new Item('Sulfuras', -1, 10)
];
$GildedRose = new GildedRose($items);
$GildedRose->updateQuality();
$this->assertEquals(-2, $items[0]->sellIn);
$this->assertEquals(10, $items[0]->quality);
}
/**
* @return void
*/
public function testBackstageIncreaseQualityBy2WhenSellInLessThanOrEqual10(): void
{
$items = [
new Item('Backstage', 9, 10)
];
$GildedRose = new GildedRose($items);
$GildedRose->updateQuality();
$this->assertEquals(8, $items[0]->sellIn);
$this->assertEquals(12, $items[0]->quality);
}
/**
* @return void
*/
public function testBackstageIncreaseQualityBy3WhenSellInLessThanOrEqual5(): void
{
$items = [
new Item('Backstage', 4, 10)
];
$GildedRose = new GildedRose($items);
$GildedRose->updateQuality();
$this->assertEquals(3, $items[0]->sellIn);
$this->assertEquals(13, $items[0]->quality);
}
/**
* @return void
*/
public function testBackstageSetQualityZeroWhenSellInLessThanOrEqualZero(): void
{
$items = [
new Item('Backstage', 0, 10)
];
$GildedRose = new GildedRose($items);
$GildedRose->updateQuality();
$this->assertEquals(-1, $items[0]->sellIn);
$this->assertEquals(0, $items[0]->quality);
}
/**
* @return void
*/
public function testConjuredDegradesQualityTwiceFastAsNormalItem(): void
{
$items = [
new Item('Conjured', 10, 10)
];
$GildedRose = new GildedRose($items);
$GildedRose->updateQuality();
$this->assertEquals(9, $items[0]->sellIn);
$this->assertEquals(8, $items[0]->quality);
}
/**
* @return void
*/
public function testUpdateSellInAndQualityWith10Days(): void
{
$dayTest = 10;
$items = $this->getPayloadItems();
$GildedRose = new GildedRose($items);
for ($i = 1; $i < $dayTest ; $i++) {
$GildedRose->updateQuality();
}
$this->assertEquals(1, $items[0]->sellIn);
$this->assertEquals(-7, $items[1]->sellIn);
$this->assertEquals(-4, $items[2]->sellIn);
$this->assertEquals(-9, $items[3]->sellIn);
$this->assertEquals(-10, $items[4]->sellIn);
$this->assertEquals(6, $items[5]->sellIn);
$this->assertEquals(1, $items[6]->sellIn);
$this->assertEquals(-4, $items[7]->sellIn);
$this->assertEquals(-6, $items[8]->sellIn);
$this->assertEquals(11, $items[0]->quality);
$this->assertEquals(16, $items[1]->quality);
$this->assertEquals(0, $items[2]->quality);
$this->assertEquals(80, $items[3]->quality);
$this->assertEquals(80, $items[4]->quality);
$this->assertEquals(34, $items[5]->quality);
$this->assertEquals(50, $items[6]->quality);
$this->assertEquals(0, $items[7]->quality);
$this->assertEquals(0, $items[8]->quality);
}
/**
* @return void
*/
public function testUpdateSellInAndQualityWith31Days(): void
{
$dayTest = 31;
$items = $this->getPayloadItems();
$GildedRose = new GildedRose($items);
for ($i = 1; $i < $dayTest ; $i++) {
$GildedRose->updateQuality();
}
$this->assertEquals(-20, $items[0]->sellIn);
$this->assertEquals(-28, $items[1]->sellIn);
$this->assertEquals(-25, $items[2]->sellIn);
$this->assertEquals(-30, $items[3]->sellIn);
$this->assertEquals(-31, $items[4]->sellIn);
$this->assertEquals(-15, $items[5]->sellIn);
$this->assertEquals(-20, $items[6]->sellIn);
$this->assertEquals(-25, $items[7]->sellIn);
$this->assertEquals(-27, $items[8]->sellIn);
$this->assertEquals(0, $items[0]->quality);
$this->assertEquals(50, $items[1]->quality);
$this->assertEquals(0, $items[2]->quality);
$this->assertEquals(80, $items[3]->quality);
$this->assertEquals(80, $items[4]->quality);
$this->assertEquals(0, $items[5]->quality);
$this->assertEquals(0, $items[6]->quality);
$this->assertEquals(0, $items[7]->quality);
$this->assertEquals(0, $items[8]->quality);
}
}