Return iets of niks

Overzicht Reageren

Sponsored by: Vacatures door Monsterboard

Ozzie PHP

Ozzie PHP

14/12/2022 12:18:16
Quote Anchor link
Een method als deze kom je vaak tegen:

Code (php)
PHP script in nieuw venster Selecteer het PHP script
1
2
3
4
5
6
7
8
9
<?php

public function getFoo() {
  if ($some_condition === true) {
    return $this->foo;
  }
}


?>

Nu geeft deze functie bijv. altijd een array terug als de conditie waar is. Daarom geef ik een return type aan.

Code (php)
PHP script in nieuw venster Selecteer het PHP script
1
2
3
4
5
6
7
8
9
<?php

public function getFoo(): array {
  if ($some_condition === true) {
    return $this->foo;
  }
}


?>

Dat gaat goed zolang er aan de voorwaarde wordt voldaan en een array wordt teruggegeven.

Echter, het kan ook zijn dat er geen array wordt teruggegeven. Ik had verwacht dat er dan impliciet NULL werd teruggegeven. Dus dan zou je denken om het return type als volgt aan te passen:

Code (php)
PHP script in nieuw venster Selecteer het PHP script
1
2
3
4
5
6
7
8
9
<?php

public function getFoo(): ?array {
  if ($some_condition === true) {
    return $this->foo;
  }
}


?>

Maar helaas blijkt dit niet te werken. Omdat er niks wordt gereturned, leidt het tot een fatal error. Ik kan natuurlijk expliciet return null toevoegen, maar vroeg me af er wellicht een andere oplossing is.

En dan nog een vraag 2:

Als iets er niet is, denk aan bijvoorbeeld:

- je voert een SELECT query succesvol uit, maar deze levert geen records op
- je haalt een geb.datum op van een user, maar die blijkt leeg / niet te zijn ingevuld
- je haalt een winkelmand op, maar er zitten geen producten in (omdat niks besteld of sessie verlopen)

wat return je dan? Is het dan gebruikelijk om NULL te returnen (als in 'leeg') of false (als in 'mislukt').
 
PHP hulp

PHP hulp

27/12/2024 06:32:07
 
Ivo P

Ivo P

14/12/2022 15:45:49
Quote Anchor link
zoek eens op VOID
 
Ozzie PHP

Ozzie PHP

14/12/2022 16:29:07
Quote Anchor link
Thanks Ivo. Dat had ik al geprobeerd, maar dat werkt niet. Void kun je wel 'stand alone' gebruiken, maar niet in combinatie met iets anders. Enkel void werkt dus, maar array|void werkt niet.
 

14/12/2022 19:52:28
Quote Anchor link
Even los van de vraag dat je een apart return NULL; statement moet ingeven in de functie, is het veelvoorkomende design toe aan verbetering.

Het probleem er van is dat je bij dat soort functies elke keer iets als dit zou moeten doen:
Code (php)
PHP script in nieuw venster Selecteer het PHP script
1
2
3
4
5
<?php
$rc
= getFoo();
if $rc === NULL { /* actie 1 */ }
elseif is_array($rc) { /* actie 2 */ }
?>

Je maakt de functie hiermee verantwoordelijk voor 2 soorten acties, die wel soortgelijk zijn maar niet dezelfde.
Dat kan beter:
Code (php)
PHP script in nieuw venster Selecteer het PHP script
1
2
3
4
<?php
function getFooCondition() {};
function
getFoo() {};
?>

Een gedeelde implementatie kan je in een derde (protected) functie plaatsen.
Het voordeel is dat je vooraf de if-beslissing zelf neemt via code:
Code (php)
PHP script in nieuw venster Selecteer het PHP script
1
2
3
4
<?php
$array
= getFooCondition();
$nul = getFoo();
?>

Het maakt ook duidelijk wat de intentie is.
En het kost iets meer programmeerwerk om het uit elkaar te trekken, en bij gedeelde implementatie het kost een extra functie call per keer dat je de functie gebruikt. Aan de andere kant is deze code voor de CPU beter te optimaliseren, omdat je een if-constructie vermijdt. Al zal dat effect in de meeste gevallen niet direct merkbaar zijn.

Toevoeging op 14/12/2022 20:06:45:

Ozzie PHP op 14/12/2022 12:18:16:
En dan nog een vraag 2:

Als iets er niet is, denk aan bijvoorbeeld:

- je voert een SELECT query succesvol uit, maar deze levert geen records op
- je haalt een geb.datum op van een user, maar die blijkt leeg / niet te zijn ingevuld
- je haalt een winkelmand op, maar er zitten geen producten in (omdat niks besteld of sessie verlopen)

wat return je dan? Is het dan gebruikelijk om NULL te returnen (als in 'leeg') of false (als in 'mislukt').

