Mulighetene med Entity bundle classes
Prosjekter varierer stort i kompleksitet når det kommer til strukturering av data. Noen ganger er det bare en eller to nodetyper, andre ganger er det bare en standard produktstruktur. Andre ganger har man mer komplekse prosjekter hvor man har flere lag med entiteter som er avhengige av informasjon mellom hverandre via entitetsreferanse-felt. Erfaringsmessig vil jeg si at det hele kan (vil) tippe over til å bli rotete kode, og det fort.
Som eksempel, la oss si at vi lager en nettside for å holde kurs over nett. Vi har 3 entiteter:
- En gruppe med personer
- En deltaker i denne gruppa
- En person (som er deltaker i flere grupper)
Denne strukturen organisert rundt tre forskjellige nodetyper, med entitetsreferanser som definerer sammenkoblingene mellom de tre.
Core & contrib
La oss så si at vi skal forberede noe data å vise fra denne strukturen. Basert på en deltaker skal vi finne beskrivelsen av gruppa for å vise data i en template ett eller annet sted på nettsida. Hvis man bruker referansefeltene direkte, vil man ende opp med noe slikt:
// Get the description and location of the group
$group_participant = $a_node;
$group = $group_participant->field_group_reference->entity->field_text->value;
$location = $group_participant->field_group_reference->entity->field_location->value;
Dette er en relativt standard greie å se i custom drupal kode når man bruker entitetsreferansefelt. Gitt side opp og side ned med sånn kode, blir det raskt relativt uvennlig for både den som skriver koden, og den som skal bugfixe koden i etterkant. Her bruker vi altså core-modulen node med to forskjellige nodetyper. Hadde vi brukt tida på å lage egendefinerte entiteter, ville vi selvfølgelig kunne skrive koden slik:
// Get description and location for the group
$participant = $custom_entity;
$participant->getGroup()->getDescription();
$participant->getGroup()->getLocation();
Dette ser ganske mye mer intuitivt ut, ikke sant? Skriver man koden slik kan man også enkelt ta i bruk slike "moderne" greier som code completion i f eks PhpStorm for å traversere de forskjellige entitetene. En slik fremgangsmåte fungerer dog ikke i dag på taxonomies, media entiteter, noder eller andre entiteter som core og contrib moduler definerer.
Her er et anna eksempel. Referanser mellom noder og taksonomitermer er vanligvis enveis referanser. En deltaker kan være deltaker i flere grupper, men det vil i dette tilfellet være gruppedeltakeren som refererer til deltaker, og ikke omvendt. La oss si at vi kun vet om deltaker, men vi vil vite hvilke grupper denne deltakeren deltar i! Med service injection vil man sannsynligvis skrive dette slik, ved hjelp av entityQuery i bakgrunnen:
// Find the groups that this participant is in.
$participant = $a_node;
/** @var MyService $some_service */
$groups = $some_service->getGroupsByParticipant($participant);
Her kan du bruke autocomplete i kode-editioren, forbeholdt at du injiserer klassen alle steder hvor denne trenges. Men hvorfor skal man trenge det? Det burde være så simpelt som:
$participant->getGroups($status);
I skrivende stund kan vi kun skrive slik kode i to tilfeller:
- Ved å override node-klassen i sin helhet, en gang for alle
- Ved å lage en egendefinert entitet
Alternativ 1, hvis du overrider hele klassen fullstendig med din egen klasse, vil vi få dette scenarioet:
// Get the Schroedinger's participant.
$participant = $node;
// Is it a participant? let's find out.
$is_participant = $participant->isParticipant();
// Or maybe like this:
$is_participant = $participant->getType() == 'participant';
// Now it is a real participant.
PHP vil vite hvilken type dette er, men kodefullføring i editoren vil ikke vite hvilken type dette er, fordi vi ikke kan differensiere på node typer. Så på samme objektet kan vi da både:
$node->isGroupParticipant(); // would be false
$node->isParticipant(); // would be true
For alternativ 2 er det sånn at det i mange tilfeller typisk er 2 eller 3 superbrukbare kommandoer som man vil legge til på hver nodetype. Å lage en custom entitet i disse tilfellene er fullstendig overkill. Det er svært mye custom kode som lett kan unngås hvis vi kan lage egendefinerte entitetsklasser for hver av disse nodetypene.
Patchen som fikser alt
Patchen er her: https://www.drupal.org/node/2570593
Denna her gir oss en mulighet å legge til entity bundle classes, som igjen gir oss mulighet til å lage en custom klasse for hver av entitetstypene vi trenger dette til. Det første vi gjør er å bruke hook_entity_bundle_info_alter for å registrere bundles med sine respektive klasser:
function fabulous_module_entity_bundle_info_alter(&$bundles) {
$bundles['node']['participant']['class'] = ParticipantNode::class;
$bundles['node']['group']['class'] = GroupNode::class;
};
Så lager vi klasser, en for gruppa:
class GroupNode extends Node {
}
Og en for deltakeren:
class ParticipantNode extends Node {
}
Selvfølgelig kan du også beskrive den nye klassen din i et interface:
class ParticipantNode extends Node implements ParticipantNodeInterface {
}
Den viktige tingen her er at klassen *må* extende den registrerte entitetsklassen, i dette tilfellet 'Node''. Og det er basically alt!
Du kan nå bygge funksjonalitet for hver av nodetypene separat. La oss si at du nå laster en node:
// If we load..
$id = $a_participant
$node = Node::load($id);
if ($node instanceOf ParticipantNode) {
// Hooray, this is true!
}
Istedenfor et standard Node objekt, får du nå tilbake ditt eget ParticipantNode objekt!
Og nå, istedenfor å huske på feltnavn, kan du legge til funksjoner som dette:
public function getParticipant() {
return $this->field_participant->entity;
}
For å finne grupper, for å følge eksemplene over, kan du legge dette til i ParticipantNode-klassen (gitt at det er en 1-mange-relasjon):
public function getGroups($status) {
$result = $this->entityQuery('node')
->condition('type', 'group')
->condition('nid', $this->id())
->condtition('field_group_status', $status)
->execute();
return Node::loadMultiple(reset($result));
}
Slik kan du basically følge et potensielt endeløst spor av entieter som støtter code completion hele veien, det være om det er noder, brukere eller taksonomier:
$group_participant->getGroup()->getMunicipality()->getRegion();
Ikke bare noder
Enhver entity bundle, core eller contrib, kan inneha en entity bundle klasse på dette viset. Taxonomier, noder, media-entiteter, eller filer.
$term->reIndexReferencedEntities();
$comment->whatever();
$file->doSomeOtherFunkyBusiness();
// More examples here!!
Det er kult det her, men vær forsiktig
Når du bruker fremgangsmåten som patchen gir mulighet for, er det noen ting man bør gjøre og ikke gjøre. Først av alt, en enkelt bundle (f eks nodetype) kan bare ha en entity bundle klasse. Så fremgangsmåten er best brukt:
- I den semi-lovløse verden av custom prosjekt-spesifikk kode
- Veldig forsiktig i kode som er gjenbrukbar, heldst kun der hvor entitetstypen er definert innad i modulen eller prosjektet
Konklusjonen blir at grensen for denne fremgangsmåten går primært ved de enkelte prosjektene. Men det er også nettopp der koden har en tendens til å bli mest rotete over tid.