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.

Heeft iemand ideeen, hoe ik dit netjes kan doen?
Jorn Reed op 23/12/2022 14:49:00

Zijn enums handig om grote if statements en switches te ontwijken?

Nee.

Om te beginnen zijn backed enums enorm beperkt. Ze kunnen zelfs niet alle scalars aan, alleen een int of string. Probeer je dat toch, dan krijg je deze melding:
Fatal error: Enum backing type must be int or string


Een manier om grote if-statements te verhelpen is door te werken met interfaces en functie-pointers. In plaats van dit:


switch $db_type {
  case 'a': query()...
  case 'b': query()...
  case 'c': query()...
  ...
}


Kan je dit doen:

interface db_type {
  function query($sql) : dataset;
}
class A implements db_type { ... }
class B implements db_type { ... }
class C implements db_type { ... }
$db = new B;
...
$data = $db->query($sql);  // geen switch nodig want class implements interface
@Ad Fundum

Ik denk dat hij meer bedoelt als je een lijst van opties hebt waar iets mee moet gebeuren. Ik vermoed dat hij op basis van de error-code een tekst wil tonen.

In plaats van dat via een switch te doen, zou je de waardes in een array kunnen zetten. Maar wellicht is het handiger als Jorn even het betreffende stukje code laat zien, dan weten we waar we over praten.
Volgens mij kun je inderdaad wel zeggen dat het aantal controles van condities met controlestructuren zoals if en switch afneemt wanneer je die condities formaliseert in abstracties. Klinkt misschien erg theoretisch, maar dat valt bij nader inzien wel mee als we in de praktijk duiken.

Terug naar een eerder voorbeeld. Stel, we willen het geslacht van een persoon vastleggen. Daar blijkt een norm voor te bestaan: ISO/IEC 5218 Information technology - Codes for the representation of human sexes.

We hebben alleen een tiny int nodig:


<?php

declare(strict_types=1);

class Person
{
    public int $gender = 0;
}

?>


Prachtig! We hebben een compacte class die zo min mogelijk geheugen gebruikt, zonder naïeve getters en setters. En de marketingafdeling kan trots zeggen dat onze software aan internationale standaardaarden voldoet.

Maar is deze code wel duidelijk? Kan een collega die deze class opent zien wat hij of zij met $gender kan doen? Snapt iedereen wel wat die 0 is?

Misschien moeten we onze ontwerpbeslissingen documenteren:


<?php

declare(strict_types=1);

/**
 * @sse https://www.iso.org/standard/36266.html ISO/IEC 5218:2004
 * @see https://en.wikipedia.org/wiki/ISO/IEC_5218 ISO/IEC 5218
 */
class Person
{
    /**
     * @var int $gender
     *   ISO/IEC 5218 compliant gender as an integer. MUST be 0 (Not known), 
     *   1 (Male), 2 (Female), or 9 (Not applicable). Defaults to 0.
     */
    public int $gender = 0;
}

?>


Beter, maar we kunnen met documentatie alleen niet voorkomen dat de class verkeerd gebruikt wordt. De oude standaardoplossing in PHP is dan de controle over een property delegeren aan een setter.

In modern PHP zie de oude oplossing er bijvoorbeeld zo uit:


<?php

declare(strict_types=1);

/**
 * @see https://www.iso.org/standard/36266.html ISO/IEC 5218:2004
 * @see https://en.wikipedia.org/wiki/ISO/IEC_5218 ISO/IEC 5218
 */
class Person
{
    /**
     * @var int $gender
     *   ISO/IEC 5218 compliant gender as an integer. MUST be 0 (Not known), 
     *   1 (Male), 2 (Female), or 9 (Not applicable). Defaults to 0.
     */
    protected int $gender = 0;
    
    public function setGender(int $gender): void
    {
        if ($gender === 0 || $gender === 1 || $gender === 2 || $gender === 9) {
            $this->gender = $gender;
        } else {
            throw new \ValueError('Invalid gender');
        }
    }
    
    public function getGender(): int
    {
        return $this->gender;
    }
}

?>


Deze PHP-code lijkt op het eerste gezicht acceptabel. Zo ziet PHP er wel vaker uit, dus je leest er gedachtenloos overheen. En de code werkt voorlopig goed, dus vooral niet aankomen. If it ain't broke, don't fix it!

De code heeft echter een aantal problematische afhankelijkheden (dependencies):

1. De code is afhankelijk van externe standaarden. Hier is het weliswaar een open standaard en een internationaal erkende standaard, maar dat is en blijft een externe afhankelijkheid die door anderen wordt bepaald. Als de standaard verandert, hebben we een probleem.

2. We hebben de afhankelijkheid deels moeten uitschrijven in documentatie, anders is het ontwerp niet duidelijk. Zo ontstaat een tweede afhankelijkheid: een goed begrip en correct gebruik van de code is afhankelijk van de documentatie. En we weten allemaal dat niemand documentatie goed of helemaal leest.

3. Om verkeerd gebruik van de property $gender tegen te gaan, hebben we een setter moeten toevoegen. En omdat de property niet meer public is ook nog een getter. Dat maakt de code ongeveer twee keer zo langzaam: test het maar. En het maakt onderhoud van de code duurder: je hebt niet langer slechts één property, maar ook nog twee methoden. We kunnen dit eventueel oplossen door een factory of builder verantwoordelijk te maken voor het setten ven properties, maar dat introduceert dan weer een andere afhankelijkheid.

4. We kunnen bij wijzigingen niet slechts de code aanpassen, maar moeten ook altijd de documentatie bijwerken. De ervaring leert dat dat laatste vaak niet gebeurt. En daarmee krijgen we een ongewenst maar bekend fenomeen: Code comments are useful lies.