Dat is een hele goede vraag. De implementatie van NULL wordt ook wel 'The Billion Dollar Mistake' genoemd. Dat ligt niet aan het concept dat een waarde kan missen, maar aan de implementatie van de programmeertaal (sorry als dit begint te klinken als een stokpaardje, ik kan het niet helpen). In Rust hebben ze een betere implementatie via de Option enum: een eigen datastructuur voor wanneer er wel en niet een waarde is, waardoor je zelf niet zonder meer een NULL-waarde in het geheugen hebt, waardoor je programma daar niet op kan crashen.

In reactie op de voorbeelden:
- query uitvoer
In mijn PHP implementatie geeft mijn zelfgemaakte query_params() functie altijd een array terug met daarin de rijen. Een lege array geeft aan dat er geen uitvoer is. Fouten worden gelogd via syslog. Sommige fouten laat ik automatisch terugkomen in de GUI, bijvoorbeeld als opslaan niet lukt door een beperking, dan wordt het commentaar van de beperking als foutmelding gebruikt. NULL gebruik ik niet.

- geboortedatum
Een string is een string, lege strings betekent geen geboortedatum. Wederom geen NULL-waarden.

- winkelmand met producten
Zelfde als met queries: altijd een array teruggeven, desnoods leeg. Zie ook mijn reactie in deze post.
Gewijzigd op 14/12/2022 20:09:51 door
 
Ozzie PHP

Ozzie PHP

14/12/2022 21:01:34
Quote Anchor link
@Ad Fundum

Thanks voor het meedenken. Het returnen van een lege array zou inderdaad een optie zijn. Dan return ik toch nog een array. Helemaal zo verkeerd nog niet. De code wordt alleen iets langer. Bijv. in het voorbeeld van ophalen van een array uit een database:

Code (php)
PHP script in nieuw venster Selecteer het PHP script
1
2
3
4
5
6
7
8
9
10
11
<?php

public function getRows(): array {
  $result = [];
  if ("er is data gevonden") {
    $result = $this->db->getRows();
  }

  return $result;
}


?>

Dat zou kunnen. Of nog korter:

Code (php)
PHP script in nieuw venster Selecteer het PHP script
1
2
3
4
5
<?php

return "er is data gevonden" ? $this->db->getRows() : [];

?>
 
Ward van der Put
Moderator

Ward van der Put

14/12/2022 22:12:45
Quote Anchor link
Er bestaat gewoon een standaard voor wat je probeert te bereiken...
 
Ozzie PHP

Ozzie PHP

14/12/2022 22:45:08
Quote Anchor link
>> Er bestaat gewoon een standaard voor wat je probeert te bereiken...

Oké ... en dat is?
 

16/12/2022 09:17:13
Quote Anchor link
Ik ben ook wel benieuwd.
Al verwacht ik er geen wonderen van, een kenmerk van standaarden is dat iedereen z'n eigen standaard heeft.
 
Ward van der Put
Moderator

Ward van der Put

16/12/2022 15:25:56
Quote Anchor link
Afgaande op de opvallende syntaxis gebruikt Ozzie WordPress en in WordPress is array|null of ?array gebruikelijk. Nou is dat wat PHP betreft niet per se een standaard die navolging verdient (ook vanwege de afwijkende syntaxis), maar wel een standaard om rekening mee te houden wanneer je toch in die context aan het werk bent.

Vanwege de eenvoudige conditie zou ik het dan zo schrijven:

Code (php)
PHP script in nieuw venster Selecteer het PHP script
1
2
3
4
5
6
7
8
<?php

public function getFoo(): ?array
{
    return ($some_condition === true) ? $this->foo : null;
}


?>


Hoewel je naïeve getters en setters beter niet kunt gebruiken, gedragen getters die optioneel null retourneren bij het ontbreken van een waarde zich zo meer als properties. Je kunt properties daardoor makkelijker vervangen door getters en vice versa, omdat hun gedrag vergelijkbaar is.

Code (php)
PHP script in nieuw venster Selecteer het PHP script
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php

declare(strict_types=1);

class Person
{
    public ?string $name = null;

    public function getName(): ?string
    {
        return $this->name;
    }
}


?>


Interessanter wordt het wanneer de geretourneerde null wordt veroorzaakt door een fout of onverwachte systeemtoestand. Databasefuncties van PHP retourneren dan doorgaans false in plaats van null. Die syntaxis mag je in PHP 8.2 ook gebruiken:

Code (php)
PHP script in nieuw venster Selecteer het PHP script
1
2
3
4
5
6
7
8
<?php

public function getFoo(): array|false
{
    return ($some_condition === true) ? $this->foo : false;
}


?>


Als je wilt weten wát er fout gaat of waarom de getter geen resultaat retourneert, heb je niet voldoende aan false alleen. En dat brengt ons bij een andere standaard: de PSR-11 ContainerInterface. In plaats van null of een lege array gebruikt deze standaard bijvoorbeeld een NotFoundException of, strikt genomen, een exception die de NotFoundExceptionInterface implementeert.
 
Ozzie PHP

Ozzie PHP

