[PHP] return types, code completion en inherited classes

Pagina: 1
Acties:

Vraag


Acties:
  • 0 Henk 'm!

  • Saven
  • Registratie: December 2006
  • Laatst online: 02-10 16:35

Saven

Administrator

Topicstarter
Ik ben wat aan het puzzelen met een wat groter/abstracter knutselproject. Ik maak gebruik van abstracte classes en interfaces, en de uiteindelijke class methods returnen allemaal een subtype (SomeEntity) van de abstracte Entity class.

Daardoor werkt alleen de code completion niet meer in PHP Storm :'( Voorbeeldje:
PHP:
1
2
3
4
5
$factory = new ExternalServiceFactory();
$serviceClient = $factory->create('ExternalServiceABC');

$customer = $serviceClient->customers(1234)->get();
// $customer->....???

In dit geval geeft $customer een CustomerEntity terug. De get() method op de CustomersResource geeft dat als return type aan. Echter, de CustomersResource extent de parent abstract Resource class, en die definieert dat deze get() method een Entity moeten teruggeven.

Ik kan dus nu niet meer zien wat de properties en method van $customer zijn, omdat ik alleen gegevens van de abstracte Entity terugkrijg.

Klinkt wellicht vaag, daarom een zo concreet mogelijke sample:
PHP:
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
60
61
62
63
64
65
66
67
interface ExternalServiceInterface
{
    // Returning a type of Resource
    public function customers($id=null): Resource;
    public function books($id=null): Resource;

}

abstract class Resource
{
    // Returning a type of Entity
    public abstract function get(): Entity;
    public abstract function create(): Entity;
    public abstract function update(): Entity;
    public abstract function delete(): Entity;
}

abstract class Entity
{
    // Some Shared entity logic....
}

class CustomerEntity extends Entity
{
    // Customer data.....
}

class BookEntity extends Entity
{
    // Other data, specific for books.....
}

class CustomersResource extends Resource
{
    public function get(): CustomerEntity // CustomerEntity extends abstract class Entity
    {
        return new CustomerEntity(1234, 'Robert', 'Kelly', 'r.kelly@gmail.com');
    }
}

class BooksResource extends Resource
{
    public function get(): BookEntity // BookEntity extends abstract class Entity
    {
        return new BookEntity(567, 'Some fictional book', 'Dan Brown', 'ISO4855485');
    }
}

class AdapterForExternalServiceABC implements ExternalServiceInterface
{
    public SomeExternalService $gateway;

    public function __construct()
    {
        $this->gateway = new SomeExternalService( getenv('API_TOKEN') );
    }

    public function customers($id=null): CustomersResource
    {
        return new CustomersResource($this, $id);
    }

    public function books($id=null): BooksResource
    {
        return new BooksResource($this, $id);
    }
}


Ik weet niet of het überhaupt mogelijk is om dit te realiseren omdat het abstractieniveau vrij ver gaat, maar het is wel een beetje een gemis. Het definiëren van methods en properties via PHPDoc in de Entity class is dus helaas niet mogelijk, want elke entity heeft zijn eigen gegevens :+

Weten jullie hier raad mee? Of is dit gewoon een kansloze missie? :P

Alle reacties


Acties:
  • 0 Henk 'm!

  • Uhmmie
  • Registratie: Januari 2000
  • Laatst online: 20-08 17:30
Is er ergens in een plek waarbij je zowel Customers als Books vanuit een zelfde plek wilt beheren?

Zo niet, laat ze dan niet overerven als ze eigenlijk niks met elkaar te maken hebben. Mocht je dit willen vanuit een perspectief om ze beide op dezelfde manier op te kunnen slaan, dan zou ik dat op een andere manier aanpakken. Want alles overerven van een entity class beperkt je enorm in de toekomst.

Zie het als het volgende.. Ik heb een auto deur en een voordeur.. Het zijn allebei deuren, maar op geen enkele tijd en plek zou ik de twee met elkaar uit willen kunnen wisselen. Het is dan ook niet logisch om ze beide te extenden van een abstract class door {}.

Overigens heb ik je code even versimpeld, maar hier werkt het wel gewoon (onder php8):

code:
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
60
61
62
63
64
65
66
67
68
69
<?php

interface ExternalServiceInterface
{
    // Returning a type of Resource
    public function customers($id=null): Resource;
    public function books($id=null): Resource;

}

