Completed
Push — master ( 3d22df...95226d )
by Georges
22s queued 12s
created

Driver::getStats()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 25
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 17
nc 1
nop 0
dl 0
loc 25
rs 9.7
c 1
b 0
f 0
1
<?php
2
3
/**
4
 *
5
 * This file is part of Phpfastcache.
6
 *
7
 * @license MIT License (MIT)
8
 *
9
 * For full copyright and license information, please see the docs/CREDITS.txt and LICENCE files.
10
 *
11
 * @author Georges.L (Geolim4)  <[email protected]>
12
 * @author Contributors  https://github.com/PHPSocialNetwork/phpfastcache/graphs/contributors
13
 */
14
declare(strict_types=1);
15
16
namespace Phpfastcache\Drivers\Dynamodb;
17
18
use Aws\Sdk as AwsSdk;
0 ignored issues
show
Bug introduced by
The type Aws\Sdk was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
19
use Aws\DynamoDb\DynamoDbClient as AwsDynamoDbClient;
0 ignored issues
show
Bug introduced by
The type Aws\DynamoDb\DynamoDbClient was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
20
use Aws\DynamoDb\Marshaler as AwsMarshaler;
0 ignored issues
show
Bug introduced by
The type Aws\DynamoDb\Marshaler was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
21
use Aws\DynamoDb\Exception\DynamoDbException as AwsDynamoDbException;
0 ignored issues
show
Bug introduced by
The type Aws\DynamoDb\Exception\DynamoDbException was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
22
23
use Phpfastcache\Cluster\AggregatablePoolInterface;
24
use Phpfastcache\Core\Item\ExtendedCacheItemInterface;
25
use Phpfastcache\Core\Pool\ExtendedCacheItemPoolInterface;
26
use Phpfastcache\Core\Pool\TaggableCacheItemPoolTrait;
27
use Phpfastcache\Entities\DriverStatistic;
28
use Phpfastcache\Event\EventReferenceParameter;
29
use Phpfastcache\Exceptions\PhpfastcacheDriverConnectException;
30
use Phpfastcache\Exceptions\PhpfastcacheDriverException;
31
use Phpfastcache\Exceptions\PhpfastcacheInvalidArgumentException;
32
use Phpfastcache\Exceptions\PhpfastcacheLogicException;
33
use Psr\Http\Message\UriInterface;
0 ignored issues
show
Bug introduced by
The type Psr\Http\Message\UriInterface was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
34
35
/**
36
 * Class Driver
37
 * @property Config $config
38
 * @property AwsDynamoDbClient $instance
39
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
40
 */