16/12/2022 17:22:09
Quote Anchor link
>> Afgaande op de opvallende syntaxis gebruikt Ozzie WordPress

Nee, dat is een onjuiste aanname. Het was gewoon een algemene vraag over return types in combinatie met null.

Je geeft bij een method een return type aan waarbij je aangeeft dat het return type ook null kan zijn. Ik heb altijd begrepen dat een functie die niks return impliciet null returnt. Mijn gedachte was, ik haal gegevens op, ik doe een controle of er iets is opgehaald. Zo ja, dan return ik het. Zo nee, dan gebeurt er niks. Als er dan geen gegevens zijn gevonden, dan wordt er niks gereturned en wordt er dus impliciet null gereturned. Ik had dus verwacht dat ik dat met een return type ?array zou kunnen bewerkstelligen. Echter werkt dat niet, omdat je dus blijkbaar expliciet null moet returnen. Dat is ook wat ik terugzie in jouw verkorte (ternary) return.

Jouw tweede voorbeeld is geinig met die parameter null initialisatie. Die zal ik onthouden.

De meest interessante vraag (voor mij) is nog steeds, wat return je als iets er niet is / leeg is.

Ik denk dat we het er wel over eens zijn dat false duidt op een mislukte actie of het aangeven van een toestand (niet waar). Die doet wat mij betreft in deze discussie dus niet mee.

Mijn vraag gaat expliciet over wat je doet als je iets ophaalt dat geen resultaat teruggeeft omdat de waarde(s) leeg zijn. Er gaat in dit geval dus niks fout, maar bijv. je SELECT query levert geen resultaat op, de geb.datum die je ophaalt is niet ingevuld door de gebruiker of de class property die je ophaalt bevat een lege string.

Dus eigenlijk situaties als deze (pseudo-code enkel voor het idee):

Code (php)
PHP script in nieuw venster Selecteer het PHP script
1
2
3
4
5
6
7
8
9
<?php

$records
= $db->get_records($sql); // geen rijen gevonden

$day_of_birth = $user->get_day_of_birth(); // geen geb.datum ingevuld

$last_name = $this->last_name; // achternaam niet ingevuld

?>

MIJN VRAAG :

Bij het ophalen van database records verwacht je bijv. een array. Echter, er worden geen resultaten gevonden. Return je een lege array of return je null?

Je kunt 2 kanten op. Zoals Ad Fundum zegt, kun je een lege array returnen. Wat mij betreft een mooie en prima oplossing. Je kunt een controle doen met empty() om te zien of er iets in de array zit en vervolgens wel of geen verdere actie ondernemen. Je geeft ook altijd hetzelfde type (in dit voorbeeld een array) terug, en ook dat is netjes en zorgt voor een voorspelbaar resultaat.

Tegelijkertijd kun je ook null returnen. Daarmee geef je "leeg" aan. Dat wat je wilde hebben is er niet. Het resultaat is leeg / bestaat niet ... is null. Ook dat is goed te verdedigen. Controle kan plaatsvinden met is_null.

Het voordeel van de eerste methode is dat je altijd eenzelfde datatype terugkrijgt. Het nadeel van de tweede methode is dat je niet weet welk datatype je terugkrijgt. Of een array of null. Dat laatste kan je ook van de andere kant bekijken. Je kunt dan altijd en overal controleren met is_null en je hoeft niet te controleren of het wel of niet een lege array is, of bijv. een lege string of een int. Je kunt dan gewoon altijd is_null gebruiken. Is het er niet? Dan is het altijd null.

Belangrijk lijkt het me om in ieder geval consistent te zijn, maar als je deze twee opties bekijkt, heeft een van beide dan de voorkeur en zo ja waarom? Kies je ervoor om een lege waarde van hetzelfde datatype te returnen, of return je in alle gevallen null?
Gewijzigd op 16/12/2022 17:23:55 door Ozzie PHP
 

16/12/2022 20:15:53
Quote Anchor link
@Ward: dank voor de bijdrage en de link naar PSR-11. Ik heb grote aversie tegen de PHP PSR's, deze in het bijzonder vanwege de vermeende standaardisatie van de dependency hell waar we het eerder over hadden. En ik heb nog nooit eerder het nut van Exceptions gezien. In mijn ogen is het laten falen van code een keuze, door wel of niet return codes te behandelen. Maar dat is een bijna religieuze discussie die afleidt van de vraag van Ozzie, dus ik laat het daar bij.

@Ozzie:
Returnwaarden van functies zijn in PHP ontzettend vaag helaas. Zo lees je in de documentatie van __construct() dat het 'void' returnt. Als je dat in PHP code stopt krijg je een fatal (!) error: "Fatal error: Method c::__construct() cannot declare a return type". Geen idee waarom, want bij andere magic functions kan het weer wel:
Code (php)
PHP script in nieuw venster Selecteer het PHP script
1
2
3
4
5
6
7
<?php
class c implements Serializable
{
  function
__unserialize(array $data): void { /* ... */ }
  ...
}