abstract class Entity {}

class CustomerEntity extends Entity
{
    public function CustomerDo()
    {
        echo "Customer doet iets.";
    }
}

class BookEntity extends Entity
{
    public function BookDo()
    {
        echo "Book doet iets";
    }
}

abstract class Resource
{
    // Returning a type of Entity
    public abstract function get(): Entity;
}

class CustomersResource extends Resource
{
    public function get(): CustomerEntity
    {
        return new CustomerEntity;
    }
}

class BooksResource extends Resource
{
    public function get(): BookEntity
    {
        return new BookEntity;
    }
}

class AdapterForExternalServiceABC implements ExternalServiceInterface
{
    public function customers($id=null): CustomersResource
    {
        return new CustomersResource;
    }

    public function books($id=null): BooksResource
    {
        return new BooksResource;
    }
}

$adapter = new AdapterForExternalServiceABC;
$customer = $adapter->customers(1)->get();
$customer->CustomerDo();

$book = $adapter->books(1)->get();
$book->BookDo();


Gewoon netjes met autcomplete.

Afbeeldingslocatie: https://tweakers.net/i/kYp7DzgIiClwSaEosuAb66iKa3k=/800x/filters:strip_exif()/f/image/6wZdqZ1E5VHdVgNH40b99C4f.png?f=fotoalbum_large

Currently playing: MTG Arena (PC)


Acties:
  • 0 Henk 'm!

  • Voutloos
  • Registratie: Januari 2002
  • Niet online
Even los van of het de beste opzet is: Als je toch met een factory of service locator eindigt waarvan je phpstorm wil helpen met een mapping, is dat appeltje eitje met de .phpstorm.meta.php feature. :)

(Meta met mate natuurlijk. Als je een testsuite voor je meta file mist, ga je misschien een tikkie te ver ;) )

[ Voor 22% gewijzigd door Voutloos op 04-04-2021 23:06 ]

{signature}


Acties:
  • 0 Henk 'm!

  • Saven
  • Registratie: December 2006
  • Laatst online: 02-10 16:35

Saven

Administrator

Topicstarter
Uhmmie schreef op zondag 4 april 2021 @ 22:17:
Is er ergens in een plek waarbij je zowel Customers als Books vanuit een zelfde plek wilt beheren?

Zo niet, laat ze dan niet overerven als ze eigenlijk niks met elkaar te maken hebben. Mocht je dit willen vanuit een perspectief om ze beide op dezelfde manier op te kunnen slaan, dan zou ik dat op een andere manier aanpakken. Want alles overerven van een entity class beperkt je enorm in de toekomst.

Zie het als het volgende.. Ik heb een auto deur en een voordeur.. Het zijn allebei deuren, maar op geen enkele tijd en plek zou ik de twee met elkaar uit willen kunnen wisselen. Het is dan ook niet logisch om ze beide te extenden van een abstract class door {}.

Overigens heb ik je code even versimpeld, maar hier werkt het wel gewoon (onder php8):
Thanks voor je input! De abstract Entity class bestaat voornamelijk zodat de Resource get(); etc. geforceerd kunnen worden om überhaupt een type Entity terug te moeten geven. Dat kan natuurlijk ook met een interface, maar er zitten een paar features in die Entity class die op zich losstaan van de Entity maar wel gedeeld worden. Denk aan uitlezen van magic properties (via __get()) of een toArray() functie oid die alle properties van een BookEntity/CustomerEntity naar een array omzet. En een $id property, omdat elke Entity een id heeft. Maar dat was het eigenlijk wel :+

Jouw code werkt idd! Maar dat heeft me meteen doen checken waarom het bij mij niet werkt. Je hebt net een stapje afgesneden :P Het lijkt erop dat het komt omdat de Factory ook een globale Interface returnt in plaats van een specifieke Adapter. Check dit maar eens: hier werkt code completion niet meer terwijl de code gewoon correct werkt (het onderste gedeelte is veranderd):
PHP:
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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
interface ExternalServiceInterface
{
    // Returning a type of Resource
    public function customers($id=null): Resource;
    public function books($id=null): Resource;

}

abstract class Entity {}

class CustomerEntity extends Entity
{
    public function CustomerDo()
    {
        echo "Customer doet iets.";
    }
}

class BookEntity extends Entity
{
    public function BookDo()
    {
        echo "Book doet iets";
    }
}

