Rôzne ceny za rôznych okolností
- MajklNajt
- Člen | 498
Nazdar kluci a klučky,
riešim v eshope takú skôr návrhovú otázku týkajúcu sa zobrazovania rôznych cien pre rôznych návštevníkov za rôznych podmienok, mám to celé v ORM, čiže do šablóny mi dojde kolekcia nejakých entít, napr.:
class Product
{
private Price $price;
public function getPrice(): Price
{
return $this->price;
}
}
Cenu teda v šablóne vypisujem jednoducho
$product->getPrice()
Tieto ceny však chcem pri splnení určitých podmienok modikovať, a tu nastáva ten vnútorný boj :)
Napadlo ma predávať závislosti priamo getteru, ktorý to prepočíta a vráti modifikovanú cenu:
public function getPrice(...$modifiers): Price
{
$price = $this->price;
foreach($modifiers as $modifier) {
$price = $price->modify($modifier);
}
return $price;
}
Čiže v šablóne budem mať
$product->getPrice($customer, $season, $otherRule)
Pri tomto riešení však nastane problém, keď sa v budúcnosti rozhodnem pridať ďalší „modifikátor“, budem to musieť pridávať vo všetkých šablónach a celkovo mi to príde nepekné…
Druhé riešenie je posielať si do šablóny nejeký resolver, kde si napr. v BasePresenteri (alebo niekde inde, to nie je teraz podstatné) nasetujem všetky „modifikátory“
class PriceResolver
{
private array $modifiers = [];
public function setCustomer(IModifier $modifier): void
{
$this->modifiers[] = $modifier;
}
public function setSeason(IModifier $modifier): void
{
$this->modifiers[] = $modifier;
}
public function setOtherRule(IModifier $modifier): void
{
$this->modifiers[] = $modifier;
}
public function resolve(Product $product): Price
{
$price = $product->getPrice();
foreach($modifiers as $modifier) {
$price = $price->modify($modifier);
}
return $price;
}
}
Čiže v šablóne budem volať
$priceResolver->resolve($product)
Čo už mi príde trošku krajšie, ale stále s tým akosi nie som spokojný…
Ako takéto niečo riešite vy?
Díky za názory :)
- dsar
- Backer | 53
Sorry for the use of english language here.
In my opinion today the best and simplest approach is to have a field that
stores a formula that modify the final price in a cart. This is very useful
considering that we have operators that are not programmers (but they can learn
a basic syntax for writing simple expressions).
Our e-shop uses a Symfony component called Expression language, but I think you can achieve the same
with Nette Tokenizer (although
less simple).
Here you can find an example that is very similar to your resolver (we use instead a separated table for discounts)
- joe
- Člen | 313
Není to tak dlouho, co jsem řešil podobný problém a vlastně i stále
tak trochu řeším. Nakonec jsem nechal po šablonách jak píšeš
$product->getPrice($customer, $season, $otherRule)
. Ale je to na
minimum místech, protože to mám v komponentách.
Ukázka řešení výš mi přijde hrozně ošklivá (ať už je to moderní jak chce), psát funkce a metody do stringu? …
Jestli si vystačíš s PriceResolver
, tak bych šel tou
cestou… ale. Ale já potřebuji někde ceny s DPH, někde ceny bez DPH,
někde ceny s aplikovanou slevou, jinde bez aplikované slevy, v některých
místech potřebuju i vědět, jaká ta sleva byla, některé slevy potřebuju
řešit ne jednotkové ceny, jiné zase až v závislosti na zadaném
množství a řeším poměrně dost takových podmínek (doteď přesně nevím
jak přesně mám ceny počítat a nemám to specifikováno, ha).
- MajklNajt
- Člen | 498
ja takisto potrebujem zobrazovať rôzne ceny, vydal som sa cestou reloveru,
ktorý prijíma všetky premenné, čiže keď potrebujem vypísať základnú
cenu, volám iba $product->getPrice()
, keď potrebujem cenu
s DPH, volám $priceResolver->applyVat($product)
, keď
potrebujem cenu so zľavami, vypisujem
$priceResolver->applyDiscounts($product, applyVat: true/false)
,
keď potrebujem vrátiť iba zľavy, použijem
$priceResolver->listDiscounts($product)
… páči sa mi, že
tá logika je stále v resolveri…
- Kcko
- Člen | 468
My to řešíme jinak. Máme upravenou Nette\Table\ActiveRow o vlastní datové typy, které prezentují konkrétní tabulku.
Takže pokud fetchnu řádek s Produktem, mám k dispozici něco jako \Nette\Table\Product a můžu si do datového typu dopsat cokoliv co chci (v rámci třídy používám $this = což je vlastně aktuální záznam v DB).
Takže pokud jsem v PHP nebo v Latte tak můžu psát $product // ActiveRow
a rovnou volat metody z datatypu $product->basePrice()
$productPriceWoVat() apod.
Mám to k dispozici v každém kontextu bez serepetiček, resolverů a dalších různých injectů, helper modelů apod.
Editoval Kcko (3. 2. 2022 21:34)
- MajklNajt
- Člen | 498
Kcko napsal(a):
My to řešíme jinak. Máme upravenou Nette\Table\ActiveRow o vlastní datové typy, které prezentují konkrétní tabulku.
Takže pokud fetchnu řádek s Produktem, mám k dispozici něco jako \Nette\Table\Product a můžu si do datového typu dopsat cokoliv co chci (v rámci třídy používám $this = což je vlastně aktuální záznam v DB).
Takže pokud jsem v PHP nebo v Latte tak můžu psát $product // ActiveRow a rovnou volat metody z datatypu $product->basePrice()
$productPriceWoVat() apod.Mám to k dispozici v každém kontextu bez serepetiček, resolverů a dalších různých injectů, helper modelů apod.
ja frčím na doktríne, takže toto mám poriešené… skôr mi ide o spôsob, akým ceny upravovať vplyvom externých faktorov, t.j. nezávislých na konkrétnom produkte (riadku z DB)
- m.brecher
- Generous Backer | 871
Čiže v šablóne budem mať
$product->getPrice($customer, $season, $otherRule)
a
>
Čiže v šablóne budem volať
$priceResolver->resolve($product)
Pokud jsem tomu správně porozuměl, tak si v presenteru předáš do šablony modelovou třídu $product a v šabloně si z ní potom taháš co potřebuješ a případně si data v šabloně upravíš resolverem nebo modifikátory jako parametrem.
Určitě to takhle jde, ale pokud se nemýlím, tak koncept MVC modelu doporučuje, aby šablona pouze vykreslovala a požadovaná data dodal model. Rovněž koncepce DI doporučuje předávat vždy konkrétní závislosti, nikoliv předat velký balík všech závislostí, z nichž si komponenta vybere co potřebuje. Do šablony je ideální předat hotový objekt který se dá rovnou vykreslit, aby byla šablona jednodušší, přehlednější a více znovupoužitelná. Samozřejmě někdy to nejde, nebo je jednodušší data dodatečně upravit v šabloně, ale to je spíše vyjímečné.
PriceResolver by měl být v Modelu a třída Product by ho měla rovnou použít tak, aby do Presenteru dodala již vykreslitelná data. Ideální je, když šablona vůbec neví, jak se vykreslovaná data získaly.
- joe
- Člen | 313
MajklNajt napsal(a):
ja takisto potrebujem zobrazovať rôzne ceny, vydal som sa cestou reloveru, ktorý prijíma všetky premenné, čiže keď potrebujem vypísať základnú cenu, volám iba
$product->getPrice()
, keď potrebujem cenu s DPH, volám$priceResolver->applyVat($product)
, keď potrebujem cenu so zľavami, vypisujem$priceResolver->applyDiscounts($product, applyVat: true/false)
, keď potrebujem vrátiť iba zľavy, použijem$priceResolver->listDiscounts($product)
… páči sa mi, že tá logika je stále v resolveri…
V podstatě to mám stejně, jenom mám metody v rámci produktu, které
vedou do resolveru :) Asi to není programově čisté a nejsem vyloženě jen
programátor, ale z lidskýho hlediska se mi líbí víc zápis v šabloně
$product->getPrice(...)
než
$resolver->...($product)
.
(Jako ORM mám LeanMapper, tak si ty entity upravuju trochu k obrazu svému)
Editoval joe (4. 2. 2022 13:06)
- MajklNajt
- Člen | 498
@m.brecher
$product
je entita (ORM ti mapuje dáta z databázy na
objekty=entity, čo ale nie je predmetom diskusie), čiže nie je to služba,
ktorú by vytváral DI, je to obálka na dáta, ktorú vytvára v mojom
prípade Doctrine, práve vďaka tomu mám úplne odtienený logiku od
zobrazovania – keďže ju ale nevytvára DI, neviem jej cez DI
predať $resolver
$resolver
je potom služba (čiže objekt modelovej vrstvy),
ktorú si odovzdám do šablóny, aby vykonala logiku nad nejakou entitou a
vrátila vykresliteľné dáta – toto je čisté riešenie, netvorím logiku
v šablóne, v šablóne iba poviem, že chcem vykresliť cenu s DPH a
aplikovanými zľavami, o všetko ostatné sa stará resolver/model.
Editoval MajklNajt (4. 2. 2022 13:21)
- Klobás
- Člen | 113
MajklNajt napsal(a):
Ahoj, jen z hlediska výuky.
public function resolve(Product $product): Price
{
$price = $product->getPrice();
foreach($modifiers as $modifier) {
$price = $price->modify($modifier);
}
return $price;
}
$product->getPrice()
vrací tedy cenu nebo Price
objekt?
co přesně dělá $price->modify
a co vrací? Zmodifikovaný
objekt ceny?
Nějaký Decorator
pattern nebo je tohle pseudokód?
- MajklNajt
- Člen | 498
Klobás napsal(a):
MajklNajt napsal(a):
@Klobás ahoj, obe metódy vracajú objekt triedy Price, modify robí to, že hodnotu ceny zvýši/zníži a vráti novú inštanciu Price, nakoľko ide o Value Object
Ahoj, děkuji.
Pokud to není tajné a příliš dlouhé, mohl bych to vidět?
asi je to off-topic, ale nech sa páči, posielam obe triedy (Price aj Modifier), aby to teda malo možno nejaký úžitok aj pre iných
<?php
class Price
{
private float $amount;
private string $currency;
public function __construct(float $amount, string $currency)
{
$this->amount = $amount;
$this->currency = $currency;
}
public function getAmount(int $precision = 2): float
{
return round($this->amount, $precision);
}
public function getCurrency(): string
{
return $this->currency;
}
public function modify(Modifier $modifier): Price
{
if($modifier->isPercentage()) {
$amount = $this->getAmount(4) * (100 + $modifier->getAmount()) / 100;
}
else {
$amount = $this->getAmount(4) + $modifier->getAmount();
}
return new self($amount, $this->currency);
}
public function multiply(float $multiplier): Price
{
return new self($this->amount * $multiplier, $this->currency);
}
public function divide(float $divisor): Price
{
return new self($this->amount / $divisor, $this->currency);
}
public function add(Price $price): Price
{
if($this->currency !== $price->getCurrency()) {
throw new CurrencyMismatchException("Currency missmatch");
}
return new self($this->amount + $price->getAmount(4), $this->currency);
}
public function minus(Price $price): Price
{
if($this->currency !== $price->getCurrency()) {
throw new CurrencyMismatchException("Currency missmatch");
}
return new self($this->amount - $price->getAmount(4), $this->currency);
}
public function equals(Price $other): bool
{
return $this->getAmount(4) === $other->getAmount(4) and $this->currency === $other->getCurrency();
}
}
?>
<?php
class Modifier
{
const TYPE_PERCENTAGE = "P";
const TYPE_ABSOLUTE = "A";
private string $type;
private float $amount;
public function __construct(string $type, float $amount)
{
$this->type = $type;
$this->amount = $amount;
}
public function getType(): string
{
return $this->type;
}
public function getAmount(int $precision = 2): float
{
return round($this->amount, $precision);
}
public function cumulate(Modifier $modifier): Modifier
{
if($this->type !== $modifier->getType()) {
throw new TypeMissmatchException("Type missmatch");
}
return new self($this->type, $this->amount + $modifier->getAmount(4));
}
public function isPercentage(): bool
{
return $this->type === self::TYPE_PERCENTAGE;
}
public function isAbsolute(): bool
{
return $this->type === self::TYPE_ABSOLUTE;
}
}
?>
- dsar
- Backer | 53
<?php
class Price
{
private float $amount;
private string $currency;
[...]
Don't use float for money, it's conceptually the wrong type and the main headache of wrong results.
For money, or everything that must have an exact answer, fixed-point arithmetic must be used instead of floating-point arithmetic, in PHP this is supported with BCMath (that operates with strings, but it is well optimized and fast) or, if not available, integers could be used. Some other languages have the decimal type.
Or leave the calculation to the database (if you used numeric/decimal type) that is also well optimized and fast.
Editoval dsar (5. 2. 2022 19:16)
- m.brecher
- Generous Backer | 871
MajklNajt napsal(a):
@m.brecher
$product
je entita (ORM ti mapuje dáta z databázy na objekty=entity, čo ale nie je predmetom diskusie), čiže nie je to služba, ktorú by vytváral DI, je to obálka na dáta, ktorú vytvára v mojom prípade Doctrine, práve vďaka tomu mám úplne odtienený logiku od zobrazovania – keďže ju ale nevytvára DI, neviem jej cez DI predať$resolver
$resolver
je potom služba (čiže objekt modelovej vrstvy), ktorú si odovzdám do šablóny, aby vykonala logiku nad nejakou entitou a vrátila vykresliteľné dáta – toto je čisté riešenie, netvorím logiku v šablóne, v šablóne iba poviem, že chcem vykresliť cenu s DPH a aplikovanými zľavami, o všetko ostatné sa stará resolver/model.
Ahoj, ano, jde to tak – „dočistit“ data až v šabloně a také to tak někdy dělám, ale správnější je, aby čistá data dodal Model. V praxi je ale někdy jednodušší filtrovat/agregovat výsledky až v šabloně. A jestli je to jak píšeš, že by asi nešlo použít $resolver v entitě Doctríny, tak je to zřejmě nejlepší postup.