?>

De reden dat ik het aanhaal is dat PHP zelf ook nog een weg te gaan heeft. Zo kan je niet een functie maken die 'niets' returnt, een return type als array|void kan gewoon niet: "Fatal error: Void can only be used as a standalone type". Je moet dus wel iets anders returnen. Kennelijk maakt PHP voor het gemak (not) wel weer onderscheid tussen niets (void) en NULL. Handig? Nee. Je blijft dit soort verrassingen tegenkomen, zelfs met de nieuwe return type 'never'. Een functie kan niet returnen (ofzo), maar wel Exceptions opgooien. Logisch? Ik vind van niet.

PHP is een van-alles-aan-elkaar-plak-taal, in die context is er niets mis met het checken met is_null() of er iets uit een functie komt. Maar die check alleen zegt niets over het resultaat, omdat er.. niets is. Je kunt zo niet weten of het de bedoeling was dat er niets uit kwam. Bijvoorbeeld: stuur een keep-alive signaal naar socket X en het maakt voor je programma niet uit of dat is gelukt, maar je wilt de respons wel loggen. Je checkt met is_null() voor het loggen, maar eigenlijk kan je makkelijker de logging class meegeven aan de functie, of als globale static class beschikbaar maken.

Gezien dat NULL echt alles kan betekenen in PHP (behalve niets) vind ik het geen goede vervanger voor een missende waarde. Je zou zelf a la Rust een Option class kunnen maken. Maar mijn oplossing is deze:
Bedenk van te voren welke situaties er kunnen ontstaan, en hoe die afgehandeld moeten worden. Laat dat zoveel mogelijk aan de functie over, die belast is met de taak. Bijvoorbeeld voor de database:
Code (php)
PHP script in nieuw venster Selecteer het PHP script
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
class db
{
  protected $link, $logger, $messager;
  function
__construct($link, $logger, $messager) { ... }
  function
query($sql, $params) : array
  {
    $data = $this->link->query_params($sql, $params);
    if $this->link->last_errno > 0 {
      if constraint_error {
        $messager->write($this->get_constraint_error());
      }
else {
        $logger->write($this->link->last_error);
      }

      return [];
    }

    return $data;
  }
}

?>

Op deze manier hoef je jezelf na het aanroepen van $db->query() niet meer druk te maken over eventuele fouten, alleen over de rijen data die er al dan niet uit komen.
Gewijzigd op 16/12/2022 20:30:51 door
 
Ozzie PHP

Ozzie PHP

16/12/2022 21:36:39
Quote Anchor link
Thanks Ad Fundum.

Ik denk dat het inderdaad wel een goede gewoonte is om een leeg identiek datatype te returnen.

Ik heb geen IT-opleiding gevolgd, maar heb van geschoolde programmeurs wel eens vernomen dat het niet 'mooi' is om op meerdere plekken binnen een functie iets te returnen. Daar zit wel wat in denk ik. Ik zou jouw oplossing dan ook eerder zo doen. Kleine nuance.

Code (php)
PHP script in nieuw venster Selecteer het PHP script
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php

  function query($sql, $params) : array
  {
    $data = $this->link->query_params($sql, $params);
    if $this->link->last_errno > 0 {
      if constraint_error {
        $messager->write($this->get_constraint_error());
      }
else {
        $logger->write($this->link->last_error);
      }

      $data = [];
    }

    return $data;
  }


?>

Nu return je maar 1x en op 1 plek. Persoonlijk vind ik dat prettiger.

Ik ben wel benieuwd. Jij schrijft:

"Op deze manier hoef je jezelf na het aanroepen van $db->query() niet meer druk te maken over eventuele fouten, alleen over de rijen data die er al dan niet uit komen."

Je krijgt dan inderdaad altijd een array terug. Ik kan me zo voorstellen dat je daar direct na ontvangst iets mee gaat doen. Waarschijnlijk gooi je de array door een foreach-loop.

Check je dan van tevoren of er iets in de array zit?

Code (php)
PHP script in nieuw venster Selecteer het PHP script
1
2
3
4
5
6
7
<?php

