1<?php
2/**
3 * Copyright © 2019-2026 Rhubarb Tech Inc. All Rights Reserved.
4 *
5 * The Object Cache Pro Software and its related materials are property and confidential
6 * information of Rhubarb Tech Inc. Any reproduction, use, distribution, or exploitation
7 * of the Object Cache Pro Software and its related materials, in whole or in part,
8 * is strictly forbidden unless prior permission is obtained from Rhubarb Tech Inc.
9 *
10 * In addition, any reproduction, use, distribution, or exploitation of the Object Cache Pro
11 * Software and its related materials, in whole or in part, is subject to the End-User License
12 * Agreement accessible in the included `LICENSE` file, or at: https://objectcache.pro/eula
13 */
14
15declare(strict_types=1);
16
17namespace RedisCachePro\Clients;
18
19use Closure;
20use Throwable;
21use InvalidArgumentException;
22
23use RedisCachePro\Configuration\Configuration;
24
25use OpenTelemetry\API\Globals;
26use OpenTelemetry\API\Trace\SpanKind;
27use OpenTelemetry\API\Trace\NoopTracer;
28use OpenTelemetry\API\Trace\TracerInterface;
29use OpenTelemetry\API\Trace\TracerProviderInterface;
30
31abstract class Client implements ClientInterface
32{
33 /**
34 * The client instance.
35 *
36 * @var object
37 */
38 protected $client;
39
40 /**
41 * The callback name.
42 *
43 * @var string
44 */
45 protected $callback;
46
47 /**
48 * The context.
49 *
50 * @var mixed
51 */
52 protected $context;
53
54 /**
55 * Creates a new `ClientInterface` instance.
56 *
57 * @param callable $client
58 * @param string|callable|null $callback
59 * @param mixed $context
60 * @return void
61 */
62 public function __construct(callable $client, $callback = null, $context = null)
63 {
64 $this->context = $this->prepareContext($context, $callback);
65
66 if (\is_callable($callback)) {
67 $callback = 'callable';
68 }
69
70 if ($callback === Configuration::TRACER_NONE) {
71 $callback = null;
72 }
73
74 if ($callback === Configuration::TRACER_OPENTELEMETRY && ! $this->context) {
75 $callback = null;
76 }
77
78 $this->callback = $callback ? "{$callback}Callback" : 'passthroughCallback';
79
80 if (! \method_exists($this, $this->callback)) {
81 throw new InvalidArgumentException("Callback `{$callback}` is not supported by " . __CLASS__);
82 }
83
84 $this->client = $this->{$this->callback}($client, '__construct');
85 }
86
87 /**
88 * Forwards all calls to registered callback.
89 *
90 * @param string $method
91 * @param array<mixed> $arguments
92 * @return mixed
93 */
94 public function __call($method, $arguments)
95 {
96 return $this->{$this->callback}(function () use ($method, $arguments) {
97 return $this->client->{$method}(...$arguments);
98 }, \strtolower($method));
99 }
100
101 /**
102 * Returns prepared context before it's being set.
103 *
104 * @param mixed $context
105 * @param string|callable|null $callback
106 * @return mixed
107 */
108 protected function prepareContext($context, $callback)
109 {
110 if (\is_callable($callback)) {
111 return $callback;
112 }
113
114 if ($callback === Configuration::TRACER_OPENTELEMETRY) {
115 if (
116 ! \interface_exists(TracerProviderInterface::class) ||
117 ! \class_exists(NoopTracer::class) ||
118 ! \class_exists(Globals::class)
119 ) {
120 error_log('objectcache.error: Unable to autoload `open-telemetry/api` components');
121
122 return $context;
123 }
124
125 if (! $context) {
126 $context = Globals::tracerProvider();
127 }
128
129 if ($context instanceof TracerProviderInterface) {
130 $context = $this->createOpenTelemetryTracer($context);
131 }
132
133 if ($context instanceof NoopTracer) {
134 return;
135 }
136 }
137
138 return $context;
139 }
140
141 /**
142 * Executes given callback.
143 *
144 * @param \Closure $cb
145 * @param string $method
146 * @return mixed
147 */
148 protected function passthroughCallback(Closure $cb, string $method)
149 {
150 return $cb();
151 }
152
153 /**
154 * Executes given callback on callable.
155 *
156 * @param \Closure $cb
157 * @param string $method
158 * @return mixed
159 */
160 protected function callableCallback(Closure $cb, string $method)
161 {
162 return ($this->context)($cb, $method);
163 }
164
165 /**
166 * Executes given callback as New Relic datastore segment.
167 *
168 * @param \Closure $cb
169 * @param string $method
170 * @return mixed
171 */
172 protected function newRelicCallback(Closure $cb, string $method)
173 {
174 return \newrelic_record_datastore_segment($cb, [
175 'product' => $this->context ?? 'Redis',
176 'operation' => $method,
177 ]);
178 }
179
180 /**
181 * Executes given callback using Open Telemetry tracer.
182 *
183 * @param \Closure $cb
184 * @param string $method
185 * @return mixed
186 */
187 protected function openTelemetryCallback(Closure $cb, string $method)
188 {
189 $span = $this->context->spanBuilder($method)
190 ->setAttribute('db.system', 'redis')
191 ->setSpanKind(SpanKind::KIND_CLIENT)
192 ->startSpan();
193
194 try {
195 return $cb();
196 } catch (Throwable $exception) {
197 $span->recordException($exception);
198
199 throw $exception;
200 } finally {
201 $span->end();
202 }
203 }
204
205 /**
206 * Creates an OpenTelemetry tracer from given tracer provider.
207 *
208 * @param TracerProviderInterface $tracerProvider
209 * @return TracerInterface
210 */
211 abstract protected function createOpenTelemetryTracer(TracerProviderInterface $tracerProvider): TracerInterface;
212}
213