Issues (58)

Client/ClientFactory.php (3 issues)

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 Exception;
14
use Koded\Caching\{Cache, CacheException};
15
use Memcached;
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
use Redis;
21
use Throwable;
22
use function error_log;
23
use function getenv;
24
use function sprintf;
25
use function strtolower;
26
27
final class ClientFactory
28
{
29
    public const CACHE_CLIENT = 'CACHE_CLIENT';
30
31
    public function __construct(private Configuration $factory) {}
32
33
    /**
34
     * Create an instance of specific cache client.
35
     *
36
     * @param string $client The required cache client
37
     *                       (memcached, redis, predis, shmop, file, memory)
38
     *
39
     * @return Cache An instance of the cache client
40
     * @throws CacheException
41
     * @throws Exception
42
     */
43 560
    public function new(string $client = ''): Cache
44
    {
45 560
        $client = strtolower($client ?: getenv(self::CACHE_CLIENT) ?: 'memory');
46 560
        $config = $this->factory->build($client);
47
48 560
        return match ($client) {
49 560
            'memory' => new MemoryClient($config->get('ttl')),
50 560
            'memcached' => $this->createMemcachedClient($config),
51 560
            'redis' => $this->createRedisClient($config),
52 560
            'predis' => $this->createPredisClient($config),
53 560
            'shmop' => new ShmopClient((string)$config->get('dir'), $config->get('ttl')),
54 560
            'file' => new FileClient($this->getLogger($config), (string)$config->get('dir'), $config->get('ttl')),
55 560
            default => throw CacheException::forUnsupportedClient($client)
56 560
        };
57
    }
58
59 70
    private function createMemcachedClient(MemcachedConfiguration|Configuration $conf): Cache
60
    {
61 70
        $client = new Memcached($conf->get('id'));
62 70
        $client->setOptions($conf->getOptions());
63 70
        if (empty($client->getServerList())) {
64 70
            $client->addServers($conf->getServers());
65
        }
66 70
        return new MemcachedClient($client, $conf->getTtl());
67
    }
68
69 155
    private function createRedisClient(RedisConfiguration|Configuration $conf): Cache
70
    {
71 155
        $serializer = $conf->get('serializer');
72 155
        $binary = $conf->get('binary');
73 155
        if (Serializer::JSON === $serializer && $binary) {
74 71
            return new RedisJsonClient(
75 71
                $this->newRedisClient($conf),
76 71
                SerializerFactory::new((string)$binary, ...$conf->get('options', [0])),
77 71
                (int)$conf->get('options'),
78 71
                $conf->get('ttl')
79 71
            );
80
        }
81 84
        return new RedisClient(
82 84
            $this->newRedisClient($conf),
83 84
            SerializerFactory::new($serializer, ...$conf->get('options', [0])),
84 84
            $conf->get('ttl')
85 84
        );
86
    }
87
88 135
    private function createPredisClient(PredisConfiguration|Configuration $conf): Cache
89
    {
90 135
        $binary = $conf->get('binary');
91 135
        if (Serializer::JSON === $conf->get('serializer') && $binary) {
92 71
            return new PredisJsonClient(
93 71
                $this->newPredisClient($conf),
94 71
                SerializerFactory::new((string)$binary, ...$conf->get('options', [0])),
95 71
                (int)$conf->get('options'),
96 71
                $conf->get('ttl')
97 71
            );
98
        }
99 64
        return new PredisClient(
100 64
            $this->newPredisClient($conf),
101 64
            SerializerFactory::new($conf->get('serializer'), ...$conf->get('options', [0])),
102 64
            $conf->get('ttl')
103 64
        );
104
    }
105
106 155
    private function newRedisClient(RedisConfiguration $conf): Redis
107
    {
108 155
        $client = new Redis;
109
        try {
110 155
            @$client->connect(...$conf->getConnectionParams());
0 ignored issues
show
$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

110
            @$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

110
            /** @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...
111 154
            $client->setOption(Redis::OPT_SERIALIZER, $conf->get('type'));
112 154
            $client->setOption(Redis::OPT_PREFIX, $conf->get('prefix'));
113 153
            $client->select((int)$conf->get('db'));
114 153
            if ($auth = $conf->get('auth')) {
115 1
                $client->auth($auth);
116
            }
117 152
            if ($message = $client->getLastError()) {
118
                // [NOTE] Redis module complains if auth is set,
119
                // but <Redis v5 or less> does not have auth
120
                throw new Exception($message);
121
            }
122 152
            return $client;
123
124
        } /** @noinspection PhpRedundantCatchClauseInspection */
125 3
        catch (Throwable $e) {
126 3
            error_log(sprintf(PHP_EOL . '[Redis] %s: %s', $e::class, $e->getMessage()));
127 3
            error_log('[Redis] with conf: ' . $conf->delete('auth')->toJSON());
128 3
            throw CacheException::withConnectionErrorFor('Redis');
129
        }
130
    }
131
132 135
    private function newPredisClient(PredisConfiguration $conf): \Predis\Client
133
    {
134 135
        $client = new \Predis\Client($conf->getConnectionParams(), $conf->getOptions());
135
136
        try {
137 135
            $client->connect();
138 134
            $client->select((int)$conf->get('db'));
139 134
            if ($auth = $conf->get('auth')) {
140 1
                $client->auth($auth);
141
            }
142 133
            return $client;
143
144
        } /** @noinspection PhpRedundantCatchClauseInspection */
145 2
        catch (Throwable $e) {
146 2
            error_log(sprintf(PHP_EOL . '[Predis] %s: %s', $e::class, $e->getMessage()));
147 2
            error_log('[Predis] with conf: ' . $conf->delete('auth')->toJSON());
148 2
            throw CacheException::withConnectionErrorFor('Predis');
149
        }
150
    }
151
152 63
    private function getLogger(Configuration $conf): LoggerInterface
153
    {
154 63
        $logger = $conf->logger ?? new NullLogger;
0 ignored issues
show
Accessing logger on the interface Koded\Stdlib\Configuration suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
155 63
        if ($logger instanceof LoggerInterface) {
156 62
            return $logger;
157
        }
158 1
        throw CacheException::forUnsupportedLogger(LoggerInterface::class, $logger::class);
159
    }
160
}
161