abstract class Resource
{
    // Returning a type of Entity
    public abstract function get(): Entity;
}

class CustomersResource extends Resource
{
    public function get(): CustomerEntity
    {
        return new CustomerEntity;
    }
}

class BooksResource extends Resource
{
    public function get(): BookEntity
    {
        return new BookEntity;
    }
}

class AdapterForExternalServiceABC implements ExternalServiceInterface
{
    public function customers($id=null): CustomersResource
    {
        return new CustomersResource;
    }

    public function books($id=null): BooksResource
    {
        return new BooksResource;
    }
}

class ExternalServiceAdapterFactory
{
    public function create($service): ExternalServiceInterface
    {
        return new $service();
    }
}

$factory = new ExternalServiceAdapterFactory();
$adapter = $factory->create('AdapterForExternalServiceABC');

$customer = $adapter->customers(1)->get();
$customer->CustomerDo();

$book = $adapter->books(1)->get();
$book->BookDo();

Afbeeldingslocatie: https://i.imgur.com/uLCVAWM.png

De Interface zorgt ervoor dat elke adapter een books() en customers() method heeft. Op die manier kun je bijv. via een abstracte laag altijd deze data op dezelfde manier ophalen; of je nu met Bol.com of Amazon verbindt. In de factory->create geef je dus aan voor welke externe service de adapter moet worden ingeladen. Is een voorbeeld hoor, maar misschien maakt zo'n voorbeeld het wat minder 'abstract' :D
Voutloos schreef op zondag 4 april 2021 @ 22:55:
Even los van of het de beste opzet is: Als je toch met een factory of service locator eindigt waarvan je phpstorm wil helpen met een mapping, is dat appeltje eitje met de .phpstorm.meta.php feature. :)

(Meta met mate natuurlijk. Als je een testsuite voor je meta file mist, ga je misschien een tikkie te ver ;) )
Dat kende ik niet, ga ik eens naar kijken :) Thanks!

Acties:
  • 0 Henk 'm!

  • Uhmmie
  • Registratie: Januari 2000
  • Laatst online: 20-08 17:30
@Saven
Kan je dit even verder uit werken?
code:
1
2
3
4
5
6
7
class ExternalServiceAdapterFactory
{
    public function create($service): ExternalServiceInterface
    {
        return new $service();
    }
}


Want ik heb het idee dat je dit met een reden wilt doen op deze manier. Maar ik gok juist dat dit net het deel is wat je zal moeten refactoren.

Die $service komt waarschijnlijk uit een database record oid?

[edit] Dit zou het op moeten lossen.

code:
1
2
3
4
5
interface ExternalServiceInterface
{
    public function customers($id=null): CustomersResource;
    public function books($id=null): BooksResource;
}

[ Voor 125% gewijzigd door Uhmmie op 05-04-2021 14:48 ]

Currently playing: MTG Arena (PC)


Acties:
  • 0 Henk 'm!

  • Saven
  • Registratie: December 2006
  • Laatst online: 02-10 16:35

Saven

Administrator

Topicstarter
Uhmmie schreef op maandag 5 april 2021 @ 14:34:
@Saven

Kan je dit even verder uit werken?
code:
1
2
3
4
5
6
7
class ExternalServiceAdapterFactory
{
    public function create($service): ExternalServiceInterface
    {
        return new $service();
    }
}


Want ik heb het idee dat je dit met een reden wilt doen op deze manier. Maar ik gok juist dat dit net het deel is wat je zal moeten refactoren.

Die $service komt waarschijnlijk uit een database record oid?
Dit is wel waar het op neer komt eigenlijk :P Uit de DB komt inderdaad (per user bijvoorbeeld) welke external service gebruikt moet worden. Bijv. als je een payment provider hebt. De ene user kiest voor iDeal en loopt via de service van Adyen en bij de andere user loopt het via PayPayl oid. Daar is het factory pattern ook voor bedoeld als ik me niet vergis.

Zit nog iets meer logica in hoor, zoals het meegeven van de api keys van de user enzo. Maar dat maakt voor de return value niks uit.

Verder werkt het eigenlijk allemaal ook wel prima. Het enige is dus, met deze extra Factory tussenlaag, dat PhpStorm vervolgens niet verder lijkt te scannen dan de abstracte Resource class ipv te scannen welke resource daadwerkelijk wordt ingeladen.

