Passed
Push — v9 ( 555fde...51468f )
by Georges
01:56
created

Driver::hasTable()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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