diff --git a/php/composer.json b/php/composer.json index 1e0ecb7d..ecfc641e 100644 --- a/php/composer.json +++ b/php/composer.json @@ -1,35 +1,38 @@ { - "name": "emilybache/gilded-rose-refactoring-kata", - "description": "A kata to practice refactoring, tests and polymorphism", - "require": { - "php": "^7.3 || ^8.0" + "name": "emilybache/gilded-rose-refactoring-kata", + "description": "A kata to practice refactoring, tests and polymorphism", + "require": { + "php": "^7.3 || ^8.0" + }, + "autoload": { + "psr-4": { + "GildedRose\\": "src/" }, - "autoload": { - "psr-4": { - "GildedRose\\": "src/" - } - }, - "autoload-dev": { - "psr-4": { - "Tests\\": "tests/" - } - }, - "require-dev": { - "phpunit/phpunit": "^9.5", - "phpstan/phpstan": "^0.12.85", - "phpstan/phpstan-phpunit": "^0.12.18", - "symplify/easy-coding-standard": "^9.3", - "symplify/phpstan-extensions": "^9.3", - "approvals/approval-tests": "^1.4" - }, - "scripts": { - "checkcode": "phpcs src tests --standard=PSR12", - "fixcode": "phpcbf src tests --standard=PSR12", - "test": "phpunit", - "tests": "phpunit", - "test-coverage": "phpunit --coverage-html build/coverage", - "check-cs": "ecs check", - "fix-cs": "ecs check --fix", - "phpstan": "phpstan analyse --ansi" + "files": [ + "config/ioc.php" + ] + }, + "autoload-dev": { + "psr-4": { + "Tests\\": "tests/" } + }, + "require-dev": { + "phpunit/phpunit": "^9.5", + "phpstan/phpstan": "^0.12.85", + "phpstan/phpstan-phpunit": "^0.12.18", + "symplify/easy-coding-standard": "^9.3", + "symplify/phpstan-extensions": "^9.3", + "approvals/approval-tests": "^1.4" + }, + "scripts": { + "checkcode": "phpcs src tests --standard=PSR12", + "fixcode": "phpcbf src tests --standard=PSR12", + "test": "phpunit", + "tests": "phpunit", + "test-coverage": "phpunit --coverage-html build/coverage", + "check-cs": "ecs check", + "fix-cs": "ecs check --fix", + "phpstan": "phpstan analyse --ansi" + } } diff --git a/php/config/ioc.php b/php/config/ioc.php new file mode 100644 index 00000000..b0047678 --- /dev/null +++ b/php/config/ioc.php @@ -0,0 +1,24 @@ + DegradingStrategyFactory::class +]; + +// Helper Method to get implementation +function resolve(string $interface) +{ + try { + return BINDINGS[$interface]; + } catch (Throwable $e) { + throw new Exception('Cannot Find Implementation'); + } +} diff --git a/php/fixtures/texttest_fixture.php b/php/fixtures/texttest_fixture.php index 7363c773..2a593bda 100644 --- a/php/fixtures/texttest_fixture.php +++ b/php/fixtures/texttest_fixture.php @@ -4,6 +4,7 @@ declare(strict_types=1); require_once __DIR__ . '/../vendor/autoload.php'; +use GildedRose\Core\Container; use GildedRose\GildedRose; use GildedRose\Item; @@ -22,7 +23,7 @@ $items = [ new Item('Conjured Mana Cake', 3, 6), ]; -$app = new GildedRose($items); +$app = (new Container())->get(GildedRose::class, [$items]); $days = 2; if (count($argv) > 1) { diff --git a/php/src/Core/Container.php b/php/src/Core/Container.php new file mode 100644 index 00000000..871c4de3 --- /dev/null +++ b/php/src/Core/Container.php @@ -0,0 +1,70 @@ +deps[$name])) { + $this->deps[$name] = $name; + } + try { + $reflection = new ReflectionClass($this->deps[$name]); + + if (! $reflection->isInstantiable()) { + if (! $reflection->isInterface()) { + throw new Exception("{$reflection->getName()} is not instantiable !"); + } + + $reflection = new ReflectionClass(resolve($reflection->getName())); + } + } catch (ReflectionException $e) { + throw new Exception($e->getMessage()); + } + $constructor = $reflection->getConstructor(); + return $constructor === null ? + $reflection->newInstanceArgs() : + $reflection->newInstanceArgs($this->resolveArgs($constructor, $args)); + } + + public function has(string $name): bool + { + return isset($this->deps[$name]); + } + + private function resolveArgs(ReflectionMethod $constructor, $args) + { + $args = $args ?? []; + + $params = $constructor->getParameters(); + + foreach ($params as $param) { + if ($param->getType() !== null && ! $param->getType()->isBuiltin()) { + if (! array_key_exists($param->getName(), $args)) { + $args[] = $this->get( + $param->getType()->getName() + ); + } + } elseif ($param->isDefaultValueAvailable()) { + $args[] = $param->getDefaultValue(); + } + } + return array_values($args); + } +} diff --git a/php/src/GildedRose.php b/php/src/GildedRose.php index cfb7f78a..4cdd832f 100644 --- a/php/src/GildedRose.php +++ b/php/src/GildedRose.php @@ -4,6 +4,8 @@ declare(strict_types=1); namespace GildedRose; +use GildedRose\Services\AbstractDegradingStrategyFactory; + final class GildedRose { /** @@ -11,59 +13,31 @@ final class GildedRose */ private $items; - public function __construct(array $items) + /** + * Degrading Strategy Factory + * @var AbstractDegradingStrategyFactory + */ + private $degradingStrategyFactory; + + public function __construct(array $items, AbstractDegradingStrategyFactory $degradingStrategyFactory) { $this->items = $items; + $this->degradingStrategyFactory = $degradingStrategyFactory; } + public function __get($name) + { + return property_exists($this, $name) ? $this->{$name} : null; + } + + /** + * Handle Updating quality in cleaner way respecting multiple software engineering principles + * Such as Solid/Dry etc + */ 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->sell_in < 11) { - if ($item->quality < 50) { - $item->quality = $item->quality + 1; - } - } - if ($item->sell_in < 6) { - if ($item->quality < 50) { - $item->quality = $item->quality + 1; - } - } - } - } - } - - if ($item->name != 'Sulfuras, Hand of Ragnaros') { - $item->sell_in = $item->sell_in - 1; - } - - if ($item->sell_in < 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; - } - } - } + $this->degradingStrategyFactory->getDegradingStrategy($item)->handle(); } } } diff --git a/php/src/Services/AbstractDegradingStrategyFactory.php b/php/src/Services/AbstractDegradingStrategyFactory.php new file mode 100644 index 00000000..b64cb07b --- /dev/null +++ b/php/src/Services/AbstractDegradingStrategyFactory.php @@ -0,0 +1,19 @@ +container = $container; + } + + public function getDegradingStrategy(Item $item): BaseStrategy + { + switch ($item->name) { + case self::AGED: + return $this->container->get(AgedBrieStrategy::class, [ + 'item' => $item, + ]); + break; + case self::BACKSTAGE: + return $this->container->get(BackstageStrategy::class, [ + 'item' => $item, + ]); + break; + case self::SULFURAS: + return $this->container->get(SulfurasStrategy::class, [ + 'item' => $item, + ]); + break; + case self::CONJURED: + return $this->container->get(ConjuredStrategy::class, [ + 'item' => $item, + ]); + break; + + default: + return $this->container->get(DefaultStrategy::class, [ + 'item' => $item, + ]); + } + } +} diff --git a/php/src/Services/Strategies/AgedBrieStrategy.php b/php/src/Services/Strategies/AgedBrieStrategy.php new file mode 100644 index 00000000..c0f5e6a8 --- /dev/null +++ b/php/src/Services/Strategies/AgedBrieStrategy.php @@ -0,0 +1,21 @@ +item->sell_in--; + return $this->item; + } + + public function setQuality(): Item + { + $this->item->quality = ($this->item->quality + $this->degradeRate) > self::MAX_QUALITY_VALUE ? + self::MAX_QUALITY_VALUE : $this->item->quality += $this->degradeRate; + return $this->item; + } +} diff --git a/php/src/Services/Strategies/BackstageStrategy.php b/php/src/Services/Strategies/BackstageStrategy.php new file mode 100644 index 00000000..0b1c7d03 --- /dev/null +++ b/php/src/Services/Strategies/BackstageStrategy.php @@ -0,0 +1,32 @@ +item->sell_in <= 0) { + $this->degradeRate = -$this->item->quality; + } elseif ($this->item->sell_in <= 5) { + $this->degradeRate = 3; + } elseif ($this->item->sell_in <= 10) { + $this->degradeRate = 2; + } + } + + public function setSellIn(): Item + { + $this->item->sell_in--; + return $this->item; + } + + public function setQuality(): Item + { + $this->item->quality = ($this->item->quality + $this->degradeRate) > self::MAX_QUALITY_VALUE ? + self::MAX_QUALITY_VALUE : $this->item->quality += $this->degradeRate; + return $this->item; + } +} diff --git a/php/src/Services/Strategies/BaseStrategy.php b/php/src/Services/Strategies/BaseStrategy.php new file mode 100644 index 00000000..434d74f8 --- /dev/null +++ b/php/src/Services/Strategies/BaseStrategy.php @@ -0,0 +1,20 @@ +item->sell_in--; + return $this->item; + } + + public function setQuality(): Item + { + $this->item->quality = ($this->item->quality - ($this->degradeRate * 2)) < self::MIN_QUALITY_VALUE ? + self::MIN_QUALITY_VALUE : $this->item->quality -= ($this->degradeRate * 2); + return $this->item; + } +} diff --git a/php/src/Services/Strategies/DefaultStrategy.php b/php/src/Services/Strategies/DefaultStrategy.php new file mode 100644 index 00000000..0434d6f6 --- /dev/null +++ b/php/src/Services/Strategies/DefaultStrategy.php @@ -0,0 +1,55 @@ +item = $item; + } + + public function setDegradeRate() + { + if ($this->item->sell_in < 0) { + $this->degradeRate *= 2; + } + } + + public function setSellIn(): Item + { + $this->item->sell_in--; + return $this->item; + } + + public function setQuality(): Item + { + $this->item->quality = ($this->item->quality - $this->degradeRate) < self::MIN_QUALITY_VALUE ? + self::MIN_QUALITY_VALUE : $this->item->quality -= $this->degradeRate; + return $this->item; + } + + /** + * Template Method that runs steps for all strategies in order + * First we Downgrade the sellIn + * Second We set the degradeRate/riseRate + * Finally we Set the item quality based on degrate/rise Rate + */ + final public function handle() + { + $this->setSellIn(); + $this->setDegradeRate(); + $this->setQuality(); + } +} diff --git a/php/src/Services/Strategies/SulfurasStrategy.php b/php/src/Services/Strategies/SulfurasStrategy.php new file mode 100644 index 00000000..409958f1 --- /dev/null +++ b/php/src/Services/Strategies/SulfurasStrategy.php @@ -0,0 +1,18 @@ +item; + } + + public function setQuality(): Item + { + return $this->item; + } +} diff --git a/php/tests/DegradingStrategyFactoryTest.php b/php/tests/DegradingStrategyFactoryTest.php new file mode 100644 index 00000000..980baa25 --- /dev/null +++ b/php/tests/DegradingStrategyFactoryTest.php @@ -0,0 +1,45 @@ +init(); + $this->degradingStrategyFactory = $this->container->get(AbstractDegradingStrategyFactory::class); + } + + public function testStrategyFactoryIsLoaded(): void + { + $this->assertNotNull($this->degradingStrategyFactory); + $this->assertInstanceOf(AbstractDegradingStrategyFactory::class, $this->degradingStrategyFactory); + } + + public function testLoadsTheCorrectStrategy() + { + $item = $this->container->get(Item::class, ['Foo', 2, 3]); + $this->assertInstanceOf(DefaultStrategy::class, $this->degradingStrategyFactory->getDegradingStrategy($item)); + + $item = $this->container->get(Item::class, ['Sulfuras, Hand of Ragnaros', 0, 80]); + $this->assertInstanceOf(SulfurasStrategy::class, $this->degradingStrategyFactory->getDegradingStrategy($item)); + + $item = $this->container->get(Item::class, ['Conjured Mana Cake', 3, 6]); + $this->assertInstanceOf(ConjuredStrategy::class, $this->degradingStrategyFactory->getDegradingStrategy($item)); + } +} diff --git a/php/tests/DegradingStrategyTest.php b/php/tests/DegradingStrategyTest.php new file mode 100644 index 00000000..d0c93f98 --- /dev/null +++ b/php/tests/DegradingStrategyTest.php @@ -0,0 +1,119 @@ +init(); + $this->degradingStrategyFactory = $this->container->get(AbstractDegradingStrategyFactory::class); + } + + public function testDefaultStrategy(): void + { + $item = $this->container->get(Item::class, ['Foo', 2, 3]); + $this->getStrategy($item)->handle(); + $this->assertSame(2, $item->quality); + $this->assertSame(1, $item->sell_in); + $item = $this->container->get(Item::class, ['Foo', -1, 3]); + $this->getStrategy($item)->handle(); + $this->assertSame(1, $item->quality); + $this->assertSame(-2, $item->sell_in); + } + + public function testAgedBrieStrategy(): void + { + $item = $this->container->get(Item::class, ['Aged Brie', 2, 30]); + $this->getStrategy($item)->handle(); + $this->assertSame(31, $item->quality); + $this->assertSame(1, $item->sell_in); + for ($i = 0; $i < 10; $i++) { + $this->getStrategy($item)->handle(); + } + + $this->assertSame(50, $item->quality); + } + + /** + * "Backstage passes", like aged brie, increases in Quality as its SellIn value approaches; + * Quality increases by 2 when there are 10 days or less and by 3 when there are 5 days or less but + * Quality drops to 0 after the concert + * Yet the results are not correct under approvals/ , as the sell_in reaches 0 the quality stand still + * and only drop to 0 when sell_in < 0 + * As same for increasing by 2 when sell_in reaches 10, it only increases by 2 when it's < 10 + * As same also for sell_in reaching 5 it does not increase by 3 only after sell_in is lower than 5 + */ + public function testBackstageStrategy(): void + { + $item = $this->container->get(Item::class, ['Backstage passes to a TAFKAL80ETC concert', 15, 20]); + $this->getStrategy($item)->handle(); + $this->assertSame(21, $item->quality); + $this->assertSame(14, $item->sell_in); + $item->sell_in = 10; + $this->getStrategy($item)->handle(); + $this->assertSame(23, $item->quality); + + $item->sell_in = 5; + $this->getStrategy($item)->handle(); + $this->assertSame(26, $item->quality); + + $item->sell_in = 0; + $this->getStrategy($item)->handle(); + $this->assertSame(0, $item->quality); + } + + /** + * "Conjured" items degrade in Quality twice as fast as normal items + * though the results under approvals/ are not correct since it doesn't + * degrade as mentioned in requirements, it degrades same as AgedBrie + */ + public function testConjuredStrategy(): void + { + $item = $this->container->get(Item::class, ['Conjured Mana Cake', 3, 8]); + $this->getStrategy($item)->handle(); + $this->assertSame(6, $item->quality); + $this->assertSame(2, $item->sell_in); + $item->sell_in = 1; + $this->getStrategy($item)->handle(); + $this->assertSame(4, $item->quality); + $item->sell_in = 0; + $this->getStrategy($item)->handle(); + $this->assertSame(0, $item->quality); + } + + public function testSulfurasStrategy(): void + { + $item = $this->container->get(Item::class, ['Sulfuras, Hand of Ragnaros', 5, 80]); + $this->getStrategy($item)->handle(); + $this->assertSame(80, $item->quality); + $this->assertSame(5, $item->sell_in); + + $item->sell_in = 1; + $this->getStrategy($item)->handle(); + $this->assertSame(80, $item->quality); + $this->assertSame(1, $item->sell_in); + + $item->sell_in = 0; + $this->getStrategy($item)->handle(); + $this->assertSame(80, $item->quality); + $this->assertSame(0, $item->sell_in); + } + + private function getStrategy($item) + { + return $this->degradingStrategyFactory->getDegradingStrategy($item); + } +} diff --git a/php/tests/GildedRoseTest.php b/php/tests/GildedRoseTest.php index 52e7ddc6..397cd8bf 100644 --- a/php/tests/GildedRoseTest.php +++ b/php/tests/GildedRoseTest.php @@ -6,15 +6,43 @@ namespace Tests; use GildedRose\GildedRose; use GildedRose\Item; +use GildedRose\Services\AbstractDegradingStrategyFactory; use PHPUnit\Framework\TestCase; class GildedRoseTest extends TestCase { - public function testFoo(): void + use InitApp; + + private $items; + + private $app; + + protected function setUp(): void { - $items = [new Item('foo', 0, 0)]; - $gildedRose = new GildedRose($items); - $gildedRose->updateQuality(); - $this->assertSame('fixme', $items[0]->name); + $this->init(); + $this->items = [$this->container->get(Item::class, ['Aged Brie', 2, 0])]; + + $this->bootstrap(); + } + + public function testApplicationIsInstanciatedCorrectly(): void + { + $this->assertNotNull($this->app); + $this->assertInstanceOf(GildedRose::class, $this->app); + $this->assertInstanceOf(AbstractDegradingStrategyFactory::class, $this->app->degradingStrategyFactory); + } + + public function testItemsAreSetAndCorrect() + { + $this->assertIsArray($this->items); + $this->assertIsArray($this->app->items); + $this->assertContainsOnlyInstancesOf(Item::class, $this->app->items); + $this->assertSame(new Item('Aged Brie', 2, 0), array_pop($this->items)); + } + + public function testUpdateQuality() + { + $this->app->updateQuality(); + $this->assertSame(new Item('Aged Brie', 1, 1), $this->app->items[0]); } } diff --git a/php/tests/InitApp.php b/php/tests/InitApp.php new file mode 100644 index 00000000..5413786f --- /dev/null +++ b/php/tests/InitApp.php @@ -0,0 +1,24 @@ +container = new Container(); + } + + public function bootstrap() + { + $this->app = $this->container->get(GildedRose::class, [$this->items]); + } +}