[ Voor 5% gewijzigd door Saven op 05-04-2021 14:48 ]


Acties:
  • 0 Henk 'm!

  • Uhmmie
  • Registratie: Januari 2000
  • Laatst online: 20-08 17:30
@Saven Persoonlijk zou ik heel die entity logica splitsen van de domein logica. Maar ik zie veel mensen dit doen ivm hoe sommige frameworks werken. Ik heb in mijn bovenstaande reactie nog een oplossing gepost waarop het misging.

Currently playing: MTG Arena (PC)


Acties:
  • 0 Henk 'm!

  • Josk79
  • Registratie: September 2013
  • Laatst online: 23:13
Als je vanuit $adapter->customers() iets wilt doen wat gedefinieerd is in CustomersResource, dan moet je in de interface aangeven dat deze een CustomersResource retourneert, i.p.v. een Resource.

code:
1
2
3
4
5
6
7
interface ExternalServiceInterface
{
    // Returning a type of Resource
    public function customers($id=null): Resource;
    public function books($id=null): Resource;

}


vs

code:
1
2
3
4
5
6
interface ExternalServiceInterface
{
    // Returning a type of Resource
    public function customers($id=null): CustomersResource;
    public function books($id=null): BooksResource;
}


Het zou natuurlijk mooi zijn als PHP generics ondersteunde...

Acties:
  • +1 Henk 'm!

  • Uhmmie
  • Registratie: Januari 2000
  • Laatst online: 20-08 17:30
Josk79 schreef op maandag 5 april 2021 @ 15:15:
Het zou natuurlijk mooi zijn als PHP generics ondersteunde...
We mogen altijd blijven dromen (of in mijn geval een andere taal leren ;)).

Currently playing: MTG Arena (PC)


Acties:
  • 0 Henk 'm!

  • Saven
  • Registratie: December 2006
  • Laatst online: 02-10 16:35

Saven

Administrator

Topicstarter
Uhmmie schreef op maandag 5 april 2021 @ 14:53:
@Saven Persoonlijk zou ik heel die entity logica splitsen van de domein logica. Maar ik zie veel mensen dit doen ivm hoe sommige frameworks werken. Ik heb in mijn bovenstaande reactie nog een oplossing gepost waarop het misging.
Dat kan kloppen hehe :P Hoewel offtopic, zou je misschien kort kunnen beschrijven hoe je dat zou opdelen? Want In principe zijn Entities al gesplitst en geheel op zichzelf staand. Ze worden alleen gevuld vanuit een Resource.

Zie ook hieronder...
Josk79 schreef op maandag 5 april 2021 @ 15:15:
Als je vanuit $adapter->customers() iets wilt doen wat gedefinieerd is in CustomersResource, dan moet je in de interface aangeven dat deze een CustomersResource retourneert, i.p.v. een Resource.

code:
1
2
3
4
5
6
7
interface ExternalServiceInterface
{
    // Returning a type of Resource
    public function customers($id=null): Resource;
    public function books($id=null): Resource;

}


vs

code:
1
2
3
4
5
6
interface ExternalServiceInterface
{
    // Returning a type of Resource
    public function customers($id=null): CustomersResource;
    public function books($id=null): BooksResource;
}


Het zou natuurlijk mooi zijn als PHP generics ondersteunde...
Aah ja goeie! Maar sorry, was niet helemaal duidelijk denk ik :Y) Elke ClientAdapter heeft zijn eigen resources. Want ClientABC heeft andere logica nodig om books bij Bol.com op te halen dan ClientXYZ bij Amazon. Vandaar dat de ClientInterface een Resource als return type forceert. De BooksResource van zowel ClientABC als ClientXYZ moet de Resource implementeren die voorschrijft welke methods altijd aanwezig moeten zijn (get, create, update, delete etc.)

Ter illustratie, mijn mapstructuur ziet er ongeveer zo uit
code:
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
.
└── App/
    ├── Entities/
    │   ├── Entity.php
    │   ├── BookEntity.php
    │   ├── CustomerEntity.php
    │   └── ...
    ├── ExternalServices/
    │   ├── ExternalServiceInterface.php
    │   ├── ExternalServiceFactory.php
    │   ├── ResourceInterface.php
    │   ├── ServiceABC/
    │   │   ├── ExternalServiceABCClientAdapter.php
    │   │   └── Resources/
    │   │       ├── BooksResource.php
    │   │       ├── CustomersResource.php
    │   │       └── ...
    │   └── ServiceXYZ/
    │       ├── ExternalServiceXYZClientAdapter.php
    │       └── Resources/
    │           ├── BooksResource.php
    │           ├── CustomersResource.php
    │           └── ...
    └── Exceptions/
        └── ExternalServiceClientException.php