Afhankelijkheden maken code slecht leesbaar, traag, foutgevoelig, moeilijk te veranderen, duur in onderhoud, enzovoort.

We proberen afhankelijkheden daarom zoveel mogelijk te beperken, te internaliseren en expliciet te maken. Dat kan, bijvoorbeeld, met een enum:


<?php

declare(strict_types=1);

enum Gender: int
{
    case NotKnown      = 0;
    case Male          = 1;
    case Female        = 2;
    case NotApplicable = 9;
}

class Person
{
    public Gender $gender = Gender::NotKnown;
}

?>


Niet zaligmakend, wel véél beter.
@Ward

Zit me even af te vragen ... de cases hebben nu een int als waarde. Ik neem aan dat dat enkel zo is om de waardes in een database op te slaan, toch?

Ervan uitgaande dat bovenstaande beredenering klopt, dan haal ik in het geval van een man dus de int 1 op. Hoe zou ik die dan moeten setten? De class property accepteert enkel Gender. Hoe vertaal ik die 1 weer naar Male?

PS 1 NotKnown -> Unknown

PS 2 Fijne dagen!
Ozzie PHP op 23/12/2022 14:03:53

@Ad Fundum

Klinkt interessant! Ik ken dat enum helemaal (nog) niet.
Wat is precies de toepassing van zoiets?

Stel we hebben een object 'deur'. En die deur kan een 'status' hebben, namelijk 'open' of 'dicht'.
Als je dan zou controleren op status, dan zou je kunnen doen:

<?php

if ($deur->status() == 'open') {
// dat doet de deur dicht
}

?>
Er zijn maar 2 juiste statussen, namelijk 'open' of 'dicht'. Nu zou ik me kunnen voorstellen dat een programmeur die de code niet kent de verkeerde status (een verkeerd woord) gebruikt, bijv. geopend in plaats van open. Of gesloten in plaats van dicht.

Kun je dit met zo'n enum ondervangen? En zo ja, hoe ziet de code van die enum er dan uit?

Voor twee statussen kan je beter een boolean gebruiken, bijvoorbeeld $isOpen = false | true; // let zelf op: kan NULL zijn..

De meerwaarde is een consistente manier om met (meer dan 2) verschillende situaties om te gaan. In PHP is dat dus beperkt tot een int en string. Andere scalars als NULL, bool, float werken niet. Het grootste gemis is dat je een object kan gebruiken in een enum.
Met alleen een string of int (of 'pure', niet 'backed') voegt een enum niet zoveel toe aan PHP. Je kan er een status mee vasthouden, via een soort gecontroleerde (trefwoorden)lijst. Dat kan je ook anders doen, zoals Ward doet met eigen setters, dat geeft al aan dat de enum eigenlijk niets toe voegt. Het gedraagt zich als een reguliere class zonder inheritance volgens de documentatie. In het geval van int backed enums kan je ook af met een eigen lijstje consts.
Ozzie PHP op 25/12/2022 18:06:03

Zit me even af te vragen ... de cases hebben nu een int als waarde. Ik neem aan dat dat enkel zo is om de waardes in een database op te slaan, toch?

Na wat refactoring hebben we een ISO-norm toegepast die het geslacht uitdrukt in kleine integers: die lenen zich inderdaad bij uitstek voor een TINYINT in een database.

Een backed enumeration lijkt op een key/value store en daarmee op een databasetabel. Elke case (ook wel member genoemd) heeft een name en een value:


<?php

declare(strict_types=1);

enum Gender: int
{
    case NotKnown      = 0;
    case Male          = 1;
    case Female        = 2;
    case NotApplicable = 9;
}

class Person
{
    public Gender $gender = Gender::NotKnown;
}


$person = new Person();

//> enum(Gender::NotKnown)
var_dump( $person->gender );

//> string(8) "NotKnown"
var_dump( $person->gender->name );

//> int(0)
var_dump( $person->gender->value );

?>


Ozzie PHP op 25/12/2022 18:06:03
Ervan uitgaande dat bovenstaande beredenering klopt, dan haal ik in het geval van een man dus de int 1 op. Hoe zou ik die dan moeten setten? De class property accepteert enkel Gender. Hoe vertaal ik die 1 weer naar Male?


Een backed enumeration implementeert de interne PHP-interface BackedEnum. Die definieert onder andere de twee standaardmethoden from() en tryFrom(): daarmee kun je een integer uit de database weer omzetten in een case uit de enum.


<?php

// Rij uit database
$row = ['gender' => 2];

$person = new Person();
$person->gender = Gender::from($row['gender']);

//> enum(Gender::Female)
var_dump( $person->gender );

//> string(6) "Female"
var_dump( $person->gender->name );

//> int(2)
var_dump( $person->gender->value );

?>
Heldere uitleg Ward. Dankjewel!
Als de database geen TINYINT (8 bit) ondersteunt kan je ook een SMALLINT (16 bit) gebruiken.

Overigens moet je de PHP enum niet verwarren met een database ENUM . Een database ENUM nummert gewoon vanaf 0 (tenzij NULL), zoals een ENUM bedoeld is. PHP heeft met 'pure' enums uberhaupt geen waarde, en met 'backed' enums heb je alleen ints en strings, die ook totaal geen relatie hebben met een database ENUM.

Eigenlijk zet de benaming ENUM in PHP je op het verkeerde spoor (het zal ook eens niet..):
ENUM is afgeleid van enumerated type ofwel een opgesomd lijstje (numeriek). Maar zowel pure als backed enums hebben niets met numerieke opsomming te maken.

Dit om een Babylonische spraakverwarring over wat een ENUM is te voorkomen.
Dat stukje scholing ontbreekt bij mij. Voor mij zijn enums compleet nieuw. De toepassing lijkt me wel handig.

Reageren