41
class Driver implements ExtendedCacheItemPoolInterface, AggregatablePoolInterface
42
{
43
    use TaggableCacheItemPoolTrait;
44
45
    protected const TTL_FIELD_NAME = 't';
46
47
    protected AwsSdk $awsSdk;
48
49
    protected AwsMarshaler $marshaler;
50
51
    /**
52
     * @return bool
53
     */
54
    public function driverCheck(): bool
55
    {
56
        return \class_exists(AwsSdk::class) && \class_exists(AwsDynamoDbClient::class);
57
    }
58
59
    /**
60
     * @return bool
61
     * @throws PhpfastcacheDriverConnectException
62
     * @throws PhpfastcacheDriverException
63
     * @throws PhpfastcacheLogicException
64
     * @throws PhpfastcacheInvalidArgumentException
65
     */
66
    protected function driverConnect(): bool
67
    {
68
        $wsAccessKey = $this->getConfig()->getSuperGlobalAccessor()('SERVER', 'AWS_ACCESS_KEY_ID');
69
        $awsSecretKey = $this->getConfig()->getSuperGlobalAccessor()('SERVER', 'AWS_SECRET_ACCESS_KEY');
70
71
        if (empty($wsAccessKey)) {
72
            throw new PhpfastcacheDriverConnectException('The environment configuration AWS_ACCESS_KEY_ID must be set');
73
        }
74
75
        if (empty($awsSecretKey)) {
76
            throw new PhpfastcacheDriverConnectException('The environment configuration AWS_SECRET_ACCESS_KEY must be set');
77
        }
78
79
        $this->awsSdk = new AwsSdk([
80
            'endpoint'   => $this->getConfig()->getEndpoint(),
81
            'region'   => $this->getConfig()->getRegion(),
82
            'version'  => $this->getConfig()->getVersion(),
83
            'debug'  => $this->getConfig()->isDebugEnabled(),
84
        ]);
85
        $this->instance = $this->awsSdk->createDynamoDb();
86
        $this->marshaler = new AwsMarshaler();
87
88
        if (!$this->hasTable()) {
89
            $this->createTable();
90
        }
91
92
        if (!$this->hasTtlEnabled()) {
93
            $this->enableTtl();
94
        }
95
96
        return true;
97
    }
98
99
    /**
100
     * @param ExtendedCacheItemInterface $item
101
     * @return bool
102
     * @throws PhpfastcacheLogicException
103
     */
104
    protected function driverWrite(ExtendedCacheItemInterface $item): bool
105
    {
106
        $awsItem = $this->marshaler->marshalItem(
107
            \array_merge(
108
                $this->encodeDocument($this->driverPreWrap($item, true)),
109
                ['t' => $item->getExpirationDate()->getTimestamp()]
110
            )
111
        );
112
113
        $result = $this->instance->putItem([
114
            'TableName' => $this->getConfig()->getTable(),
115
            'Item' => $awsItem
116
        ]);
117
118
        return ($result->get('@metadata')['statusCode'] ?? null) === 200;
119
    }
120
121
    /**
122
     * @param ExtendedCacheItemInterface $item
123
     * @return null|array
124
     * @throws \Exception
125
     */
126
    protected function driverRead(ExtendedCacheItemInterface $item): ?array
127
    {
128
        $key = $this->marshaler->marshalItem([
129
            $this->getConfig()->getPartitionKey() => $item->getKey()
130
        ]);
131
132
        $result = $this->instance->getItem([
133
            'TableName' => $this->getConfig()->getTable(),
134
            'Key' => $key
135
        ]);
136
137
        $awsItem = $result->get('Item');
138
139
        if ($awsItem !== null) {
140
            return $this->decodeDocument(
141
                $this->marshaler->unmarshalItem($awsItem)
142
            );
143
        }
144
145
        return null;
146
    }
147
148
    /**
149
     * @param ExtendedCacheItemInterface $item
150
     * @return bool
151
     */
152
    protected function driverDelete(ExtendedCacheItemInterface $item): bool
153
    {
154
        $key = $this->marshaler->marshalItem([
155
            $this->getConfig()->getPartitionKey() => $item->getKey()
156
        ]);
157
158
        $result = $this->instance->deleteItem([
159
            'TableName' => $this->getConfig()->getTable(),
160
            'Key' => $key
161
        ]);
162
163
        return ($result->get('@metadata')['statusCode'] ?? null) === 200;
164
    }
165
166
    /**
167
     * @return bool
168
     * @throws PhpfastcacheDriverException
169
     */
170
    protected function driverClear(): bool
171
    {
172
        $params = [
173
            'TableName' => $this->getConfig()->getTable(),
174
        ];
175
176
        $result = $this->instance->deleteTable($params);
177
178
        $this->instance->waitUntil('TableNotExists', $params);
179
180
        $this->createTable();
181
        $this->enableTtl();
182
183
        return ($result->get('@metadata')['statusCode'] ?? null) === 200;
184
    }
185
186
    protected function hasTable(): bool
187
    {
188
        return \count($this->instance->listTables(['TableNames' => [$this->getConfig()->getTable()]])->get('TableNames')) > 0;
189
    }
190
191
    protected function createTable() :void
192
    {
193
        $params = [
194
            'TableName' => $this->getConfig()->getTable(),
195
            'KeySchema' => [
196
                [
197
                    'AttributeName' => $this->getConfig()->getPartitionKey(),
198
                    'KeyType' => 'HASH'
199
                ]
200
            ],
201
            'AttributeDefinitions' => [
202
                [
203
                    'AttributeName' => $this->getConfig()->getPartitionKey(),
204
                    'AttributeType' => 'S'
205
                ],
206
            ],
207
            'ProvisionedThroughput' => [
208
                'ReadCapacityUnits' => 10,
209
                'WriteCapacityUnits' => 10
210
            ]
211
        ];
212
213
        $this->eventManager->dispatch('DynamodbCreateTable', $this, new EventReferenceParameter($params));
214
215
        $this->instance->createTable($params);
216
        $this->instance->waitUntil('TableExists', $params);
217
    }
218
219
    protected function hasTtlEnabled(): bool
220
    {
221
        $ttlDesc = $this->instance->describeTimeToLive(['TableName' => $this->getConfig()->getTable()])->get('TimeToLiveDescription');
222
223
        if (!isset($ttlDesc['AttributeName'], $ttlDesc['TimeToLiveStatus'])) {
224
            return false;
225
        }
226
227
        return $ttlDesc['TimeToLiveStatus'] === 'ENABLED' && $ttlDesc['AttributeName'] === self::TTL_FIELD_NAME;
228
    }
229
230
    /**
231
     * @throws PhpfastcacheDriverException
232
     */
233
    protected function enableTtl(): void
234
    {
235
        try {
236
            $this->instance->updateTimeToLive([
237
                'TableName' => $this->getConfig()->getTable(),
238
                'TimeToLiveSpecification' => [
239
                    "AttributeName" => self::TTL_FIELD_NAME,
240
                    "Enabled" => true
241
                ],
242
            ]);
243
        } catch (AwsDynamoDbException $e) {
244
            /**
245
             * Error 400 can be an acceptable error of a
246
             * Dynamodb restriction: "Time to live has been modified multiple times within a fixed interval"
247
             * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_UpdateTimeToLive.html
248
             */
249
            if ($e->getStatusCode() !== 400) {
250
                throw new PhpfastcacheDriverException(
251
                    'Failed to enable TTL with the following error: ' . $e->getMessage()
252
                );
253
            }
254
        }
255
    }
256
257
    public function getStats(): DriverStatistic
258
    {
259
        /** @var UriInterface $endpoint */
260
        $endpoint = $this->instance->getEndpoint();
261
        $table = $this->instance->describeTable(['TableName' => $this->getConfig()->getTable()])->get('Table');
262
263
        $info = \sprintf(
264
            'Dynamo server "%s" | Table "%s" with %d item(s) stored',
265
            $endpoint->getHost(),
266
            $table['TableName'] ?? 'Unknown table name',
267
            $table['ItemCount'] ?? 'Unknown item count',
268
        );
269
270
        $data = [
271
            'dynamoEndpoint' => $endpoint,
272
            'dynamoTable' => $table,
273
            'dynamoConfig' => $this->instance->getConfig(),
274
            'dynamoApi' => $this->instance->getApi()->toArray(),
275
        ];
276
277
        return (new DriverStatistic())
278
            ->setData(implode(', ', array_keys($this->itemInstances)))
279
            ->setInfo($info)
280
            ->setRawData($data)
281
            ->setSize($data['dynamoTable']['TableSizeBytes'] ?? 0);
282
    }
283
284
    protected function encodeDocument(array $data): array
285
    {
286
        $data[self::DRIVER_DATA_WRAPPER_INDEX] = $this->encode($data[self::DRIVER_DATA_WRAPPER_INDEX]);
287
288
        return $data;
289
    }
290
291
    protected function decodeDocument(array $data): array
292
    {
293
        $data[self::DRIVER_DATA_WRAPPER_INDEX] = $this->decode($data[self::DRIVER_DATA_WRAPPER_INDEX]);
294
295
        return $data;
296
    }
297
298
    public function getConfig(): Config
299
    {
300
        return $this->config;
301
    }
302
}
303