Zoals je misschien hebt gemerkt ben ik ook veel aan het ontdekken. Mocht ik iets als een debiel hebben ingericht/opgezet ( :') ) laat het dan vooral weten. :P

Acties:
  • 0 Henk 'm!

  • Josk79
  • Registratie: September 2013
  • Laatst online: 23:13
Maar implementeren al die BooksResource niet allemaal van dezelfde interface of abstracte klasse, bijv. IBooksResource of AbstractBooksResource? Idem voor CustomersResource...

In mijn ogen is het onjuist als je interface een return type specificeert (bijv Resource) en je daarna een method oproept welke niet in die return type bestaat... druist ook in tegen het substitutieprincipe van Liskov.

Acties:
  • 0 Henk 'm!

  • Uhmmie
  • Registratie: Januari 2000
  • Laatst online: 20-08 17:30
@Saven Ik zou eens kijken naar het repository pattern . 1 voor books en 1 voor customers.
Vervolgens zou ik een Customer en Book aanmaken die alleen de domein logica bevallen en hierin een toArray en een static createFromArray functie maken.

Die createFormArray moet met de output van toArray weer het object in dezelfde staat terug op kunnen bouwen.

in je repostory doe je iets van save(Book $book) en findByBookId($id); maar natuurlijke kan je ook allerlei andere functies aanmaken.

Indien je meerdere type recourses hebt zou je ook nog een implementatie per recourse kunnen maken (en dan 1 overkoepelende interface, zodat ze allemaal wel dezelfde functionaliteit hebt). Die kan je dan weer met een Factory zoals je nu doet bepalen.

Even snel met google naar een voorbeeld gezocht en toen kwam ik het volgende tegen:
https://designpatternsphp...re/Repository/README.html

[ Voor 39% gewijzigd door Uhmmie op 05-04-2021 17:27 ]

Currently playing: MTG Arena (PC)


Acties:
  • 0 Henk 'm!

  • Saven
  • Registratie: December 2006
  • Laatst online: 02-10 16:35

Saven

Administrator

Topicstarter
Josk79 schreef op maandag 5 april 2021 @ 16:06:
Maar implementeren al die BooksResource niet allemaal van dezelfde interface of abstracte klasse, bijv. IBooksResource of AbstractBooksResource? Idem voor CustomersResource...

In mijn ogen is het onjuist als je interface een return type specificeert (bijv Resource) en je daarna een method oproept welke niet in die return type bestaat... druist ook in tegen het substitutieprincipe van Liskov.
Nope, alleen een globale resource. Maar ook dit zet me wel aan het denken :)
Uhmmie schreef op maandag 5 april 2021 @ 17:22:
@Saven Ik zou eens kijken naar het repository pattern . 1 voor books en 1 voor customers.
Vervolgens zou ik een Customer en Book aanmaken die alleen de domein logica bevallen en hierin een toArray en een static createFromArray functie maken.

Die createFormArray moet met de output van toArray weer het object in dezelfde staat terug op kunnen bouwen.

in je repostory doe je iets van save(Book $book) en findByBookId($id); maar natuurlijke kan je ook allerlei andere functies aanmaken.

Indien je meerdere type recourses hebt zou je ook nog een implementatie per recourse kunnen maken (en dan 1 overkoepelende interface, zodat ze allemaal wel dezelfde functionaliteit hebt). Die kan je dan weer met een Factory zoals je nu doet bepalen.

Even snel met google naar een voorbeeld gezocht en toen kwam ik het volgende tegen:
https://designpatternsphp...re/Repository/README.html
De hele opzet lijkt idd een beetje vaag tussen allerlei soorten 'patterns' te hangen :P Ik denk niet dat het écht volwaardig repository is, maar ook dit zet me wel aan het denken :)


Ik ga er nog eens aan sleutelen van t weekend. Als ik nog iets spannends te melden heb houd ik jullie op de hoogte :P Thnx nogmaals!
Pagina: 1