Examples for presentation.

This commit is contained in:
giedrius
2024-11-19 13:57:03 +02:00
commit 41f2b5eec1
23 changed files with 3351 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
.idea
composer.phar
vendor
.phpunit.result.cache

1
README.md Normal file
View File

@@ -0,0 +1 @@
Example for presentation.

30
composer.json Normal file
View File

@@ -0,0 +1,30 @@
{
"name": "kukulis/readable-example",
"description": "Example for presentation",
"type": "project",
"authors": [
{
"name": "Giedrius Tumelis"
}
],
"autoload": {
"psr-4": {
"Example\\": "sources/Example/"
}
},
"autoload-dev": {
"psr-4": {
"Tests\\": "tests/"
}
},
"require": {
"psr/event-dispatcher": "^1.0",
"symfony/uid": "^5.4",
"guzzlehttp/guzzle": "^7.9",
"ext-pdo": "*",
"ext-json": "*"
},
"require-dev": {
"phpunit/phpunit": "^9.5"
}
}

2563
composer.lock generated Normal file

File diff suppressed because it is too large Load Diff

12
phpunit.xml Normal file
View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="./vendor/phpunit/phpunit/phpunit.xsd"
bootstrap="vendor/autoload.php"
colors="true"
defaultTestSuite="Unit" >
<testsuites>
<testsuite name="Unit">
<directory suffix="Test.php">./tests/Unit</directory>
</testsuite>
</testsuites>
</phpunit>

View File

