From b0b603fdc1a8ed209ad844079675e294bedf6a5f Mon Sep 17 00:00:00 2001 From: Thang Tran Date: Wed, 1 Mar 2023 18:50:56 +0700 Subject: [PATCH] Refactor Code and write Unit test --- .gitignore | 2 + php/.docker/php/Dockerfile | 168 +++++++++++++ php/.docker/php/conf.d/core.ini | 7 + php/.docker/php/conf.d/opcache.ini | 9 + php/.docker/php/conf.d/xdebug.ini | 6 + php/.gitignore | 4 + php/docker-compose.yml | 20 ++ php/phpstan.neon | 2 + php/src/GildedRose.php | 74 +++--- .../Abstract/AbstractItemUpdater.php | 73 ++++++ php/src/ItemUpdater/AgedBrieItemUpdater.php | 23 ++ .../ItemUpdater/BackstagePassItemUpdater.php | 27 +++ php/src/ItemUpdater/ConjuredItemUpdater.php | 23 ++ .../Interface/ItemUpdaterInterface.php | 24 ++ php/src/ItemUpdater/NormalItemUpdater.php | 22 ++ php/src/ItemUpdater/SulfurasItemUpdater.php | 22 ++ php/tests/GildedRoseTest.php | 228 +++++++++++++++++- 17 files changed, 684 insertions(+), 50 deletions(-) create mode 100644 php/.docker/php/Dockerfile create mode 100644 php/.docker/php/conf.d/core.ini create mode 100644 php/.docker/php/conf.d/opcache.ini create mode 100644 php/.docker/php/conf.d/xdebug.ini create mode 100644 php/docker-compose.yml create mode 100644 php/src/ItemUpdater/Abstract/AbstractItemUpdater.php create mode 100644 php/src/ItemUpdater/AgedBrieItemUpdater.php create mode 100644 php/src/ItemUpdater/BackstagePassItemUpdater.php create mode 100644 php/src/ItemUpdater/ConjuredItemUpdater.php create mode 100644 php/src/ItemUpdater/Interface/ItemUpdaterInterface.php create mode 100644 php/src/ItemUpdater/NormalItemUpdater.php create mode 100644 php/src/ItemUpdater/SulfurasItemUpdater.php diff --git a/.gitignore b/.gitignore index 95bd72b9..7862f541 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,5 @@ obj vendor .idea *.iml +.history +.vscode \ No newline at end of file diff --git a/php/.docker/php/Dockerfile b/php/.docker/php/Dockerfile new file mode 100644 index 00000000..5df1d228 --- /dev/null +++ b/php/.docker/php/Dockerfile @@ -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"] diff --git a/php/.docker/php/conf.d/core.ini b/php/.docker/php/conf.d/core.ini new file mode 100644 index 00000000..82bc1b87 --- /dev/null +++ b/php/.docker/php/conf.d/core.ini @@ -0,0 +1,7 @@ +[PHP] +file_uploads = On +upload_max_filesize = 256M +post_max_size = 256M +memory_limit=256M +[Date] +date.timezone="UTC" diff --git a/php/.docker/php/conf.d/opcache.ini b/php/.docker/php/conf.d/opcache.ini new file mode 100644 index 00000000..60ed5b57 --- /dev/null +++ b/php/.docker/php/conf.d/opcache.ini @@ -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 diff --git a/php/.docker/php/conf.d/xdebug.ini b/php/.docker/php/conf.d/xdebug.ini new file mode 100644 index 00000000..b201ddde --- /dev/null +++ b/php/.docker/php/conf.d/xdebug.ini @@ -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 diff --git a/php/.gitignore b/php/.gitignore index 179636ca..558b3dab 100644 --- a/php/.gitignore +++ b/php/.gitignore @@ -4,3 +4,7 @@ /.editorconfig /.phpunit.result.cache /composer.lock +.history +.vscode +.bash_history +.composer \ No newline at end of file diff --git a/php/docker-compose.yml b/php/docker-compose.yml new file mode 100644 index 00000000..36224525 --- /dev/null +++ b/php/docker-compose.yml @@ -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 diff --git a/php/phpstan.neon b/php/phpstan.neon index 95e29c1c..e8d64eda 100644 --- a/php/phpstan.neon +++ b/php/phpstan.neon @@ -14,3 +14,5 @@ parameters: checkGenericClassInNonGenericObjectType: false checkMissingIterableValueType: false + + treatPhpDocTypesAsCertain: false diff --git a/php/src/GildedRose.php b/php/src/GildedRose.php index db81ee3b..e0a6ee10 100644 --- a/php/src/GildedRose.php +++ b/php/src/GildedRose.php @@ -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() + }; + } } diff --git a/php/src/ItemUpdater/Abstract/AbstractItemUpdater.php b/php/src/ItemUpdater/Abstract/AbstractItemUpdater.php new file mode 100644 index 00000000..d49943c9 --- /dev/null +++ b/php/src/ItemUpdater/Abstract/AbstractItemUpdater.php @@ -0,0 +1,73 @@ +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); + } +} diff --git a/php/src/ItemUpdater/AgedBrieItemUpdater.php b/php/src/ItemUpdater/AgedBrieItemUpdater.php new file mode 100644 index 00000000..7a17886b --- /dev/null +++ b/php/src/ItemUpdater/AgedBrieItemUpdater.php @@ -0,0 +1,23 @@ +increaseQuality($item, $this->getQualityDecrease($item)); + } +} diff --git a/php/src/ItemUpdater/BackstagePassItemUpdater.php b/php/src/ItemUpdater/BackstagePassItemUpdater.php new file mode 100644 index 00000000..fd735930 --- /dev/null +++ b/php/src/ItemUpdater/BackstagePassItemUpdater.php @@ -0,0 +1,27 @@ +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) + }; + } +} diff --git a/php/src/ItemUpdater/ConjuredItemUpdater.php b/php/src/ItemUpdater/ConjuredItemUpdater.php new file mode 100644 index 00000000..e3aba9ba --- /dev/null +++ b/php/src/ItemUpdater/ConjuredItemUpdater.php @@ -0,0 +1,23 @@ +decreaseQuality($item, $this->getQualityDecrease($item) * 2); + } +} diff --git a/php/src/ItemUpdater/Interface/ItemUpdaterInterface.php b/php/src/ItemUpdater/Interface/ItemUpdaterInterface.php new file mode 100644 index 00000000..02efe947 --- /dev/null +++ b/php/src/ItemUpdater/Interface/ItemUpdaterInterface.php @@ -0,0 +1,24 @@ +decreaseQuality($item, $this->getQualityDecrease($item)); + } +} diff --git a/php/src/ItemUpdater/SulfurasItemUpdater.php b/php/src/ItemUpdater/SulfurasItemUpdater.php new file mode 100644 index 00000000..a99394c8 --- /dev/null +++ b/php/src/ItemUpdater/SulfurasItemUpdater.php @@ -0,0 +1,22 @@ +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); } }