Completed
Push — master ( d71f26...985e99 )
by Mihail
10:44 queued 04:09
created

ClientFactory::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
c 1
b 0
f 0
nc 1
nop 1
dl 0
loc 3
rs 10
1
<?php
2
/*
3
 * This file is part of the Koded package.
4
 *
5
 * (c) Mihail Binev <[email protected]>
6
 *
7
 * Please view the LICENSE distributed with this source code
8
 * for the full copyright and license information.
9
 */
10
11
namespace Koded\Caching\Client;
12
13
use Error;
14
use Exception;
15
use Koded\Caching\{Cache, CacheException};
16
use Koded\Caching\Configuration\{MemcachedConfiguration, PredisConfiguration, RedisConfiguration};
17
use Koded\Stdlib\{Configuration, Serializer};
18
use Koded\Stdlib\Serializer\SerializerFactory;
19
use Psr\Log\{LoggerInterface, NullLogger};
20
21
final class ClientFactory
22
{
23
    public const CACHE_CLIENT = 'CACHE_CLIENT';
24
25
    private Configuration $factory;
26
27
    public function __construct(Configuration $factory)
28
    {
29
        $this->factory = $factory;
30
    }
31
32
    /**
33
     * Create an instance of specific cache client.
34
     *
35
     * @param string $client The required cache client
36
     *                       (memcached, redis, predis, shmop, file, memory)
37
     *
38
     * @return Cache An instance of the cache client
39
     * @throws CacheException
40
     * @throws Exception
41
     */
42
    public function new(string $client = ''): Cache
43
    {
44
        $client = \strtolower($client ?: \getenv(self::CACHE_CLIENT) ?: 'memory');
45
        $config = $this->factory->build($client);
46
47
        return match ($client) {
48
            'memory' => new MemoryClient($config->get('ttl')),
49
            'memcached' => $this->createMemcachedClient($config),
50
            'redis' => $this->createRedisClient($config),
51
            'predis' => $this->createPredisClient($config),
52
            'shmop' => new ShmopClient((string)$config->get('dir'), $config->get('ttl')),
53
            'file' => new FileClient($this->getLogger($config), (string)$config->get('dir'), $config->get('ttl')),
54
            default => throw CacheException::forUnsupportedClient($client)
55
        };
56
    }
57
58
    private function createMemcachedClient(MemcachedConfiguration|Configuration $conf): Cache
59
    {
60
        $client = new \Memcached($conf->get('id'));
61
        $client->setOptions($conf->getOptions());
62
        if (empty($client->getServerList())) {
63
            $client->addServers($conf->getServers());
64
        }
65
        return new MemcachedClient($client, $conf->getTtl());
66
    }
67
68
    private function createRedisClient(RedisConfiguration|Configuration $conf): Cache
69
    {
70
        $serializer = $conf->get('serializer');
71
        $binary = $conf->get('binary');
72
        if (Serializer::JSON === $serializer && $binary) {
73
            return new RedisJsonClient(
74
                $this->newRedisClient($conf),
75
                SerializerFactory::new((string)$binary, ...$conf->get('options', [0])),
76
                (int)$conf->get('options'),
77
                $conf->get('ttl')
78
            );
79
        }
80
        return new RedisClient(
81
            $this->newRedisClient($conf),
82
            SerializerFactory::new($serializer, ...$conf->get('options', [0])),
83
            $conf->get('ttl')
84
        );
85
    }
86
87
    private function createPredisClient(PredisConfiguration|Configuration $conf): Cache
88
    {
89
        $binary = $conf->get('binary');
90
        if (Serializer::JSON === $conf->get('serializer') && $binary) {
91
            return new PredisJsonClient(
92
                $this->newPredisClient($conf),
93
                SerializerFactory::new((string)$binary, ...$conf->get('options', [0])),
94
                (int)$conf->get('options'),
95
                $conf->get('ttl')
96
            );
97
        }
98
        return new PredisClient(
99
            $this->newPredisClient($conf),
100
            SerializerFactory::new($conf->get('serializer'), ...$conf->get('options', [0])),
101
            $conf->get('ttl')
102
        );
103
    }
104
105
    private function newRedisClient(RedisConfiguration $conf): \Redis
106
    {
107
        $client = new \Redis;
108
        try {
109
            @$client->connect(...$conf->getConnectionParams());
0 ignored issues
show
Bug introduced by
$conf->getConnectionParams() is expanded, but the parameter $host of Redis::connect() does not expect variable arguments. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

109
            @$client->connect(/** @scrutinizer ignore-type */ ...$conf->getConnectionParams());
Loading history...
Security Best Practice introduced by
It seems like you do not handle an error condition for connect(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

109
            /** @scrutinizer ignore-unhandled */ @$client->connect(...$conf->getConnectionParams());

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
110
            $client->setOption(\Redis::OPT_SERIALIZER, $conf->get('type'));
111
            $client->setOption(\Redis::OPT_PREFIX, $conf->get('prefix'));
112
            $client->select((int)$conf->get('db'));
113
            if ($auth = $conf->get('auth')) {
114
                $client->auth($auth);
115
            }
116
117
        } /** @noinspection PhpRedundantCatchClauseInspection */
118
        catch (\RedisException $e) {
119
            if (!\strpos($e->getMessage(), ' AUTH ')) {
120
                \error_log(sprintf(PHP_EOL . '[Redis] %s: %s', \get_class($e), $e->getMessage()));
121
                \error_log('[Redis] with conf: ' . $conf->toJSON());
122
                throw CacheException::withConnectionErrorFor('Redis');
123
            }
124
        } catch (Exception | Error $e) {
125
            throw CacheException::from($e);
126
        }
127
        return $client;
128
    }
129
130
    private function newPredisClient(PredisConfiguration $conf): \Predis\Client
131
    {
132
        $client = new \Predis\Client($conf->getConnectionParams(), $conf->getOptions());
133
134
        try {
135
            $client->connect();
136
            $client->select((int)$conf->get('db'));
137
            if ($auth = $conf->get('auth')) {
138
                $client->auth($auth);
139
            }
140
141
        } /** @noinspection PhpRedundantCatchClauseInspection */
142
        catch (\Predis\Connection\ConnectionException $e) {
143
            if (!\strpos($e->getMessage(), ' AUTH ')) {
144
                \error_log(sprintf(PHP_EOL . '[Predis] %s: %s', \get_class($e), $e->getMessage()));
145
                \error_log('[Predis] with conf: ' . $conf->toJSON());
146
                throw CacheException::withConnectionErrorFor('Predis');
147
            }
148
        } catch (Exception $e) {
149
            throw CacheException::from($e);
150
        }
151
        return $client;
152
    }
153
154
    private function getLogger(Configuration $conf): LoggerInterface
155
    {
156
        $logger = $conf->logger ?? new NullLogger;
0 ignored issues
show
Bug introduced by
Accessing logger on the interface Koded\Stdlib\Configuration suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
157
        if ($logger instanceof LoggerInterface) {
158
            return $logger;
159
        }
160
        throw CacheException::forUnsupportedLogger(LoggerInterface::class, \get_class($logger));
161
    }
162
}
163