@@ -0,0 +1,57 @@
<?php
namespace Example\Bad;
use GuzzleHttp\Client;
class APIService
{
private Repository $dbService;
private Client $client;
public function __construct(Repository $dbService, Client $client)
{
$this->dbService = $dbService;
$this->client = $client;
}
public function getApiData(string $filter) : array
{
$response = $this->client->request('GET', 'api_endpoint/products', [
'query' => ['filter'=>$filter]
]);
$json = $response->getBody()->getContents();
return json_decode($json, true);
}
/**
* @return Product[]
*/
public function getProducts(string $filter) : array {
$data = $this->getApiData($filter);
$skus = [];
foreach ($data as $product) {
$skus[] = $product['sku'];
}
$products = $this->dbService->getProducts($skus);
/** @var Product[] $productsMap */
$productsMap = [];
foreach ($products as $product) {
$productsMap[$product->sku] = $product;
}
foreach ($data as $apiProduct) {
$productsMap[$apiProduct['sku']]->quantity = $apiProduct['amount'];
}
return $products;
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace Example\Bad;
class CalculatorService
{
private float $vatPercent;
private APIService $apiService;
public function __construct(float $vatPercent, APIService $apiService)
{
$this->vatPercent = $vatPercent;
$this->apiService = $apiService;
}
/**
* @return Product[]
*/
public function getProducts(string $requestData) : array {
$products = $this->apiService->getProducts($requestData);
foreach ($products as $product) {
$product->price = $product->price * ( 1 + $this->vatPercent);
}
return $products;
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace Example\Bad;
use Psr\Http\Message\RequestInterface;
class Controller
{
private CalculatorService $calculator;
public function __construct(CalculatorService $calculator)
{
$this->calculator = $calculator;
}
/**
* @return Product[]
*/
public function getProducts(RequestInterface $request) : array {
$params = $request->getBody();
return $this->calculator->getProducts($params);
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace Example\Bad;
class Product
{
public string $sku = '';
public int $quantity = 0;
public float $price = 0;
public function setSku(string $sku): Product
{
$this->sku = $sku;
return $this;
}
public function setQuantity(int $quantity): Product
{
$this->quantity = $quantity;
return $this;
}
public function setPrice(float $price): Product
{
$this->price = $price;
return $this;
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace Example\Bad;
use PDO;
class Repository
{
private PDO $pdo;
public function __construct(PDO $pdo)
{
$this->pdo = $pdo;
}
/**
* @return Product[]
*/
public function getProducts($skus) : array{
$quotedSkus = [];
foreach($skus as $sku){
$quotedSkus[] = $this->pdo->quote($sku);
}
$skusStr = implode(',', $quotedSkus);
$sql = "SELECT * FROM products where id in ($skusStr)";
return $this->pdo->query($sql)->fetchAll(PDO::FETCH_CLASS, Product::class);
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace Example\Good;
use GuzzleHttp\Client;
class ApiClient
{
private Client $client;
public function __construct(Client $client)
{
$this->client = $client;
}
/**
* @return Product[]
*/
public function getProducts(string $filter): array
{
$response = $this->client->request('GET', 'api_endpoint/products', [
'query' => ['filter' => $filter]
]);
$json = $response->getBody()->getContents();
$data = json_decode($json, true);
$products = [];
foreach ($data as $productArray) {
$products[] = (new Product())->setData($productArray);
}
return $products;
}
}

View File

@@ -0,0 +1,43 @@
<?php
namespace Example\Good;
use Psr\Http\Message\RequestInterface;
class Controller
{
private ApiClient $apiClient;
private Repository $repository;
private VATCalculator $vatCalculator;
public function __construct(ApiClient $apiClient, Repository $repository, VATCalculator $vatCalculator)
{
$this->apiClient = $apiClient;
$this->repository = $repository;
$this->vatCalculator = $vatCalculator;
}
/**
* @return Product[]
*/
public function getProducts(RequestInterface $request) : array {
$params = $request->getBody();
$products = $this->apiClient->getProducts($params);
$skus = [];
foreach ($products as $product) {
$skus[] = $product->sku;
}
$dbProducts = $this->repository->getProducts($skus);
Product::copyQuantities($dbProducts, $products);
$this->vatCalculator->applyVat($dbProducts);
return $dbProducts;
}
}

View File

@@ -0,0 +1,58 @@
<?php
namespace Example\Good;
class Product
{
public string $sku = '';
public int $quantity = 0;
public float $price = 0;
public function setSku(string $sku): Product
{
$this->sku = $sku;
return $this;
}
public function setQuantity(int $quantity): Product
{
$this->quantity = $quantity;
return $this;
}
public function setPrice(float $price): Product
{
$this->price = $price;
return $this;
}
/**
* Instead of this better to use some DAO lib like JMSSerializer, or cuyz/valinor.
*/
public function setData(array $data): Product {
$this->sku = $data['sku'] ?? '';
$this->quantity = $data['amount'] ?? 0;
$this->price = $data['price'] ?? 0;
return $this;
}
/**
* @param Product[] $destinationProducts
* @param Product[] $sourceProducts
*/
public static function copyQuantities(array $destinationProducts, array $sourceProducts ) {
$indexedBySku = [];
foreach ($destinationProducts as $destinationProduct) {
$indexedBySku[$destinationProduct->sku] = $destinationProduct;
}
foreach ($sourceProducts as $sourceProduct) {
if ( !array_key_exists($sourceProduct->sku, $indexedBySku) ) {
continue;
}
$indexedBySku[$sourceProduct->sku]->setQuantity($sourceProduct->quantity);
}
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace Example\Good;
use PDO;
class Repository
{
private PDO $pdo;
public function __construct(PDO $pdo)
{
$this->pdo = $pdo;
}
/**
* @return Product[]
*/
public function getProducts($skus) : array{
$quotedSkus = [];
foreach($skus as $sku){
$quotedSkus[] = $this->pdo->quote($sku);
}
$skusStr = implode(',', $quotedSkus);
$sql = "SELECT * FROM products where id in ($skusStr)";
return $this->pdo->query($sql)->fetchAll(PDO::FETCH_CLASS, Product::class);
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace Example\Good;
class VATCalculator
{
private float $vat;
/**
* @param float $vat
*/
public function __construct(float $vat)
{
$this->vat = $vat;
}
/**
* @param Product[] $products
*/
public function applyVat(array $products) : void {
foreach ($products as $product) {
$product->setPrice( $product->price * (1+$this->vat/100) );
}
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace Example\VeryGood;
use GuzzleHttp\Client;
class ApiClient
{
private Client $client;
public function __construct(Client $client)
{
$this->client = $client;
}
/**
* @return Product[]
*/
public function getProducts(string $filter): array {
$response = $this->client->request('GET', 'api_endpoint/products', [
'query' => ['filter'=>$filter]
]);
$json = $response->getBody()->getContents();
$data = json_decode($json, true);
return array_map( fn($element)=> (new Product())->setData($element), $data);
}
}

View File

@@ -0,0 +1,40 @@
<?php
namespace Example\VeryGood;
use Psr\Http\Message\RequestInterface;
class Controller
{
private ApiClient $apiClient;
private Repository $repository;
private float $vat;
public function __construct(ApiClient $apiClient, Repository $repository, float $vat)
{
$this->apiClient = $apiClient;
$this->repository = $repository;
$this->vat = $vat;
}
public function getProducts(RequestInterface $request): array
{
$params = $request->getBody();
$apiProducts = $this->apiClient->getProducts($params);
$skus = array_map(fn($product) => $product->sku, $apiProducts);
$dbProducts = $this->repository->getProducts($skus);
Indexer::applyWhenMatch(
$dbProducts,
$apiProducts,
fn($product) => $product->getSku(),
fn($product) => $product->getSku(),
fn(Product $dbProduct, Product $apiProduct) => $dbProduct->setQuantity($apiProduct->quantity));
return array_map(fn($product) => $product->applyVat($this->vat), $dbProducts);
}
}

View File

@@ -0,0 +1,34 @@
<?php
namespace Example\VeryGood;
class Indexer
{
public static function reindex(array $elements, callable $indexGetter)
{
$result = [];
foreach ($elements as $element) {
$index = call_user_func($indexGetter, $element);
$result[$index] = $element;
}
return $result;
}
public static function applyWhenMatch(
array $elements,
array $sourceElements,
callable $sourceKeyGetter,
callable $destinationKeyGetter,
callable $action
)
{
$indexedSources = self::reindex($sourceElements, $sourceKeyGetter);
foreach ($elements as $element) {
$destinationKey = call_user_func($destinationKeyGetter, $element);
if (!array_key_exists($destinationKey, $indexedSources)) {
continue;
}
call_user_func($action, $element, $indexedSources[$destinationKey]);
}
}
}

View File

@@ -0,0 +1,62 @@
<?php
namespace Example\VeryGood;
class Product
{
public string $sku = '';
public int $quantity = 0;
public float $price = 0;
public function setSku(string $sku): Product
{
$this->sku = $sku;
return $this;
}
public function setQuantity(int $quantity): Product
{
$this->quantity = $quantity;
return $this;
}
public function setPrice(float $price): Product
{
$this->price = $price;
return $this;
}
/**
* Instead of this better to use some DAO lib like JMSSerializer, or cuyz/valinor.
*/
public function setData(array $data): Product
{
$this->sku = $data['sku'] ?? '';
$this->quantity = $data['amount'] ?? 0;
$this->price = $data['price'] ?? 0;
return $this;
}
public function applyVat(float $vat): Product
{
$this->price = $this->price * (1 + $vat / 100);
return $this;
}
public function getSku(): string
{
return $this->sku;
}
public function getQuantity(): int
{
return $this->quantity;
}
public function getPrice(): float
{
return $this->price;
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace Example\VeryGood;
use PDO;
class Repository
{
private PDO $pdo;
public function __construct(PDO $pdo)
{
$this->pdo = $pdo;
}
/**
* @return Product[]
*/
public function getProducts($skus): array
{
$quotedSkus = [];
foreach ($skus as $sku) {
$quotedSkus[] = $this->pdo->quote($sku);
}
$skusStr = implode(',', $quotedSkus);
$sql = "SELECT * FROM products where id in ($skusStr)";
return $this->pdo->query($sql)->fetchAll(PDO::FETCH_CLASS, Product::class);
}
}

58
tests/Unit/BadTest.php Normal file
View File

@@ -0,0 +1,58 @@
<?php
namespace Tests\Unit;
use Example\Bad\APIService;
use Example\Bad\CalculatorService;
use Example\Bad\Controller;
use Example\Bad\Repository;
use Example\Bad\Product;
use GuzzleHttp\Client;
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Psr7\Response;
use PDO;
use PDOStatement;
use PHPUnit\Framework\TestCase;
class BadTest extends TestCase
{
public function testBad()
{
$guzzleClient = $this->createMock(Client::class);
// make call of controller from the guzzle client
$guzzleClient->method('request')->willReturn(
new Response(200, [],
'[
{ "sku" : "x1234", "amount": "10" },
{ "sku" : "x1235", "amount": "20" },
{ "sku" : "x4567", "amount": "5" }
]'
)
);
$pdo = $this->createMock(PDO::class);
$pdoResult = $this->createMock(PDOStatement::class);
$pdoResult->method('fetchAll')->willReturn([
(new Product())->setSku("x1234")->setPrice(10),
(new Product())->setSku("x1235")->setPrice(20),
(new Product())->setSku("x4567")->setPrice(30),
]);
$pdo->method('query')->willReturn($pdoResult);
$dbService = new Repository($pdo);
$apiService = new ApiService($dbService, $guzzleClient);
$calculatorService = new CalculatorService(0.20, $apiService);
$controller = new Controller($calculatorService);
$products = $controller->getProducts(new Request('get', '/api', [], 'cosmetics'));
$expectedProducts = [
(new Product())->setSku("x1234")->setPrice(12)->setQuantity(10),
(new Product())->setSku("x1235")->setPrice(24)->setQuantity(20),
(new Product())->setSku("x4567")->setPrice(36)->setQuantity(5),
];
$this->assertEquals($expectedProducts, $products);
}
}

63
tests/Unit/GoodTest.php Normal file
View File

@@ -0,0 +1,63 @@
<?php
namespace Tests\Unit;
use Example\Good\ApiClient;
use Example\Good\Controller;
use Example\Good\Product;
use Example\Good\Repository;
use Example\Good\VATCalculator;
use GuzzleHttp\Client;
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Psr7\Response;
use PDO;
use PDOStatement;
use PHPUnit\Framework\TestCase;
class GoodTest extends TestCase
{
public function testGood()
{
$guzzleClient = $this->createMock(Client::class);
// make call of controller from the guzzle client
$guzzleClient->method('request')->willReturnCallback(
function () {
return new Response(200, [],
'[
{ "sku" : "x1234", "amount": "10" },
{ "sku" : "x1235", "amount": "20" },
{ "sku" : "x4567", "amount": "5" }
]'
);
}
);
$pdo = $this->createMock(PDO::class);
$pdoResult = $this->createMock(PDOStatement::class);
$pdoResult->method('fetchAll')->willReturn([
(new Product())->setSku("x1234")->setPrice(10),
(new Product())->setSku("x1235")->setPrice(20),
(new Product())->setSku("x4567")->setPrice(30),
]);
$pdo->method('query')->willReturn($pdoResult);
$dbService = new Repository($pdo);
$apiService = new ApiClient($guzzleClient);
$vatCalculator = new VATCalculator(20);
$controller = new Controller($apiService, $dbService, $vatCalculator);
$products = $controller->getProducts(new Request('get', '/api', [], 'cosmetics'));
$expectedProducts = [
(new Product())->setSku("x1234")->setPrice(12)->setQuantity(10),
(new Product())->setSku("x1235")->setPrice(24)->setQuantity(20),
(new Product())->setSku("x4567")->setPrice(36)->setQuantity(5),
];
$this->assertEquals($expectedProducts, $products);
}
}

View File

@@ -0,0 +1,63 @@
<?php
namespace Tests\Unit;
use Example\VeryGood\ApiClient;
use Example\VeryGood\Controller;
use Example\VeryGood\Product;
use Example\VeryGood\Repository;
use GuzzleHttp\Client;
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Psr7\Response;
use PDO;
use PDOStatement;
use PHPUnit\Framework\TestCase;
class VeryGoodTest extends TestCase
{
public function testVeryGood2()
{
$guzzleClient = $this->createMock(Client::class);
// make call of controller from the guzzle client
$guzzleClient->method('request')->willReturnCallback(
function () {
return new Response(200, [],
'[
{ "sku" : "x1234", "amount": "10" },
{ "sku" : "x1235", "amount": "20" },
{ "sku" : "x4567", "amount": "5" }
]'
);
}
);
$pdo = $this->createMock(PDO::class);
$pdoResult = $this->createMock(PDOStatement::class);
$pdoResult->method('fetchAll')->willReturn([
(new Product())->setSku("x1234")->setPrice(10),
(new Product())->setSku("x1235")->setPrice(20),
(new Product())->setSku("x4567")->setPrice(30),
]);
$pdo->method('query')->willReturn($pdoResult);
$dbService = new Repository($pdo);
$apiService = new ApiClient($guzzleClient);
$controller = new Controller($apiService, $dbService, 20);
$products = $controller->getProducts(new Request('get', '/api', [], 'cosmetics'));
$expectedProducts = [
(new Product())->setSku("x1234")->setPrice(12)->setQuantity(10),
(new Product())->setSku("x1235")->setPrice(24)->setQuantity(20),
(new Product())->setSku("x4567")->setPrice(36)->setQuantity(5),
];
$this->assertEquals($expectedProducts, $products);
}
}