1717use Ecotone \Messaging \Config \Annotation \AnnotatedDefinitionReference ;
1818use Ecotone \Messaging \Config \Annotation \AnnotationModule ;
1919use Ecotone \Messaging \Config \Configuration ;
20+ use Ecotone \Messaging \Config \Container \Definition ;
2021use Ecotone \Messaging \Config \Container \InterfaceToCallReference ;
2122use Ecotone \Messaging \Config \Container \Reference ;
2223use Ecotone \Messaging \Config \ModulePackageList ;
2526use Ecotone \Messaging \Handler \InterfaceToCallRegistry ;
2627use Ecotone \Messaging \Handler \Processor \MethodInvoker \MethodInvokerBuilder ;
2728use Ecotone \Messaging \Handler \ServiceActivator \MessageProcessorActivatorBuilder ;
29+ use Ecotone \Messaging \Handler \ServiceActivator \ServiceActivatorBuilder ;
2830use Ecotone \Messaging \Support \Assert ;
31+ use Ecotone \Messaging \Config \ConfigurationException ;
32+ use Ecotone \Messaging \Endpoint \InboundChannelAdapter \InboundChannelAdapterBuilder ;
2933use Ecotone \Modelling \Attribute \EventHandler ;
3034use Ecotone \Modelling \Attribute \NamedEvent ;
35+ use Ecotone \Projecting \Attribute \PollingProjection ;
3136use Ecotone \Projecting \Attribute \Projection ;
3237use Ecotone \Projecting \Attribute \ProjectionBatchSize ;
3338use Ecotone \Projecting \Attribute \ProjectionFlush ;
39+ use Ecotone \Projecting \EventStoreAdapter \PollingProjectionChannelAdapter ;
40+ use Ecotone \Projecting \EventStoreAdapter \StreamingProjectionMessageHandler ;
41+ use Ecotone \Projecting \ProjectorExecutor ;
3442use LogicException ;
3543
3644/**
@@ -42,10 +50,14 @@ class ProjectingAttributeModule implements AnnotationModule
4250 /**
4351 * @param EcotoneProjectionExecutorBuilder[] $projectionBuilders
4452 * @param MessageProcessorActivatorBuilder[] $lifecycleHandlers
53+ * @param array<string, string> $pollingProjections Map of projection name to endpoint ID
54+ * @param array<string, array{streamingChannelName: string, endpointId: string, projectionBuilder: EcotoneProjectionExecutorBuilder}> $eventStreamingProjections
4555 */
4656 public function __construct (
4757 private array $ projectionBuilders = [],
48- private array $ lifecycleHandlers = []
58+ private array $ lifecycleHandlers = [],
59+ private array $ pollingProjections = [],
60+ private array $ eventStreamingProjections = []
4961 ) {
5062 }
5163
@@ -59,15 +71,45 @@ public static function create(AnnotationFinder $annotationRegistrationService, I
5971
6072 /** @var array<string, EcotoneProjectionExecutorBuilder> $projectionBuilders */
6173 $ projectionBuilders = [];
74+ $ pollingProjections = [];
75+ $ eventStreamingProjections = [];
6276 foreach ($ annotationRegistrationService ->findAnnotatedClasses (Projection::class) as $ projectionClassName ) {
6377 $ projectionAttribute = $ annotationRegistrationService ->getAttributeForClass ($ projectionClassName , Projection::class);
6478 $ batchSizeAttribute = $ annotationRegistrationService ->findAttributeForClass ($ projectionClassName , ProjectionBatchSize::class);
6579 $ projectionBuilder = new EcotoneProjectionExecutorBuilder ($ projectionAttribute ->name , $ projectionAttribute ->partitionHeaderName , $ projectionAttribute ->automaticInitialization , $ namedEvents , batchSize: $ batchSizeAttribute ?->batchSize);
6680
6781 $ asynchronousChannelName = self ::getProjectionAsynchronousChannel ($ annotationRegistrationService , $ projectionClassName );
82+
83+ if ($ projectionAttribute ->isPolling () && $ asynchronousChannelName !== null ) {
84+ throw ConfigurationException::create (
85+ "Projection ' {$ projectionAttribute ->name }' cannot use both PollingProjection and #[Asynchronous] attributes. " .
86+ 'A projection must be either polling-based or event-driven (synchronous/asynchronous), not both. '
87+ );
88+ }
89+
90+ if ($ projectionAttribute ->isEventStreaming () && $ asynchronousChannelName !== null ) {
91+ throw ConfigurationException::create (
92+ "Projection ' {$ projectionAttribute ->name }' cannot use both EventStreamingProjection and #[Asynchronous] attributes. " .
93+ 'Event streaming projections consume directly from streaming channels. '
94+ );
95+ }
96+
6897 if ($ asynchronousChannelName !== null ) {
6998 $ projectionBuilder ->setAsyncChannel ($ asynchronousChannelName );
7099 }
100+
101+ if ($ projectionAttribute ->isPolling ()) {
102+ $ pollingProjections [$ projectionAttribute ->name ] = $ projectionAttribute ->getEndpointId ();
103+ }
104+
105+ if ($ projectionAttribute ->isEventStreaming ()) {
106+ $ eventStreamingProjections [$ projectionAttribute ->name ] = [
107+ 'streamingChannelName ' => $ projectionAttribute ->streamingChannelName ,
108+ 'endpointId ' => $ projectionAttribute ->name ,
109+ 'projectionBuilder ' => $ projectionBuilder ,
110+ ];
111+ }
112+
71113 $ projectionBuilders [$ projectionAttribute ->name ] = $ projectionBuilder ;
72114 }
73115
@@ -110,14 +152,54 @@ public static function create(AnnotationFinder $annotationRegistrationService, I
110152 ->withInputChannelName ($ inputChannel );
111153 }
112154
113- return new self (array_values ($ projectionBuilders ), $ lifecycleHandlers );
155+ return new self (array_values ($ projectionBuilders ), $ lifecycleHandlers, $ pollingProjections , $ eventStreamingProjections );
114156 }
115157
116158 public function prepare (Configuration $ messagingConfiguration , array $ extensionObjects , ModuleReferenceSearchService $ moduleReferenceSearchService , InterfaceToCallRegistry $ interfaceToCallRegistry ): void
117159 {
118160 foreach ($ this ->lifecycleHandlers as $ lifecycleHandler ) {
119161 $ messagingConfiguration ->registerMessageHandler ($ lifecycleHandler );
120162 }
163+
164+ foreach ($ this ->pollingProjections as $ projectionName => $ endpointId ) {
165+ $ messagingConfiguration ->registerConsumer (
166+ InboundChannelAdapterBuilder::createWithDirectObject (
167+ ProjectingModule::inputChannelForProjectingManager ($ projectionName ),
168+ new PollingProjectionChannelAdapter (),
169+ $ interfaceToCallRegistry ->getFor (PollingProjectionChannelAdapter::class, 'execute ' )
170+ )
171+ ->withEndpointId ($ endpointId )
172+ );
173+ }
174+
175+ foreach ($ this ->eventStreamingProjections as $ projectionName => $ config ) {
176+ $ projectorExecutorReference = ProjectingModule::getProjectorExecutorReference ($ projectionName );
177+ $ projectionBuilder = $ config ['projectionBuilder ' ];
178+ $ moduleReferenceSearchService ->store (
179+ $ projectorExecutorReference ,
180+ $ projectionBuilder
181+ );
182+
183+ $ handlerReference = StreamingProjectionMessageHandler::class . ': ' . $ projectionName ;
184+
185+ // Register the handler service
186+ $ messagingConfiguration ->registerServiceDefinition (
187+ $ handlerReference ,
188+ new Definition (StreamingProjectionMessageHandler::class, [
189+ new Reference ($ projectorExecutorReference ),
190+ $ projectionName ,
191+ ])
192+ );
193+
194+ $ messagingConfiguration ->registerMessageHandler (
195+ ServiceActivatorBuilder::create (
196+ $ handlerReference ,
197+ InterfaceToCallReference::create (StreamingProjectionMessageHandler::class, 'handle ' )
198+ )
199+ ->withEndpointId ($ config ['endpointId ' ])
200+ ->withInputChannelName ($ config ['streamingChannelName ' ])
201+ );
202+ }
121203 }
122204
123205 public function canHandle ($ extensionObject ): bool
@@ -127,7 +209,12 @@ public function canHandle($extensionObject): bool
127209
128210 public function getModuleExtensions (ServiceConfiguration $ serviceConfiguration , array $ serviceExtensions ): array
129211 {
130- return $ this ->projectionBuilders ;
212+ // Filter out event streaming projections - they don't need ProjectingManager
213+ $ eventStreamingProjectionNames = array_keys ($ this ->eventStreamingProjections );
214+ return array_filter (
215+ $ this ->projectionBuilders ,
216+ fn ($ builder ) => !in_array ($ builder ->projectionName (), $ eventStreamingProjectionNames , true )
217+ );
131218 }
132219
133220 public function getModulePackageName (): string
0 commit comments