if (!empty($array)) {

foreach(....

?>

Of laat je die empty() check achterwege en gooi je de lege array direct in de foreach loop?
Gewijzigd op 16/12/2022 21:38:29 door Ozzie PHP
 

17/12/2022 09:07:31
Quote Anchor link
Ozzie PHP op 16/12/2022 21:36:39:
Ik heb geen IT-opleiding gevolgd, maar heb van geschoolde programmeurs wel eens vernomen dat het niet 'mooi' is om op meerdere plekken binnen een functie iets te returnen. Daar zit wel wat in denk ik.

Het volgen van een opleiding (of zelfs maar een basisschool) geeft geen garanties. Het belangrijkste is om er zelf over na te blijven denken en open te staan voor onderbouwde nieuwe inzichten. Vaak is het juist om een voordeel om geen IT-opleiding gehad te hebben, omdat je dan beter 'out-of-the-box' kunt redeneren.

Of je nou op 1 plek iets returnt of niet is een kwestie van voorkeur. Het return-statement bestaat niet voor niets, het kan een paar grote if-blokken schelen en de code overzichtelijker maken. Maar in weinig code maakt het weinig verschil.

Ozzie PHP op 16/12/2022 21:36:39:
Check je dan van tevoren of er iets in de array zit? [..]
Of laat je die empty() check achterwege en gooi je de lege array direct in de foreach loop?

Wat is het nut om van te voren te checken of een array leeg is? Als er geen data in een array zit wordt een foreach-lus niet uitgevoerd.
 
Ward van der Put
Moderator

Ward van der Put

17/12/2022 09:35:34
Quote Anchor link
Ozzie PHP op 16/12/2022 17:22:09:
Je kunt 2 kanten op. Zoals Ad Fundum zegt, kun je een lege array returnen. Wat mij betreft een mooie en prima oplossing. Je kunt een controle doen met empty() om te zien of er iets in de array zit en vervolgens wel of geen verdere actie ondernemen. Je geeft ook altijd hetzelfde type (in dit voorbeeld een array) terug, en ook dat is netjes en zorgt voor een voorspelbaar resultaat.

Ad zegt ook dat hij hetzelfde doet met strings. Als er geen resultaat is, wordt er een lege string geretourneerd. Naar het eerste voorbeeld:
Code (php)
PHP script in nieuw venster Selecteer het PHP script
1
2
3
4
5
6
7
8
9
10
11
12
<?php

public function getFoo(): string
{
    if ($some_condition === true) {
        return $this->foo;
    }
else {
        return '';
    }
}


?>

Dat doet hij bijvoorbeeld bij een geboortedatum:
Ad Fundum op 14/12/2022 19:52:28:
- geboortedatum
Een string is een string, lege strings betekent geen geboortedatum. Wederom geen NULL-waarden.

Laten we dat eens uitwerken, inclusief low-budget unittest. ;)
Code (php)
PHP script in nieuw venster Selecteer het PHP script
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<?php

declare(strict_types=1);

class Person
{
    public \DateTimeImmutable $dateOfBirth;
    
    public function getDateOfBirth(): string
    {
        if (isset($this->dateOfBirth)) {
            return $this->dateOfBirth->format('Y-m-d');
        }
else {
            return '';
        }
    }
}



//> string(0) ""
$person = new Person();
var_dump($person->getDateOfBirth());

//> string(10) "1980-04-01"
$person->dateOfBirth = new \DateTimeImmutable('1 april 1980');
var_dump($person->getDateOfBirth());

?>

Dat werkt zoals verwacht: als er geen geboortedatum is, komt er een lege string uit. Het lijkt bovendien consistent, want een lege array en een lege string zijn beide empty().

Maar nu introduceren we twee complicaties, die tonen waarom en wanneer deze systematiek in duigen valt:

(a) Stel, we moeten een getAge() toevoegen die de leeftijd in jaren als integer retourneert. Wat retourneer je dan als leeftijd onbekend is? Een lege integer? Of 0 want empty(0) is true?

(b) Stel, we willen getDateOfBirth() herschrijven zodat de methode geen string met de geboortedatum meer retourneert, maar een value object, bijvoorbeeld een DateTime- of DateTimeImmutable-object. Wat retourneer je dan bij een onbekende geboortedatum? Een leeg object?

Met een null in de returns kunnen we het geheel leesbaar, consistent én voorspelbaar houden. En ja, dat is slechts een compromis, maar wel een heel gebruikelijk compromis.

(Terzijde: ik begrijp heel goed waarom Ad een lege string gebruikt voor een ontbrekende geboortedatum. In een grootschalige database wil je namelijk zo min mogelijk nullable kolommen hebben. Bovendien is er een groot verschil tussen "we weten de waarde niet" en "we weten dat de waarde er niet is". Ik wil dat echter lager in het systeem dicht bij de datalaag houden, bijvoorbeeld in een mapper of data access object, en liever niet meeslepen als lege strings in hogere-orde classes in een applicatielaag.)

Ozzie PHP op 16/12/2022 21:36:39:
Ik heb geen IT-opleiding gevolgd, maar heb van geschoolde programmeurs wel eens vernomen dat het niet 'mooi' is om op meerdere plekken binnen een functie iets te returnen. Daar zit wel wat in denk ik.

Er zijn inderdaad programmeurs die dat doen, maar die schrijven vanwege die regel soms overbodig complexe én, niet onbelangrijker, inefficiënte code.

Neem dit voorbeeld uit OpenCart. Deze methode controleert of er producten met downloads zijn:
Code (php)
PHP script in nieuw venster Selecteer het PHP script
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php

public function hasDownload()
{

    $download = false;

    foreach ($this->getProducts() as $product) {
        if ($product['download']) {
            $download = true;
            break;
        }
    }


    return $download;
}


?>

Lelijk. Hier wordt de dubbele vuistregel gevolgd: er is maar één return en die return volgt aan het einde na alle operaties. Maar het gevolg daarvan is dat er nodeloos een hulpvariabele wordt geïntroduceerd. En dat er een break nodig is om uit een loop te ontsnappen.

Het kan ook gewoon zo:
Code (php)
PHP script in nieuw venster Selecteer het PHP script
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php

public function hasDownload(): bool
{
    foreach ($this->getProducts() as $product) {
        if ($product['download']) {
            return true;
        }
    }


    return false;
}


?>

Of als je niet vies bent van een eenregelige if zonder else:
Code (php)
PHP script in nieuw venster Selecteer het PHP script
1
2
3
4
5
6
7
8
9
10
11
<?php

public function hasDownload(): bool
{
    foreach ($this->getProducts() as $product) {
        if ($product['download']) return true;
    }

    return false;
}


?>


Twee returns, maar je kunt veel beter zien wat de methode doet.

En dat brengt ons via een omweg terug bij het begin van dit topic. De return types zijn onder andere ingevoerd omdat je altijd aan de signature van een methode kunt zien wat eruit komt. Je hoeft helemaal niet in de methode te zoeken naar de return. En dus is de regel dat die return altijd voorspelbaar aan het einde staat overbodig geworden.
 
Ozzie PHP

Ozzie PHP

17/12/2022 16:07:46
Quote Anchor link
Hmmm food for thought ... erg interessant.

@Ward

Dus om terug te komen op mijn vraag. Jij returnt dus altijd null als iets er niet is. Vat ik het daarmee correct samen?

Waar Ad Fundum dus een lege array returnt, zou jij gewoon null returnen. En ik neem aan dat je dan voorafgaand aan een foreach loop eerst checkt met is_null?
Gewijzigd op 17/12/2022 16:08:28 door Ozzie PHP
 
Ward van der Put
Moderator

Ward van der Put

17/12/2022 16:55:16
Quote Anchor link
Ik gebruik zelf inderdaad liever array|null of verkort ?array. Return types bieden je nu de kans om expliciet te maken wat voorheen verborgen bleef; benut die kans dan ook.

public function getCaptions(): array|null

zegt meer dan

public function getCaptions(): array

In het eerste geval wordt expliciet gemaakt dat de getter ook een lege waarde kan en mag retourneren. In het tweede geval moet je ernaar raden en het dus voor de zekerheid controleren of in de documentatie duiken (en hopen dat iemand de return later niet verandert).

Voor de vergelijking gebruik ik liever de strikte gelijkheid === null dan de functie is_null(). En dat meestal met een ontkenning: je hebt immers pas bij !== null een array teruggekregen als de return type array|null is.

Tot slot: het is geen goed/beter/best-keuze. Softwarearchitectuur lijkt veel op bouwkundige architectuur. Er kunnen bijvoorbeeld goede redenen zijn om de huiskamer op de eerste verdieping te plaatsen, want dan kun je een auto kwijt onderin je drive-in woning. Er is geen reden om zo'n ontwerp rigoureus af te keuren: het heeft in een bepaalde context absoluut bestaansrecht.
Gewijzigd op 17/12/2022 16:56:28 door Ward van der Put
 
Ozzie PHP

Ozzie PHP

17/12/2022 18:23:47
Quote Anchor link
Thanks Ward, duidelijk verhaal.

Zelf er ook nog over nadenkend heeft null ook wel iets moois in symantische zin.

Je haalt iets op, het is er niet en dus krijg je null: wat je hebt geprobeerd op te halen is er niet.

De lege array en het direct door een foreach loop gooien, heeft ook wel wat. Maar als ik praktisch denk (en dat zal voor iedere situatie verschillend zijn) dan haal ik bewust iets op om iets mee te doen.

Stel ik wil een dynamische lijst opbouwen, maar de gegevens ontbreken of de lijst is leeg, dan wil ik dat eigenlijk weten zodat ik de lay-out erop kan aanpassen. Zo maar een voorbeeldje. Dan is een controle dus altijd nuttig. Want een 'Top 10 best verkochte producten' met daaronder een lege lijst ziet er ook maar raar uit. Als er geen producten zijn, dan wil je die hele lijst + lay-out niet gaan opbouwen.
 

18/12/2022 13:59:38
Quote Anchor link
Ward van der Put op 17/12/2022 09:35:34:
Ad zegt ook dat hij hetzelfde doet met strings. Als er geen resultaat is, wordt er een lege string geretourneerd. [..]
Dat werkt zoals verwacht: als er geen geboortedatum is, komt er een lege string uit. Het lijkt bovendien consistent, want een lege array en een lege string zijn beide empty().

Maar nu introduceren we twee complicaties, die tonen waarom en wanneer deze systematiek in duigen valt:

(a) Stel, we moeten een getAge() toevoegen die de leeftijd in jaren als integer retourneert. Wat retourneer je dan als leeftijd onbekend is? Een lege integer? Of 0 want empty(0) is true?

(b) Stel, we willen getDateOfBirth() herschrijven zodat de methode geen string met de geboortedatum meer retourneert, maar een value object, bijvoorbeeld een DateTime- of DateTimeImmutable-object. Wat retourneer je dan bij een onbekende geboortedatum? Een leeg object?

Met een null in de returns kunnen we het geheel leesbaar, consistent én voorspelbaar houden. En ja, dat is slechts een compromis, maar wel een heel gebruikelijk compromis.

(Terzijde: ik begrijp heel goed waarom Ad een lege string gebruikt voor een ontbrekende geboortedatum. In een grootschalige database wil je namelijk zo min mogelijk nullable kolommen hebben. Bovendien is er een groot verschil tussen "we weten de waarde niet" en "we weten dat de waarde er niet is". Ik wil dat echter lager in het systeem dicht bij de datalaag houden, bijvoorbeeld in een mapper of data access object, en liever niet meeslepen als lege strings in hogere-orde classes in een applicatielaag.)

Leuk voorbeeld Ward, en de voorkeur om NULL te vermijden komt inderdaad bij databases vandaan.

Om op de voorbeelden in te gaan:
a.) getAge() kan nooit een tijdsperiode berekenen als 1 (of 2) van de 2 datums onbekend is. Een poging daartoe is een logische fout. Als je dan simpelweg een NULL returnt (of erger, een foute waarde 0), kom je weer op mijn eerste voorbeeld: je moet altijd checken of de returnwaarde NULL is of iets anders. En daar laat PHP het afweten, het is heel makkelijk om die logische fout te laten bestaan in een onverwachte NULL-waarde, want het is voor programmeurs te makkelijk om die NULL te negeren. De functie getAge() moet dus een error genereren. Tenzij programmeurs altijd op NULL checken wanneer dat nodig is.
En zo komen we weer op de implementatie van NULL in PHP. Je zou het kunnen proberen te verbeteren:
Code (php)
PHP script in nieuw venster Selecteer het PHP script
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
<?php
// Naieve methode
class Date1 {
  protected $date = NULL;
  function
__construct($date = NULL) { $this->date = $date; }
  function
get() : ?string { return $this->date; }
}

