Lines 97.36% 74 / 76
Functions and Methods 50.00% 2 / 4
Classes and Traits 0.00% 0 / 1
Name Lines Functions and Methods CRAP Classes and Traits
EntityHandleEmitter 97.36% 74 / 76 50.00% 2 / 4 9 0.00% 0 / 1
 __construct 0.00% 0 / 1 0.00% 0 / 1 2
 supportsDomain 100.00% 1 / 1 100.00% 1 / 1 1
 emitFile 98.63% 72 / 73 0.00% 0 / 1 5
 listReservedMemberNames 100.00% 1 / 1 100.00% 1 / 1 2
1<?php
2
3declare(strict_types=1);
4
5namespace Stewart\Codegen\Emitter\Entity;
6
7use Stewart\Codegen\Emitter\DomainFileEmitter;
8use Stewart\Codegen\Emitter\EmitContext;
9use Stewart\Codegen\Emitter\Service\ServiceMethodEmitter;
10use Stewart\Codegen\Model\DomainModel;
11use Stewart\Codegen\Output\GeneratedFile;
12use Stewart\Codegen\Php\MemberScope;
13use Stewart\Codegen\Php\ReservesMemberNames;
14use Stewart\Codegen\Service\ServiceModel;
15use Stewart\Contracts\Entity\Entity;
16use Stewart\Contracts\Entity\EntityId;
17use Stewart\Contracts\Entity\TypedEntity;
18use Stewart\Contracts\Exception\HistoryException;
19use Stewart\Contracts\Exception\IdentifierException;
20use Stewart\Contracts\Exception\StateException;
21use Stewart\Contracts\Exception\WaitException;
22use Stewart\Contracts\HaContext;
23use Stewart\Contracts\History\EntityStateHistory;
24use Stewart\Contracts\History\HistoryQuery;
25use Stewart\Contracts\Service\ServiceResponse;
26use Stewart\Contracts\Service\ServiceTarget;
27use Stewart\Contracts\State\EventContext;
28use Stewart\Contracts\StateChangeStream;
29use Stewart\Contracts\Time\Duration;
30
31final readonly class EntityHandleEmitter implements DomainFileEmitter, ReservesMemberNames
32{
33    private const array RESERVED_MEMBER_NAMES = ['id', 'getDomain', 'isGeneratedEntityId', 'getState', 'requireState', 'watchStateChanges', 'getHistory', 'waitForState', 'listAttributes', 'getEntity', 'toServiceTarget', 'ha'];
34
35    private const string ENTITY_IDS_CONSTANT = 'GENERATED_ENTITY_IDS';
36
37    public function __construct(private ServiceMethodEmitter $services) {}
38
39    public function supportsDomain(DomainModel $domain): bool
40    {
41        return $domain->hasEntities();
42    }
43
44    public function emitFile(DomainModel $domain, EmitContext $context): GeneratedFile
45    {
46        $namespace = $context->createNamespace();
47        $declared = $domain->getHandleClass();
48        $stateException = $context->importClass($namespace, StateException::class, $declared);
49        $identifierException = $context->importClass($namespace, IdentifierException::class, $declared);
50        $historyException = $context->importClass($namespace, HistoryException::class, $declared);
51        $waitException = $context->importClass($namespace, WaitException::class, $declared);
52
53        foreach ([Entity::class, EntityId::class, HaContext::class, EntityStateHistory::class, HistoryQuery::class, StateChangeStream::class, ServiceTarget::class, TypedEntity::class, Duration::class] as $import) {
54            $context->importClass($namespace, $import, $declared);
55        }
56
57        if (array_any($domain->handleServices, static fn(ServiceModel $service): bool => $service->definition->returnsResponse)) {
58            $context->importClass($namespace, ServiceResponse::class, $declared);
59        }
60
61        if (array_any($domain->handleServices, static fn(ServiceModel $service): bool => !$service->definition->returnsResponse)) {
62            $context->importClass($namespace, EventContext::class, $declared);
63        }
64
65        $class = $namespace->addClass($declared)
66            ->setFinal()
67            ->setReadOnly()
68            ->addImplement(TypedEntity::class)
69            ->addComment(\sprintf('One `%s` entity.', $domain->domain));
70
71        $class->addConstant(self::ENTITY_IDS_CONSTANT, array_fill_keys($domain->entityIds->toStrings(), true))
72            ->setType('array')
73            ->setPrivate();
74
75        $class->addProperty('entity')->setType(Entity::class)->setPrivate();
76
77        $constructor = $class->addMethod('__construct')->addComment('@throws ' . $identifierException);
78        $constructor->addParameter('ha')->setType(HaContext::class);
79        $constructor->addPromotedParameter('id')->setType(EntityId::class)->setPublic();
80        $constructor->setBody(\sprintf(
81            "if (!self::isGeneratedEntityId(\$id)) {\n    throw %s::entityNotGenerated(\$id->value, self::getDomain());\n}\n\n\$this->entity = \$ha->getEntity(\$id);",
82            $identifierException,
83        ));
84
85        $class->addMethod('getDomain')
86            ->setStatic()
87            ->setReturnType('string')
88            ->setBody('return ?;', [$domain->domain]);
89
90        $class->addMethod('isGeneratedEntityId')
91            ->setStatic()
92            ->setReturnType('bool')
93            ->setBody(\sprintf('return isset(self::%s[$id->value]);', self::ENTITY_IDS_CONSTANT))
94            ->addParameter('id')
95            ->setType(EntityId::class);
96
97        $class->addMethod('getState')
98            ->setReturnType('?' . $context->resolveClassName($domain->getStateClass()))
99            ->setBody(\sprintf('return %s::fromNullableState($this->entity->getState());', $domain->getStateClass()));
100
101        $class->addMethod('requireState')
102            ->setReturnType($context->resolveClassName($domain->getStateClass()))
103            ->addComment('@throws ' . $stateException)
104            ->setBody(\sprintf('return new %s($this->entity->requireState());', $domain->getStateClass()));
105
106        $class->addMethod('watchStateChanges')
107            ->setReturnType(StateChangeStream::class)
108            ->setBody('return $this->entity->watchStateChanges();');
109
110        $class->addMethod('getHistory')
111            ->setReturnType(EntityStateHistory::class)
112            ->addComment('@throws ' . $historyException)
113            ->setBody('return $this->entity->getHistory($query);')
114            ->addParameter('query')
115            ->setType(HistoryQuery::class);
116
117        $waitForState = $class->addMethod('waitForState')
118            ->setReturnType('?' . $context->resolveClassName($domain->getStateClass()))
119            ->addComment('@throws ' . $waitException)
120            ->setBody(\sprintf('return %s::fromNullableState($this->entity->waitForState($state, $timeout));', $domain->getStateClass()));
121        $waitForState->addParameter('state')->setType('string');
122        $waitForState->addParameter('timeout', null)->setType(Duration::class)->setNullable();
123
124        $class->addMethod('listAttributes')
125            ->setReturnType('array')
126            ->addComment('@return array<string, mixed>')
127            ->setBody("\$state = \$this->entity->getState();\n\nreturn \$state === null ? [] : \$state->attributes;");
128
129        $class->addMethod('getEntity')
130            ->setReturnType(Entity::class)
131            ->setBody('return $this->entity;');
132
133        $class->addMethod('toServiceTarget')
134            ->setReturnType(ServiceTarget::class)
135            ->setBody('return $this->entity->toServiceTarget();');
136
137        foreach ($domain->handleServices as $service) {
138            $this->services->addHandleMethod($class, $service);
139        }
140
141        return $context->printFile($namespace, $declared);
142    }
143
144    public function listReservedMemberNames(MemberScope $scope): array
145    {
146        return $scope === MemberScope::EntityHandle ? self::RESERVED_MEMBER_NAMES : [];
147    }
148}