Hoi, ik werk aan een project waarin ik een class heb gemaakt die api calls doet. Dit haalt producten op, maar kan ook producten/klanten etc opslaan.
Ook heb ik een Cart class gemaakt. Ik vind het handig om alle producten zo bij te houden en het totaal bedrag te bereken. De Class methodes worden uitgevoerd via ajax calls, dus de product ids komen vanuit de voorkant meegestuurd via JS. Nu is het zo dat ik nog het id, product naam en prijs meestuur naar de Cart. Maar wil eigenlijk alleen nog maar het id meesturen, om dat de gebruiker daar weinig mee kan saboteren. Ik weet wel hoe ik de data enzo kan ophalen, maar vind het slordig om direct die api call functies van die api class direct in mijn Cart class te gebruiken.
Als je de class Cart beschouwt als een container met references naar producten, dan is het vullen van die container een andere verantwoordelijkheid. Die verantwoordelijkheid hoort daarom buiten de class: de class Cart met dependency injection rechtstreeks afhankelijk maken van een API request handler is een onzalig idee.
Iets soortgelijks geldt voor de methode toonProducten(). Als je de class Cart nog steeds beschouwt als een class die verantwoordelijk is voor het bewaren van references naar producten, dan is het tonen van die producten een verantwoordelijkheid die buiten de class hoort.
Hetzelfde geldt in de vraag voor het berekenen van totalen: dat is niet de verantwoordelijkheid van de container, maar van een class die deze container gebruikt.
Maak daarom eens een lijstje van dingen die moeten gebeuren (de verantwoordelijkheden) en probeer elke verantwoordelijkheid vervolgens zo veel mogelijk in een aparte class onder te brengen.
Ideaal is het niet, maar topic starter vroeg hoe hij op een nette manier de API kan aanspreken in plaats van rechtstreeks aan te roepen in de class zelf. Over een wel of niet juiste interpretatie van OOP heb ik het niet gehad. Dat is een totaal andere discussie. toonProducten is in het voorbeeld inderdaad niet zaligmakend, want een (library) class hoort niks te tonen. Dat lijkt me eerder thuis te horen in een controller. Maar zoals ik zei, niet te veel waarde hechten aan de code. Het ging er even om hoe je het ene object op een nette manier kunt meegeven aan het andere.
Ideaal is het niet, maar topic starter vroeg hoe hij op een nette manier de API kan aanspreken in plaats van rechtstreeks aan te roepen in de class zelf.
En jouw oplossing is dan de API meegeven in de constructor?! Dat verplaatst het probleem, maar lost het niet op. Daarmee maak je zelfs de gehele class afhankelijk van de API. Ook wanneer je de API helemaal niet nodig hebt.
Je hebt hiermee de deur naar Dependency Hell op een kier gezet.
Leuk, al die feedback, maar nog leuker als je zelf een voorbeeldje plaatst van hoe het volgens jou wel zou moeten. Ik gaf enkel een korte reactie op hoe je iets anders kunt doen. Ik ken heel zijn framework niet, evenmin als jij (neem ik aan). Wellicht werkt hij niet eens volgens OOP. Anyhow ... ik zou zeggen, geef een mooi voorbeeld van hoe het volgens jou zou moeten. Dat lijkt me een waardevolle toevoeging aan de discussie.
?Onbekende gebruiker
10-12-2022 15:58
gewijzigd op 10-12-2022 16:18
Het is een inkopper, maar het hele idee van object georiënteerd programmeren is dat je van objecten classes maakt.
Wat ik nu zie is dat Cart niet zozeer een winkelwagen is, maar de rekening met alle specificaties, ofwel alle gegevens worden op 1 hoop gegooid. Het zou mooier zijn wanneer Cart alleen producten bevat. Een product verdient dus een eigen Product class:
<?php
class Product
{
/** @var string productnaam */
protected string $name;
/** @var int prijs in eurocent */
protected int $price;
/**
* ctor
* @param string $name Productnaam
* @param int $price Prijs in eurocent
* @return void (implicit)
*/
function __construct(string $name, int $price)
{
$this->name = $name;
$this->price = $price;
}
/**
* getter
* @return Productnaam
*/
function name() : string
{
return $this->name;
}
/**
* getter
* @return Productprijs in eurocent
*/
function price() : int
{
return $this->price;
}
}
class ProductBuilder
{
static function build_product_by_id(int $id) : Product
{
$sql = 'SELECT name, price FROM product WHERE id = $1';
// $data = $db->query_params(sql, $id);
$data = [['name' => 'Voorbeeld', 'price' => 250]];
if (count($data) == 0) { trigger_error('Product not found'); }
$row = $data[0];
return new Product($row['name'], $row['price']);
}
}
class Cart
{
/** @var array $products Producten */
protected $products;
/**
* ctor
* @return void (implicit)
*/
function __construct()
{
$this->products = [];
}
/**
* Voeg product toe aan winkelwagen
*/
function add_product(Product $product)
{
$this->products[] = $product;
}
/**
* Voeg product toe aan winkelwagen via product ID
*/
function add_product_by_id(int $id)
{
$this->products[] = ProductBuilder::build_product_by_id($id);
}
/**
* Bereken totale prijs in eurocent
*/
function total_price() : int
{
/** @var int $total Totaalbedrag in eurocent */
$total = 0;
foreach ($this->products as $product) {
$total += $product->price();
}
return $total;
}
}
$cart = new Cart;
$cart->add_product_by_id(1);
$cart->add_product(new Product('Nog een voorbeeld', 300));
print 'Totaalbedrag: ' . $cart->total_price();
?>
In deze trend is de 'signup costs' ook een product, die zou je standaard zo kunnen meenemen:
Belangrijk is om concepten uit elkaar te trekken. Ik weet niet precies hoe je API er uit ziet, maar normaal gesproken wordt onder een API verstaan een generieke manier om code aan te roepen, ongeacht hoe het is geïmplementeerd. Dan kom je op het gebied van interfaces. Met een kleine omweg kan je via JavaScript ook PHP aanroepen. In dat geval kan je vanuit die 'omweg' bovenstaande classes gebruiken.
Wil je Products nog verder uit elkaar trekken, omdat memberships, addons en signup costs verschillende dingen zijn, maar toch allemaal producten, dan kan je dat verder specificeren via inheritance. Maak de class Product eventueel een abstracte class, en maak voor elk soort product een eigen class, die bouwt op de Product class ('extenden'), met daarin de specifieke eigenschappen (methoden en variabelen). Hierover is op PHPHulp.nl een tutorial verschenen.
Ik ben eerder ook in de dependency hell getrapt. Men en iedereen heeft het erover hoe je wel MVC zou moeten implementeren. Maar inmiddels weet ik beter, door schade en schande.
PHP zit in zijn geheel gewoon te losjes in elkaar waardoor het vrijwel ondoenlijk is om er echt goed in te programmeren. Dat ze in PHP 8 nog de booleaanse logica fundamenteel moesten herzien is mijn inziens een grote smet.
Ondertussen krijg je aan alle kanten verkeerd advies over raamwerken. Mijn ogen werden pas geopend na het lezen van dit artikel. MVC blijkt nooit bedoeld voor de situatie van PHP! Er blijkt ook uit dat het compleet onzinnig is om voor een enkele thread een database object te maken, en daarvan de pointer mee te geven aan elke class om zo in de dependency hell te belanden. Sommigen zoeken dan hun toevlucht tot de verguisde singleton, maar dat is vaak wel een praktische oplossing.
Nog praktischer zou zijn om het hele OOP-geneuzel in PHP met een enorme korrel zout te nemen. Zo is het nog beter om gewoon een globale static class te maken voor de DB, beter dan dat wordt het toch niet voor de klassieke situatie van een eigen thread per HTTP-request.
En laten we eerlijk zijn, zolang je in PHP dit kan doen zonder dat je een error krijgt:
<?php
if ('Test') { $t = 'Test'; }
var_dump($t);
?>
is het dweilen met de kraan open. Een string kan zomaar gecast worden naar een bool, zonder dat duidelijk is waarvoor. Vervolgens heeft $t gewoon de waarde NULL, zoals elke niet bestaande variabele dat standaard heeft.
Nog los van de waslijst van dingen en dingetjes die aan PHP zijn op te merken (wat is er wel perfect?) merk ik dat het programmeren in een georganiseerde taal als Rust wel een stuk makkelijker gaat. Ja je moet alles uitspellen, maar daarmee betrap je jezelf eerder op foutjes, en er is meer controle en ondersteuning vanuit de ontwikkeltools. Programma's bevatten minder bugs.
Het gaat ook beter omdat OOP ook niet zaligmakend is, het is niet langer het antwoord op alles. Sterker, inheritance wordt niet eens ondersteund. In plaats daarvan ontwerp je code op basis van gedrag, waardoor je allerlei OOP-problemen voor bent.
Het is een inkopper, maar het hele idee van object georiënteerd programmeren is dat je van objecten classes maakt.
Wat ik nu zie is dat Cart niet zozeer een winkelwagen is, maar de rekening met alle specificaties, ofwel alle gegevens worden op 1 hoop gegooid. Het zou mooier zijn wanneer Cart alleen producten bevat. Een product verdient dus een eigen Product class:
<?php
class Product
{
/** @var string productnaam */
protected string $name;
/** @var int prijs in eurocent */
protected int $price;
/**
* ctor
* @param string $name Productnaam
* @param int $price Prijs in eurocent
* @return void (implicit)
*/
function __construct(string $name, int $price)
{
$this->name = $name;
$this->price = $price;
}
/**
* getter
* @return Productnaam
*/
function name() : string
{
return $this->name;
}
/**
* getter
* @return Productprijs in eurocent
*/
function price() : int
{
return $this->price;
}
}
class ProductBuilder
{
static function build_product_by_id(int $id) : Product
{
$sql = 'SELECT name, price FROM product WHERE id = $1';
// $data = $db->query_params(sql, $id);
$data = [['name' => 'Voorbeeld', 'price' => 250]];
if (count($data) == 0) { trigger_error('Product not found'); }
$row = $data[0];
return new Product($row['name'], $row['price']);
}
}
class Cart
{
/** @var array $products Producten */
protected $products;
/**
* ctor
* @return void (implicit)
*/
function __construct()
{
$this->products = [];
}
/**
* Voeg product toe aan winkelwagen
*/
function add_product(Product $product)
{
$this->products[] = $product;
}
/**
* Voeg product toe aan winkelwagen via product ID
*/
function add_product_by_id(int $id)
{
$this->products[] = ProductBuilder::build_product_by_id($id);
}
/**
* Bereken totale prijs in eurocent
*/
function total_price() : int
{
/** @var int $total Totaalbedrag in eurocent */
$total = 0;
foreach ($this->products as $product) {
$total += $product->price();
}
return $total;
}
}
$cart = new Cart;
$cart->add_product_by_id(1);
$cart->add_product(new Product('Nog een voorbeeld', 300));
print 'Totaalbedrag: ' . $cart->total_price();
?>
In deze trend is de 'signup costs' ook een product, die zou je standaard zo kunnen meenemen:
Belangrijk is om concepten uit elkaar te trekken. Ik weet niet precies hoe je API er uit ziet, maar normaal gesproken wordt onder een API verstaan een generieke manier om code aan te roepen, ongeacht hoe het is geïmplementeerd. Dan kom je op het gebied van interfaces. Met een kleine omweg kan je via JavaScript ook PHP aanroepen. In dat geval kan je vanuit die 'omweg' bovenstaande classes gebruiken.
Wil je Products nog verder uit elkaar trekken, omdat memberships, addons en signup costs verschillende dingen zijn, maar toch allemaal producten, dan kan je dat verder specificeren via inheritance. Maak de class Product eventueel een abstracte class, en maak voor elk soort product een eigen class, die bouwt op de Product class ('extenden'), met daarin de specifieke eigenschappen (methoden en variabelen). Hierover is op PHPHulp.nl een tutorial verschenen.
Thanks dit is precies wat ik zocht. Dit scheidt erg goed de functionaliteit. Het enige wat in mijn geval anders is, is dat ik geen database gebruik, maar een api, maar dan kan ik die sql query gemakkelijk vervangen door een soortgelijke api call. Is het overigens ook handig om per product property een class te maken, of is dat iets te veel van het goede? Vind het zelf niet nodig, maar hoorde dat van iemand.