$d = new Date1;  // geen datum
$g = $d->get();  // hierna moet dan altijd een if volgen..
if (is_null($g)) { print 'NULL'; }  // wat doe je als er niets is om te printen?
else { print $g; }

// Verbeterde methode
class Date2 {
  protected $date = '';
  function
__construct(string $date = NULL) {  // ook zoiets, string = NULL..
    $this->date = $date;
  }
  function
get() : string {
    if (is_null($this->date)) {
      trigger_error('Kan geen lege datum returnen');
      return '';
    }

    return $this->date;
  }
}

$d = new Date2;  // geen datum
print $d->get();  // als er iets fout gaat merken we het wel

// Falen kan ook een keuze zijn
// Poging om een Option enum in PHP te maken:

class Option {
  protected $data, $none = true;
  function
__construct($data = NULL) { $this->data = $data; }
  function
is_some() : bool { return ! $this->none; }
  function
is_none() : bool { return $this->none; }
  function
if_let() : array {
    if ($this->none) { return []; }
    $a = [];
    $a[] = $this->s;
    return $a;
  }
}
function
None() : Option { return new Option; };
function
Some($data) : Option { return new Option($data); }
class Date3 {
  protected $date;
  function
__construct(string $date = NULL) {
    // input validatie, stel alleen valide datumstring in of geef een error
    if (is_null($date)) { $this->date = None(); }
    else { $this->date = Some($date); }
  }
  function
get() : Option { return $this->date; }
}

$d = new Date3;  // geen datum
foreach ($d->get()->if_let() as $date) {
  print $date;  // er gaat niets fout, we slaan bewust niet bestaande datums over
}
?>

Het subtiele maar belangrijke verschil is dat een programmeur verplicht is na te denken over beide uitkomsten: wel een datum of geen datum. Dit is een manier om de verantwoordelijkheid voor de logische fout te leggen waar die ligt, namelijk buiten de functies zoals getAge().

b.) Het is inderdaad waar dat allerlei functies in PHP rekening houden met de NULL-situatie, en zoals we weten is NULL in PHP niet echt een geheugenpointer naar het adres 0. Maar het is juist het complete vage van NULL, omdat het niet concreet iets zegt, dat er problemen ontstaan omdat het zo simpel over het hoofd is te zien. PHP heeft een bijzonder excentrieke en inconsistente benadering van veel van dit soort concepten. Ik haalde de reparatie op booleaanse logica in PHP 8 aan, maar jij had ook een mooi voorbeeld: empty(0) geeft true. Omdat 0 wordt beschouwd als leeg. Wat is daar de logica van in relatie tot NULL? Ik kan met terugwerkende kracht werkelijk niet begrijpen waarom 0 voor PHP net zo leeg is als NULL. Dat is wat ik noem een ontwerpfout in PHP.
 
Ozzie PHP

Ozzie PHP

18/12/2022 14:37:48
Quote Anchor link
Het komt er dus op neer dat er geen ideale oplossing is.

De voor mij meest logische oplossing is om dan toch voor consistentie te gaan. En voor mij houdt dat dat dan in om null te returnen indien iets er niet is. Dan kun je altijd een controle op null uitvoeren.

Wat mij betreft heeft het 2 voordelen.

Eerste voordeel: je voert altijd een controle uit en je weet dus of je wel of niet "iets" hebt. Heb je iets wat je verwacht te krijgen niet, dan kun je je uitvoer daarop aanpassen. Dit in tegenstelling tot bijv. het direct doorlopen van een lege array wat kan leiden tot een lege 'Onze bestsellers Top-10' lijst.

Tweede voordeel: de check is altijd identiek. Je hoeft dus niet te controleren op een lege string, lege array, een afwijkende integer, maar je checkt altijd op null.

Dank jullie beiden voor jullie input. Het is interessant om af en toe even te sparren om zo tot nieuwe inzichten te komen.
 

19/12/2022 10:17:16
Quote Anchor link
Ozzie PHP op 18/12/2022 14:37:48:
Het komt er dus op neer dat er geen ideale oplossing is.

De voor mij meest logische oplossing is om dan toch voor consistentie te gaan. En voor mij houdt dat dat dan in om null te returnen indien iets er niet is. Dan kun je altijd een controle op null uitvoeren.

Wat mij betreft heeft het 2 voordelen.

Eerste voordeel: je voert altijd een controle uit en je weet dus of je wel of niet "iets" hebt. Heb je iets wat je verwacht te krijgen niet, dan kun je je uitvoer daarop aanpassen. Dit in tegenstelling tot bijv. het direct doorlopen van een lege array wat kan leiden tot een lege 'Onze bestsellers Top-10' lijst.

Tweede voordeel: de check is altijd identiek. Je hoeft dus niet te controleren op een lege string, lege array, een afwijkende integer, maar je checkt altijd op null.

Dank jullie beiden voor jullie input. Het is interessant om af en toe even te sparren om zo tot nieuwe inzichten te komen.

Een lege top 10-lijst zou meerdere checks kunnen hebben, eentje die je gewoon zou kunnen gebruiken is count($rijen_uit_de_database);

De check met is_null() is een veelgebruikte en ook veel vergeten controle. Je moet dan zelf bij elke functie-aanroep bewust zijn van de signature ( ?string of iets anders ). Je geeft al aan dat je daar liever niet te veel bewust van bent door alle checks gelijk te trekken. Dat is niet heel vreemd, ik zou liever ook een consistente manier hebben om te werken. PHP gaat je daarbij in ieder geval niet helpen.
Misschien heb ik een IDE over het hoofd gezien die de gewenste werkwijze wel ondersteunt, maar degene die ik gebruikte (Eclipse PDT) deed dat in ieder geval niet.

Eind dit jaar stop ik geheel met PHP, ook particulier. Debian Linux ondersteunt geen recente PHP versie en ik wil niet afhankelijk zijn van 1 packager voor updates. En alles wat PHP is heb ik inmiddels omgeschreven in Rust.
Rust maakt mij wel elke keer bewust van de keuze om te falen (of niet). Dat gebeurt doordat functies een Option of een Result object returnen, zodat je van de taal een signaal krijgt om iets met dat resultaat te doen en ook alle situaties af te handelen.

Ik heb in deze thread een korte poging gedaan om iets dergelijks op te zetten voor PHP, maar uiteindelijk wint het gemak van is_null(). En zoals Ward al aangaf is dat op zich niet verkeerd, het is ook de context waarin de code moet functioneren die maakt dat het is wat het is.
Maar je zult het me vast niet kwalijk nemen dat ik na 20+ jaar PHP toe ben aan iets anders :-)
 
Ozzie PHP

Ozzie PHP

19/12/2022 13:28:52
Quote Anchor link
>> Een lege top 10-lijst zou meerdere checks kunnen hebben, eentje die je gewoon zou kunnen gebruiken is count($rijen_uit_de_database);

Correct ... maar dat is dus inderdaad precies wat ik bedoel. De ene keer zou je dan controleren op een lege array. De andere keer met count, dan weer controle op lege string. Altijd dezelfde controle op null is natuurlijk veel handiger.

>> Maar je zult het me vast niet kwalijk nemen dat ik na 20+ jaar PHP toe ben aan iets anders :-)

Dat mag uiteraard ;-)
 



Overzicht Reageren

 
 

Om de gebruiksvriendelijkheid van onze website en diensten te optimaliseren maken wij gebruik van cookies. Deze cookies gebruiken wij voor functionaliteiten, analytische gegevens en marketing doeleinden. U vindt meer informatie in onze privacy statement.