├── Dockerfile ├── LICENSE ├── README.md ├── assets ├── css │ └── styles.css ├── favicon.png ├── icons │ ├── close.svg │ ├── dashboards │ │ ├── apcu.svg │ │ ├── memcached.svg │ │ ├── opcache.svg │ │ ├── realpath.svg │ │ ├── redis.svg │ │ └── server.svg │ ├── down.svg │ ├── edit.svg │ ├── export.svg │ ├── github.svg │ ├── import.svg │ ├── logo.svg │ ├── logout.svg │ ├── moon.svg │ ├── plus.svg │ ├── save.svg │ ├── search.svg │ ├── sun.svg │ ├── system.svg │ ├── tableview.svg │ ├── trash.svg │ └── treeview.svg └── js │ └── scripts.js ├── composer.json ├── config.dist.php ├── docker-compose.yml ├── index.php ├── predis.phar ├── src ├── Admin.php ├── Config.php ├── Dashboards │ ├── APCu │ │ ├── APCuDashboard.php │ │ └── APCuTrait.php │ ├── DashboardException.php │ ├── DashboardInterface.php │ ├── Memcached │ │ ├── MemcachedDashboard.php │ │ ├── MemcachedException.php │ │ ├── MemcachedTrait.php │ │ └── PHPMem.php │ ├── OPCache │ │ ├── OPCacheDashboard.php │ │ └── OPCacheTrait.php │ ├── Realpath │ │ ├── RealpathDashboard.php │ │ └── RealpathTrait.php │ ├── Redis │ │ ├── Compatibility │ │ │ ├── Cluster │ │ │ │ └── RedisCluster.php │ │ │ ├── Predis.php │ │ │ ├── Redis.php │ │ │ ├── RedisCompatibilityInterface.php │ │ │ ├── RedisJson.php │ │ │ └── RedisModules.php │ │ ├── RedisDashboard.php │ │ ├── RedisTrait.php │ │ └── RedisTypes.php │ └── Server │ │ ├── ServerDashboard.php │ │ └── ServerTrait.php ├── Format.php ├── Helpers.php ├── Http.php ├── Paginator.php ├── Template.php ├── Value.php └── functions.php ├── templates ├── components │ ├── alert.twig │ ├── badge.twig │ ├── button.twig │ ├── checkbox.twig │ ├── input.twig │ ├── modal.twig │ ├── paginator.twig │ ├── select.twig │ ├── tabs.twig │ └── textarea.twig ├── dashboards │ ├── apcu.twig │ ├── memcached.twig │ ├── opcache.twig │ ├── realpath.twig │ ├── redis │ │ ├── form.twig │ │ └── redis.twig │ └── server.twig ├── layout.twig └── partials │ ├── form.twig │ ├── import_form.twig │ ├── info.twig │ ├── info_table.twig │ ├── key_type_badge.twig │ ├── keys_list.twig │ ├── panel.twig │ ├── table_view.twig │ ├── tree_view.twig │ ├── view_key.twig │ └── view_key_array.twig └── twig.phar /Dockerfile: -------------------------------------------------------------------------------- 1 | # Build stage 2 | FROM php:8.4-cli-alpine AS builder 3 | 4 | RUN apk add --no-cache --virtual .build-deps autoconf build-base git \ 5 | && pecl install -o -f redis \ 6 | && docker-php-ext-enable redis \ 7 | && rm -rf /tmp/pear 8 | 9 | WORKDIR /app 10 | RUN git clone --depth=1 https://github.com/RobiNN1/phpCacheAdmin.git . \ 11 | && rm -rf .git tests composer.json package.json phpstan.neon phpunit.xml README.md docker-compose.yml Dockerfile \ 12 | && apk del .build-deps 13 | 14 | # Final stage 15 | FROM php:8.4-fpm-alpine 16 | 17 | COPY --from=builder /usr/local/lib/php/extensions /usr/local/lib/php/extensions 18 | COPY --from=builder /usr/local/etc/php/conf.d /usr/local/etc/php/conf.d 19 | COPY --from=builder /app /var/www/html 20 | 21 | # NGINX configuration is here intentionally 22 | RUN apk add --no-cache nginx \ 23 | && mkdir -p /var/www/html/tmp \ 24 | && chown -R nobody:nobody /var/www/html \ 25 | && chmod -R 777 /var/www/html \ 26 | && mkdir -p /run/nginx \ 27 | && echo 'server { \ 28 | listen 80; \ 29 | root /var/www/html; \ 30 | index index.php; \ 31 | location / { \ 32 | try_files $uri $uri/ /index.php?$query_string; \ 33 | } \ 34 | location ~ \.php$ { \ 35 | fastcgi_pass 127.0.0.1:9000; \ 36 | fastcgi_index index.php; \ 37 | fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; \ 38 | include fastcgi_params; \ 39 | } \ 40 | }' > /etc/nginx/http.d/default.conf 41 | 42 | EXPOSE 80 43 | 44 | CMD php-fpm -D && nginx -g 'daemon off;' 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Róbert Kelčák 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Logo

2 |

Web GUI for managing Redis, Memcached, APCu, OPCache and Realpath with data management.

3 |

Preview

4 | 5 | ![Visitor Badge](https://visitor-badge.laobi.icu/badge?page_id=RobiNN1.phpCacheAdmin) 6 | ![Docker Pulls](https://img.shields.io/docker/pulls/robinn/phpcacheadmin) 7 | 8 | ## Installation 9 | 10 | Unzip the archive and launch index.php in a web browser. No installation is required. 11 | However, it is highly recommended (although not required) to run `composer install`. 12 | 13 | If you use the defaults (e.g. Redis, Memcached servers), everything should work out of the box. 14 | To customize the configuration, do not edit `config.dist.php` directly, but copy it into `config.php`. 15 | 16 | ## Updating 17 | 18 | Replace all files and delete the `tmp` folder (this folder contains only compiled Twig templates). 19 | 20 | ## Environment variables 21 | 22 | All keys from the [config](https://github.com/RobiNN1/phpCacheAdmin/blob/master/config.dist.php) file are supported ENV variables, 23 | they just must start with `PCA_` prefix. 24 | 25 | Options with an array can be set using "dot notation" but use `_` instead of a dot. 26 | Or you can even use JSON (e.g. Redis SSL option). 27 | 28 | Redis: 29 | 30 | - `PCA_REDIS_0_NAME` The server name (optional). 31 | - `PCA_REDIS_0_HOST` Optional when a path or nodes is specified. 32 | - `PCA_REDIS_0_NODES` List of cluster nodes. You can set value as JSON `["127.0.0.1:7000","127.0.0.1:7001","127.0.0.1:7002"]`. 33 | - `PCA_REDIS_0_PORT` Optional when the default port is used. 34 | - `PCA_REDIS_0_SCHEME` Connection scheme (optional). If you need a TLS connection, set it to `tls`. 35 | - `PCA_REDIS_0_SSL` [SSL options](https://www.php.net/manual/en/context.ssl.php) for TLS. Requires Redis >= 6.0 (optional). You can set value as JSON `{"cafile":"private.pem","verify_peer":true}`. 36 | - `PCA_REDIS_0_DATABASE` Default database (optional). 37 | - `PCA_REDIS_0_USERNAME` ACL - requires Redis >= 6.0 (optional). 38 | - `PCA_REDIS_0_PASSWORD` Optional. 39 | - `PCA_REDIS_0_AUTHFILE` File with a password, e.g. Docker secrets (optional). 40 | - `PCA_REDIS_0_PATH` Unix domain socket (optional). 41 | - `PCA_REDIS_0_DATABASES` Number of databases, use this if the CONFIG command is disabled (optional). 42 | - `PCA_REDIS_0_SCANSIZE` Number of keys, the server will use the SCAN command instead of KEYS (optional). 43 | 44 | Memcached: 45 | 46 | - `PCA_MEMCACHED_0_NAME` The server name (optional). 47 | - `PCA_MEMCACHED_0_HOST` Optional when a path is specified. 48 | - `PCA_MEMCACHED_0_PORT` Optional when the default port is used. 49 | - `PCA_MEMCACHED_0_PATH` Unix domain socket (optional). 50 | 51 | Open [config](https://github.com/RobiNN1/phpCacheAdmin/blob/master/config.dist.php) file for more info. 52 | 53 | > To add another server, add the same environment variables, but change `0` to `1` (`2` for third server and so on). 54 | 55 | ## Docker 56 | 57 | A Docker image is also available: https://hub.docker.com/r/robinn/phpcacheadmin 58 | 59 | Run with single command: 60 | 61 | ```bash 62 | docker run -p 8080:80 -d --name phpcacheadmin -e "PCA_REDIS_0_HOST=redis_host" -e "PCA_REDIS_0_PORT=6379" -e "PCA_MEMCACHED_0_HOST=memcached_host" -e "PCA_MEMCACHED_0_PORT=11211" robinn/phpcacheadmin 63 | ``` 64 | 65 | Or use it in **docker-compose.yml** 66 | 67 | ```yaml 68 | version: '3' 69 | services: 70 | phpcacheadmin: 71 | image: robinn/phpcacheadmin 72 | ports: 73 | - "8080:80" 74 | #volumes: 75 | # If you want to use config.php instead of ENV variables 76 | # - "./config.php:/var/www/html/config.php" 77 | environment: 78 | - PCA_REDIS_0_HOST=redis 79 | - PCA_REDIS_0_PORT=6379 80 | - PCA_MEMCACHED_0_HOST=memcached 81 | - PCA_MEMCACHED_0_PORT=11211 82 | links: 83 | - redis 84 | - memcached 85 | redis: 86 | image: redis 87 | memcached: 88 | image: memcached 89 | ``` 90 | 91 | ## Requirements 92 | 93 | - PHP >= 8.2 (Use [v1 branch](https://github.com/RobiNN1/phpCacheAdmin/tree/v1.x) if you need support for >=7.4) 94 | - Redis server >= 3.0.0 95 | - Memcached server >= 1.4.31 (ideally >= 1.5.19 to see more data). If you do not see the keys, you need to enable `lru_crawler`. SASL is not supported because there is no way to get the keys. 96 | 97 | > It is not necessary to have all dashboards enabled. 98 | 99 | ## Custom Dashboards 100 | 101 | - [FileCache](https://github.com/RobiNN1/FileCache-Dashboard) ([`robinn/cache`](https://github.com/RobiNN1/Cache)) dashboard. 102 | 103 | ## Contributing 104 | 105 | Thank you for your interest in contributing! However, this project is not accepting pull requests as I prefer to maintain the codebase myself. 106 | 107 | If you encounter any issues or have suggestions for improvements, please create an issue instead. This allows us to discuss the problem or idea before any changes are made. 108 | 109 | I appreciate your understanding and look forward to hearing your feedback! 110 | 111 | 112 | -------------------------------------------------------------------------------- /assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RobiNN1/phpCacheAdmin/f68bc366231ed6665fe94aa6d8f0d70dab974633/assets/favicon.png -------------------------------------------------------------------------------- /assets/icons/close.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /assets/icons/dashboards/apcu.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /assets/icons/dashboards/memcached.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /assets/icons/dashboards/opcache.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /assets/icons/dashboards/realpath.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /assets/icons/dashboards/redis.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /assets/icons/dashboards/server.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /assets/icons/down.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /assets/icons/edit.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /assets/icons/export.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /assets/icons/github.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /assets/icons/import.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /assets/icons/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /assets/icons/logout.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /assets/icons/moon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /assets/icons/plus.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /assets/icons/save.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /assets/icons/search.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /assets/icons/sun.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /assets/icons/system.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /assets/icons/tableview.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /assets/icons/trash.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /assets/icons/treeview.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "robinn/phpcacheadmin", 3 | "description": "A web dashboard for your favorite caching system.", 4 | "license": "MIT", 5 | "type": "project", 6 | "keywords": [ 7 | "redis", 8 | "memcache", 9 | "memcached", 10 | "opcache", 11 | "apcu", 12 | "realpath", 13 | "cache", 14 | "admin", 15 | "dashboard", 16 | "gui", 17 | "phpcacheadmin" 18 | ], 19 | "authors": [ 20 | { 21 | "name": "Róbert Kelčák", 22 | "email": "robo@kelcak.com", 23 | "homepage": "https://kelcak.com" 24 | } 25 | ], 26 | "require": { 27 | "php": ">=8.2", 28 | "twig/twig": "^3.21" 29 | }, 30 | "require-dev": { 31 | "clue/phar-composer": "^1.4", 32 | "phpstan/phpstan": "^2.1", 33 | "phpunit/phpunit": "^11|^12.1" 34 | }, 35 | "suggest": { 36 | "ext-apcu": "Required for the APCu dashboard.", 37 | "ext-zend-opcache": "Required for use the OPCache dashboard.", 38 | "ext-redis": "Required for use the Redis dashboard.", 39 | "ext-zlib": "Required for encoding/decoding with gz* functions.", 40 | "predis/predis": "Required for use the Redis dashboard, when Redis extension is not installed." 41 | }, 42 | "minimum-stability": "dev", 43 | "prefer-stable": true, 44 | "autoload": { 45 | "psr-4": { 46 | "RobiNN\\Pca\\": "src/" 47 | } 48 | }, 49 | "autoload-dev": { 50 | "psr-4": { 51 | "Tests\\": "tests/" 52 | }, 53 | "files": [ 54 | "src/functions.php" 55 | ] 56 | }, 57 | "scripts": { 58 | "phar:twig": "phar-composer build twig/twig", 59 | "phar:predis": "phar-composer build predis/predis", 60 | "phpstan": "phpstan --ansi", 61 | "test": "phpunit --colors=always --display-skipped" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /config.dist.php: -------------------------------------------------------------------------------- 1 | [ 21 | RobiNN\Pca\Dashboards\Server\ServerDashboard::class, 22 | RobiNN\Pca\Dashboards\Redis\RedisDashboard::class, 23 | RobiNN\Pca\Dashboards\Memcached\MemcachedDashboard::class, 24 | RobiNN\Pca\Dashboards\OPCache\OPCacheDashboard::class, 25 | RobiNN\Pca\Dashboards\APCu\APCuDashboard::class, 26 | RobiNN\Pca\Dashboards\Realpath\RealpathDashboard::class, 27 | ], 28 | 'redis' => [ 29 | [ 30 | 'name' => 'Localhost', // The server name (optional). 31 | 'host' => '127.0.0.1', // Optional when a path or nodes is specified. 32 | /*'nodes' => [ 33 | // List of cluster nodes. 34 | '127.0.0.1:7000', 35 | '127.0.0.1:7001', 36 | '127.0.0.1:7002', 37 | ],*/ 38 | 'port' => 6379, // Optional when the default port is used. 39 | //'scheme' => 'tls', // Connection scheme (optional). 40 | /*'ssl' => [ 41 | // SSL options for TLS https://www.php.net/manual/en/context.ssl.php - requires Redis >= 6.0 (optional). 42 | 'cafile' => 'private.pem', 43 | 'verify_peer' => true, 44 | ],*/ 45 | //'database' => 0, // Default database (optional). 46 | //'username' => '', // ACL - requires Redis >= 6.0 (optional). 47 | //'password' => '', // Optional. 48 | //'authfile' => '/run/secrets/file_name', // File with a password, e.g. Docker secrets (optional). 49 | //'path' => '/var/run/redis/redis-server.sock', // Unix domain socket (optional). 50 | //'databases' => 16, // Number of databases, use this if the CONFIG command is disabled (optional). 51 | //'scansize' => 1000, // Number of keys, the server will use the SCAN command instead of KEYS (optional). 52 | //'separator' => ':', // Separator for tree view (optional) 53 | ], 54 | ], 55 | 'memcached' => [ 56 | [ 57 | 'name' => 'Localhost', // The server name, optional. 58 | 'host' => '127.0.0.1', // Optional when a path is specified. 59 | 'port' => 11211, // Optional when the default port is used. 60 | //'path' => '/var/run/memcached/memcached.sock', // Unix domain socket (optional). 61 | //'separator' => ':', // Separator for tree view (optional) 62 | ], 63 | ], 64 | //'apcu-separator' => ':', // Separator for tree view (optional) 65 | // Example of authentication with http auth. 66 | /*'auth' => static function (): void { 67 | $username = 'admin'; 68 | $password = 'pass'; 69 | 70 | if ( 71 | !isset($_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW']) || 72 | $_SERVER['PHP_AUTH_USER'] !== $username || $_SERVER['PHP_AUTH_PW'] !== $password 73 | ) { 74 | header('WWW-Authenticate: Basic realm="phpCacheAdmin Login"'); 75 | header('HTTP/1.0 401 Unauthorized'); 76 | 77 | exit('Incorrect username or password!'); 78 | } 79 | 80 | // Use this section for the logout. It will display a link in the sidebar. 81 | if (isset($_GET['logout'])) { 82 | $is_https = ( 83 | (isset($_SERVER['HTTPS']) && ($_SERVER['HTTPS'] === 'on' || $_SERVER['HTTPS'] === 1)) || 84 | (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') 85 | ); 86 | 87 | header('Location: http'.($is_https ? 's' : '').'://reset:reset@'.($_SERVER['HTTP_HOST'].$_SERVER['REQUEST_URI'])); 88 | } 89 | },*/ 90 | // Decoding / Encoding functions 91 | 'converters' => [ 92 | 'gzcompress' => [ 93 | 'view' => static fn (string $value): ?string => @gzuncompress($value) !== false ? gzuncompress($value) : null, 94 | 'save' => static fn (string $value): string => gzcompress($value), 95 | ], 96 | 'gzencode' => [ 97 | 'view' => static fn (string $value): ?string => @gzdecode($value) !== false ? gzdecode($value) : null, 98 | 'save' => static fn (string $value): string => gzencode($value), 99 | ], 100 | 'gzdeflate' => [ 101 | 'view' => static fn (string $value): ?string => @gzinflate($value) !== false ? gzinflate($value) : null, 102 | 'save' => static fn (string $value): string => gzdeflate($value), 103 | ], 104 | 'zlib' => [ 105 | 'view' => static fn (string $value): ?string => @zlib_decode($value) !== false ? zlib_decode($value) : null, 106 | 'save' => static fn (string $value): string => zlib_encode($value, ZLIB_ENCODING_DEFLATE), 107 | ], 108 | /*'gz_magento' => [ 109 | 'view' => static function (string $value): ?string { 110 | // https://github.com/colinmollenhour/Cm_Cache_Backend_Redis/blob/master/Cm/Cache/Backend/Redis.php (_encodeData method) 111 | $value = str_starts_with($value, "gz:\x1f\x8b") ? substr($value, 5); 112 | 113 | return @gzuncompress($value) !== false ? gzuncompress($value) : null; 114 | }, 115 | 'save' => static fn (string $value): string => "gz:\x1f\x8b".gzcompress($value), 116 | ],*/ 117 | ], 118 | // Formatting functions, it runs after decoding 119 | 'formatters' => [ 120 | 'unserialize' => static function (string $value): ?string { 121 | $unserialized_value = @unserialize($value, ['allowed_classes' => false]); 122 | if ($unserialized_value !== false && is_array($unserialized_value)) { 123 | try { 124 | return json_encode($unserialized_value, JSON_THROW_ON_ERROR); 125 | } catch (JsonException) { 126 | return null; 127 | } 128 | } 129 | 130 | return null; 131 | }, 132 | ], 133 | // Customizations 134 | //'timezone' => 'Europe/Bratislava', // Leave empty (or commented out) to get it automatically obtained. 135 | 'time-format' => 'd. m. Y H:i:s', 136 | 'decimal-sep' => ',', 137 | 'thousands-sep' => ' ', 138 | 'list-view' => 'table', // table/tree - default key list view 139 | ]; 140 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | phpcacheadmin: 3 | build: . 4 | ports: 5 | - "8080:80" 6 | volumes: 7 | - "./:/var/www/html/" 8 | environment: 9 | - PCA_REDIS_0_HOST=redis 10 | - PCA_REDIS_0_PORT=6379 11 | - PCA_MEMCACHED_0_HOST=memcached 12 | - PCA_MEMCACHED_0_PORT=11211 13 | links: 14 | - redis 15 | - memcached 16 | redis: 17 | image: redis 18 | memcached: 19 | image: memcached 20 | -------------------------------------------------------------------------------- /index.php: -------------------------------------------------------------------------------- 1 | $d_dashboard->dashboardInfo(), $admin->dashboards); 45 | 46 | $current = $admin->currentDashboard(); 47 | $dashboard = $admin->getDashboard($current); 48 | $info = $dashboard->dashboardInfo(); 49 | 50 | $tpl->addGlobal('current', $current); 51 | 52 | if (isset($_GET['ajax'])) { 53 | echo $dashboard->ajax(); 54 | } else { 55 | if (isset($info['colors'])) { 56 | $colors = ''; 57 | 58 | foreach ((array) $info['colors'] as $key => $color) { 59 | $colors .= '--color-primary-'.$key.':'.$color.';'; 60 | } 61 | } 62 | 63 | echo $tpl->render('layout', [ 64 | 'colors' => $colors ?? null, 65 | 'site_title' => $info['title'], 66 | 'nav' => $nav, 67 | 'logout_url' => isset($auth) ? Http::queryString([], ['logout' => 'yes']) : null, 68 | 'version' => Admin::VERSION, 69 | 'repo' => 'https://github.com/RobiNN1/phpCacheAdmin', 70 | 'dashboard' => $dashboard->dashboard(), 71 | ]); 72 | } 73 | -------------------------------------------------------------------------------- /predis.phar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RobiNN1/phpCacheAdmin/f68bc366231ed6665fe94aa6d8f0d70dab974633/predis.phar -------------------------------------------------------------------------------- /src/Admin.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | public array $dashboards = []; 20 | 21 | public function __construct(Template $template) { 22 | foreach (Config::get('dashboards', []) as $class) { 23 | if (is_subclass_of($class, DashboardInterface::class) && $class::check()) { 24 | $dashboard = new $class($template); 25 | $info = $dashboard->dashboardInfo(); 26 | $this->dashboards[$info['key']] = $dashboard; 27 | } 28 | } 29 | } 30 | 31 | public function getDashboard(string $dashboard): DashboardInterface { 32 | return $this->dashboards[$dashboard]; 33 | } 34 | 35 | public function currentDashboard(): string { 36 | $current = Http::get('dashboard', ''); 37 | 38 | return array_key_exists($current, $this->dashboards) ? $current : array_key_first($this->dashboards); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Config.php: -------------------------------------------------------------------------------- 1 | $config The default config that will be merged with ENV. 51 | * 52 | * @return array 53 | */ 54 | private static function getEnvConfig(array $config): array { 55 | foreach (getenv() as $var => $value) { 56 | if (str_starts_with($var, 'PCA_')) { 57 | self::envVarToArray($config, $var, $value); 58 | } 59 | } 60 | 61 | return $config; 62 | } 63 | 64 | /** 65 | * Convert ENV variable to an array. 66 | * 67 | * It allows the app to use ENV variables and config.php together. 68 | * 69 | * @param array $array 70 | */ 71 | private static function envVarToArray(array &$array, string $var, string $value): void { 72 | $var = substr($var, 4); 73 | $keys = array_map(strtolower(...), explode('_', $var)); 74 | 75 | foreach ($keys as $i => $key) { 76 | if (count($keys) === 1) { 77 | break; 78 | } 79 | 80 | unset($keys[$i]); 81 | 82 | $array = &$array[$key]; 83 | } 84 | 85 | if (json_validate($value)) { 86 | try { 87 | $value = json_decode($value, true, 512, JSON_THROW_ON_ERROR); 88 | } catch (JsonException) { 89 | // 90 | } 91 | } 92 | 93 | $array[array_shift($keys)] = $value; 94 | } 95 | 96 | /** 97 | * Get encoders. 98 | * 99 | * Used in forms. 100 | * 101 | * @return array 102 | */ 103 | public static function getEncoders(): array { 104 | $encoders = self::get('converters', []); 105 | 106 | if ($encoders === []) { 107 | return []; 108 | } 109 | 110 | $encoders_array = array_keys($encoders); 111 | $encoders_array[] = 'none'; 112 | 113 | static $array = []; 114 | 115 | foreach ($encoders_array as $encoder) { 116 | $array[$encoder] = $encoder; 117 | } 118 | 119 | return $array; 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/Dashboards/APCu/APCuDashboard.php: -------------------------------------------------------------------------------- 1 | |string> 27 | */ 28 | public function dashboardInfo(): array { 29 | return [ 30 | 'key' => 'apcu', 31 | 'title' => 'APCu', 32 | 'colors' => [ 33 | 50 => '#eff6ff', 34 | 100 => '#dbeafe', 35 | 200 => '#bfdbfe', 36 | 300 => '#93c5fd', 37 | 400 => '#60a5fa', 38 | 500 => '#3b82f6', 39 | 600 => '#2563eb', 40 | 700 => '#1d4ed8', 41 | 800 => '#1e40af', 42 | 900 => '#1e3a8a', 43 | 950 => '#172554', 44 | ], 45 | ]; 46 | } 47 | 48 | public function ajax(): string { 49 | if (isset($_GET['deleteall']) && apcu_clear_cache()) { 50 | return Helpers::alert($this->template, 'Cache has been cleaned.', 'success'); 51 | } 52 | 53 | if (isset($_GET['delete'])) { 54 | return Helpers::deleteKey($this->template, static fn (string $key): bool => apcu_delete($key), true); 55 | } 56 | 57 | return ''; 58 | } 59 | 60 | public function dashboard(): string { 61 | $this->template->addGlobal('side', $this->panels()); 62 | 63 | if (isset($_GET['moreinfo'])) { 64 | return $this->moreInfo(); 65 | } 66 | 67 | if (isset($_GET['view'], $_GET['key'])) { 68 | return $this->viewKey(); 69 | } 70 | 71 | if (isset($_GET['form'])) { 72 | return $this->form(); 73 | } 74 | 75 | return $this->mainDashboard(); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Dashboards/APCu/APCuTrait.php: -------------------------------------------------------------------------------- 1 | 'PHP APCu extension v'.phpversion('apcu'), 34 | 'moreinfo' => true, 35 | 'data' => [ 36 | 'Start time' => Format::time($info['start_time']), 37 | 'Uptime' => Format::seconds(time() - $info['start_time']), 38 | 'Cache full count' => $info['expunges'], 39 | ], 40 | ], 41 | [ 42 | 'title' => 'Memory', 43 | 'data' => [ 44 | 'Type' => $info['memory_type'].' - '.$memory_info['num_seg'].' segment(s)', 45 | 'Total' => Format::bytes((int) $total_memory, 0), 46 | ['Used', Format::bytes((int) $memory_used).' ('.$memory_usage.'%)', $memory_usage], 47 | 'Free' => Format::bytes((int) $memory_info['avail_mem']), 48 | ], 49 | ], 50 | [ 51 | 'title' => 'Stats', 52 | 'data' => [ 53 | 'Slots' => $info['num_slots'], 54 | 'Keys' => Format::number((int) $info['num_entries']), 55 | ['Hits / Misses', Format::number($num_hits).' / '.Format::number($num_misses).' (Rate '.$hit_rate.'%)', $hit_rate, 'higher'], 56 | 'Expunges' => Format::number((int) $info['expunges']), 57 | ], 58 | ], 59 | ]; 60 | 61 | return $this->template->render('partials/info', ['panels' => $panels]); 62 | } 63 | 64 | private function moreInfo(): string { 65 | $info = (array) apcu_cache_info(true); 66 | 67 | foreach (apcu_sma_info(true) as $mem_name => $mem_value) { 68 | if (!is_array($mem_value)) { 69 | $info['memory'][$mem_name] = $mem_value; 70 | } 71 | } 72 | 73 | $info += Helpers::getExtIniInfo('apcu'); 74 | 75 | return $this->template->render('partials/info_table', [ 76 | 'panel_title' => 'APCu Info', 77 | 'array' => Helpers::convertTypesToString($info), 78 | ]); 79 | } 80 | 81 | private function getKeySize(string $key): int { 82 | $cache_info = apcu_cache_info(); 83 | 84 | // For some reason apcu_key_info() does not contain the key size 85 | foreach ($cache_info['cache_list'] as $entry) { 86 | if ($entry['info'] === $key) { 87 | return $entry['mem_size']; 88 | } 89 | } 90 | 91 | return 0; 92 | } 93 | 94 | private function viewKey(): string { 95 | $key = Http::get('key', ''); 96 | 97 | if (apcu_exists($key) === false) { 98 | Http::redirect(); 99 | } 100 | 101 | $value = Helpers::mixedToString(apcu_fetch($key)); 102 | $key_data = apcu_key_info($key); 103 | $ttl = $key_data['ttl'] === 0 ? -1 : $key_data['creation_time'] + $key_data['ttl'] - time(); 104 | 105 | if (isset($_GET['export'])) { 106 | Helpers::export( 107 | [['key' => $key, 'ttl' => $ttl]], 108 | $key, 109 | static fn (string $key): string => base64_encode(serialize(apcu_fetch($key))) 110 | ); 111 | } 112 | 113 | if (isset($_GET['delete'])) { 114 | apcu_delete($key); 115 | Http::redirect(); 116 | } 117 | 118 | [$formatted_value, $encode_fn, $is_formatted] = Value::format($value); 119 | 120 | return $this->template->render('partials/view_key', [ 121 | 'key' => $key, 122 | 'value' => $formatted_value, 123 | 'ttl' => Format::seconds($ttl), 124 | 'size' => Format::bytes($this->getKeySize($key)), 125 | 'encode_fn' => $encode_fn, 126 | 'formatted' => $is_formatted, 127 | 'edit_url' => Http::queryString(['ttl'], ['form' => 'edit', 'key' => $key]), 128 | 'export_url' => Http::queryString(['ttl', 'view', 'p', 'key'], ['export' => 'key']), 129 | 'delete_url' => Http::queryString(['view'], ['delete' => 'key', 'key' => $key]), 130 | ]); 131 | } 132 | 133 | public function saveKey(): void { 134 | $key = Http::post('key', ''); 135 | $expire = Http::post('expire', 0); 136 | $old_key = Http::post('old_key', ''); 137 | $value = Value::converter(Http::post('value', ''), Http::post('encoder', ''), 'save'); 138 | 139 | if ($old_key !== '' && $old_key !== $key) { // @phpstan-ignore-line 140 | apcu_delete($old_key); 141 | } 142 | 143 | apcu_store($key, $value, $expire); 144 | 145 | Http::redirect([], ['view' => 'key', 'key' => $key]); 146 | } 147 | 148 | /** 149 | * Add/edit form. 150 | */ 151 | private function form(): string { 152 | $key = Http::get('key', ''); 153 | $expire = 0; 154 | 155 | $encoder = Http::get('encoder', 'none'); 156 | $value = Http::post('value', ''); 157 | 158 | if (isset($_GET['key']) && apcu_exists($key)) { 159 | $value = Helpers::mixedToString(apcu_fetch($key)); 160 | $info = apcu_key_info($key); 161 | $expire = $info['ttl']; 162 | } 163 | 164 | if (isset($_POST['submit'])) { 165 | $this->saveKey(); 166 | } 167 | 168 | $value = Value::converter($value, $encoder, 'view'); 169 | 170 | return $this->template->render('partials/form', [ 171 | 'exp_attr' => ' min="0"', 172 | 'key' => $key, 173 | 'value' => $value, 174 | 'expire' => $expire, 175 | 'encoders' => Config::getEncoders(), 176 | 'encoder' => $encoder, 177 | ]); 178 | } 179 | 180 | /** 181 | * @return array> 182 | */ 183 | public function getAllKeys(): array { 184 | $search = Http::get('s', ''); 185 | $this->template->addGlobal('search_value', $search); 186 | 187 | $info = apcu_cache_info(); 188 | $keys = []; 189 | $time = time(); 190 | 191 | foreach ($info['cache_list'] as $key_data) { 192 | $key = $key_data['info']; 193 | 194 | if (stripos($key, $search) !== false) { 195 | $keys[] = [ 196 | 'key' => $key, 197 | 'mem_size' => $key_data['mem_size'], 198 | 'num_hits' => $key_data['num_hits'], 199 | 'access_time' => $key_data['access_time'], 200 | 'creation_time' => $key_data['creation_time'], 201 | 'ttl' => $key_data['ttl'] === 0 ? 'Doesn\'t expire' : $key_data['creation_time'] + $key_data['ttl'] - $time, 202 | ]; 203 | } 204 | } 205 | 206 | if (Http::get('view', Config::get('list-view', 'table')) === 'tree') { 207 | return $this->keysTreeView($keys); 208 | } 209 | 210 | return $this->keysTableView($keys); 211 | } 212 | 213 | /** 214 | * @param array $keys 215 | * 216 | * @return array> 217 | */ 218 | private function keysTableView(array $keys): array { 219 | $formatted_keys = []; 220 | 221 | foreach ($keys as $key_data) { 222 | $formatted_keys[] = [ 223 | 'key' => $key_data['key'], 224 | 'base64' => true, 225 | 'info' => [ 226 | 'link_title' => $key_data['key'], 227 | 'bytes_size' => $key_data['mem_size'], 228 | 'number_hits' => $key_data['num_hits'], 229 | 'timediff_last_used' => $key_data['access_time'], 230 | 'time_created' => $key_data['creation_time'], 231 | 'ttl' => $key_data['ttl'], 232 | ], 233 | ]; 234 | } 235 | 236 | return Helpers::sortKeys($this->template, $formatted_keys); 237 | } 238 | 239 | /** 240 | * @param array $keys 241 | * 242 | * @return array> 243 | */ 244 | private function keysTreeView(array $keys): array { 245 | $separator = Config::get('apcu-separator', ':'); 246 | $this->template->addGlobal('separator', $separator); 247 | 248 | $tree = []; 249 | 250 | foreach ($keys as $key_data) { 251 | $key = $key_data['key']; 252 | $parts = explode($separator, $key); 253 | 254 | /** @var array $current */ 255 | $current = &$tree; 256 | $path = ''; 257 | 258 | foreach ($parts as $i => $part) { 259 | $path = $path ? $path.$separator.$part : $part; 260 | 261 | if ($i === count($parts) - 1) { // check last part 262 | $current[] = [ 263 | 'type' => 'key', 264 | 'name' => $part, 265 | 'key' => $key, 266 | 'base64' => true, 267 | 'info' => [ 268 | 'bytes_size' => $key_data['mem_size'], 269 | 'number_hits' => $key_data['num_hits'], 270 | 'timediff_last_used' => $key_data['access_time'], 271 | 'time_created' => $key_data['creation_time'], 272 | 'ttl' => $key_data['ttl'], 273 | ], 274 | ]; 275 | } else { 276 | if (!isset($current[$part])) { 277 | $current[$part] = [ 278 | 'type' => 'folder', 279 | 'name' => $part, 280 | 'path' => $path, 281 | 'children' => [], 282 | 'expanded' => false, 283 | ]; 284 | } 285 | 286 | $current = &$current[$part]['children']; 287 | } 288 | } 289 | } 290 | 291 | Helpers::countChildren($tree); 292 | 293 | return $tree; 294 | } 295 | 296 | private function mainDashboard(): string { 297 | $keys = $this->getAllKeys(); 298 | 299 | if (isset($_POST['submit_import_key'])) { 300 | Helpers::import( 301 | static fn (string $key): bool => apcu_exists($key), 302 | static function (string $key, string $value, int $ttl): bool { 303 | return apcu_store($key, unserialize(base64_decode($value), ['allowed_classes' => false]), $ttl); 304 | } 305 | ); 306 | } 307 | 308 | if (isset($_GET['export_btn'])) { 309 | Helpers::export($keys, 'apcu_backup', static fn (string $key): string => base64_encode(serialize(apcu_fetch($key)))); 310 | } 311 | 312 | $paginator = new Paginator($this->template, $keys); 313 | 314 | $info = apcu_cache_info(true); 315 | 316 | return $this->template->render('dashboards/apcu', [ 317 | 'keys' => $paginator->getPaginated(), 318 | 'all_keys' => (int) $info['num_entries'], 319 | 'paginator' => $paginator->render(), 320 | 'view_key' => Http::queryString([], ['view' => 'key', 'key' => '__key__']), 321 | ]); 322 | } 323 | } 324 | -------------------------------------------------------------------------------- /src/Dashboards/DashboardException.php: -------------------------------------------------------------------------------- 1 | |string> 21 | */ 22 | public function dashboardInfo(): array; 23 | 24 | /** 25 | * Ajax content. 26 | */ 27 | public function ajax(): string; 28 | 29 | /** 30 | * Main dashboard content. 31 | */ 32 | public function dashboard(): string; 33 | } 34 | -------------------------------------------------------------------------------- /src/Dashboards/Memcached/MemcachedDashboard.php: -------------------------------------------------------------------------------- 1 | > 23 | */ 24 | private array $servers; 25 | 26 | private int $current_server; 27 | 28 | public PHPMem $memcached; 29 | 30 | public function __construct(private readonly Template $template) { 31 | $this->servers = Config::get('memcached', []); 32 | 33 | $server = Http::get('server', 0); 34 | $this->current_server = array_key_exists($server, $this->servers) ? $server : 0; 35 | } 36 | 37 | public static function check(): bool { 38 | return class_exists(PHPMem::class); 39 | } 40 | 41 | /** 42 | * @return array> 43 | */ 44 | public function dashboardInfo(): array { 45 | return [ 46 | 'key' => 'memcached', 47 | 'title' => 'Memcached', 48 | 'colors' => [ 49 | 50 => '#f2fbf9', 50 | 100 => '#d4f3ec', 51 | 200 => '#a9e6db', 52 | 300 => '#75d3c5', 53 | 400 => '#49b8ab', 54 | 500 => '#2b8e84', 55 | 600 => '#247d76', 56 | 700 => '#206560', 57 | 800 => '#1e514e', 58 | 900 => '#1d4441', 59 | 950 => '#0b2827', 60 | ], 61 | ]; 62 | } 63 | 64 | /** 65 | * Connect to the server. 66 | * 67 | * @param array $server 68 | * 69 | * @throws DashboardException 70 | */ 71 | public function connect(array $server): PHPMem { 72 | $server['port'] ??= 11211; 73 | 74 | if (class_exists(PHPMem::class)) { 75 | $memcached = new PHPMem($server); 76 | } else { 77 | throw new DashboardException('PHPMem client is not installed.'); 78 | } 79 | 80 | if (!$memcached->isConnected()) { 81 | $connection = $server['path'] ?? $server['host'].':'.$server['port']; 82 | throw new DashboardException(sprintf('Failed to connect to Memcached server %s.', $connection)); 83 | } 84 | 85 | return $memcached; 86 | } 87 | 88 | public function ajax(): string { 89 | try { 90 | $this->memcached = $this->connect($this->servers[$this->current_server]); 91 | 92 | if (isset($_GET['deleteall'])) { 93 | return $this->deleteAllKeys(); 94 | } 95 | 96 | if (isset($_GET['delete'])) { 97 | return Helpers::deleteKey($this->template, fn (string $key): bool => $this->memcached->delete($key)); 98 | } 99 | } catch (DashboardException|MemcachedException $e) { 100 | return $e->getMessage(); 101 | } 102 | 103 | return ''; 104 | } 105 | 106 | public function dashboard(): string { 107 | if ($this->servers === []) { 108 | return 'No servers'; 109 | } 110 | 111 | $this->template->addGlobal('servers', Helpers::serverSelector($this->template, $this->servers, $this->current_server)); 112 | 113 | try { 114 | $this->memcached = $this->connect($this->servers[$this->current_server]); 115 | 116 | $this->template->addGlobal('side', $this->panels()); 117 | 118 | if (isset($_GET['moreinfo'])) { 119 | return $this->moreInfo(); 120 | } 121 | 122 | if (isset($_GET['view'], $_GET['key'])) { 123 | return $this->viewKey(); 124 | } 125 | 126 | if (isset($_GET['form'])) { 127 | return $this->form(); 128 | } 129 | 130 | return $this->mainDashboard(); 131 | } catch (DashboardException|MemcachedException $e) { 132 | return $e->getMessage(); 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/Dashboards/Memcached/MemcachedException.php: -------------------------------------------------------------------------------- 1 | $server 21 | */ 22 | public function __construct(protected array $server = []) { 23 | } 24 | 25 | /** 26 | * Store an item. 27 | * 28 | * @throws MemcachedException 29 | */ 30 | public function set(string $key, mixed $value, int $expiration = 0): bool { 31 | $value = is_scalar($value) ? (string) $value : serialize($value); 32 | $raw = $this->runCommand('set '.$key.' 0 '.$expiration.' '.strlen($value)."\r\n".$value); 33 | 34 | return str_starts_with($raw, 'STORED'); 35 | } 36 | 37 | /** 38 | * Retrieve an item. 39 | * 40 | * @throws MemcachedException 41 | */ 42 | public function get(string $key): string|false { 43 | $raw = $this->runCommand('get '.$key); 44 | $lines = explode("\r\n", $raw); 45 | 46 | if (str_starts_with($raw, 'VALUE') && str_ends_with($raw, 'END')) { 47 | return $lines[1]; 48 | } 49 | 50 | return false; 51 | } 52 | 53 | /** 54 | * Delete item from the server. 55 | * 56 | * @throws MemcachedException 57 | */ 58 | public function delete(string $key): bool { 59 | return $this->runCommand('delete '.$key) === 'DELETED'; 60 | } 61 | 62 | /** 63 | * Invalidate all items in the cache. 64 | * 65 | * @throws MemcachedException 66 | */ 67 | public function flush(): bool { 68 | return $this->runCommand('flush_all') === 'OK'; 69 | } 70 | 71 | /** 72 | * @return array 73 | * 74 | * @throws MemcachedException 75 | */ 76 | public function getServerStats(?string $type = null): array { 77 | $type = in_array($type, ['settings', 'items', 'sizes', 'slabs', 'conns'], true) ? ' '.$type : ''; 78 | $raw = $this->runCommand('stats'.$type); 79 | $stats = []; 80 | 81 | foreach (explode("\r\n", $raw) as $line) { 82 | if (str_starts_with($line, 'STAT')) { 83 | [, $key, $value] = explode(' ', $line, 3); 84 | $stats[$key] = is_numeric($value) ? (int) $value : $value; 85 | } 86 | } 87 | 88 | return $stats; 89 | } 90 | 91 | public function isConnected(): bool { 92 | try { 93 | $stats = $this->getServerStats(); 94 | 95 | return isset($stats['pid']) && $stats['pid'] > 0; 96 | } catch (MemcachedException) { 97 | return false; 98 | } 99 | } 100 | 101 | /** 102 | * Get all the keys. 103 | * 104 | * Note: `getAllKeys()` or `stats cachedump` based functions do not work 105 | * properly, and this is currently the best way to retrieve all keys. 106 | * 107 | * This command requires Memcached server >= 1.4.31 108 | * 109 | * @link https://github.com/memcached/memcached/wiki/ReleaseNotes1431 110 | * 111 | * @return array 112 | * 113 | * @throws MemcachedException 114 | */ 115 | public function getKeys(): array { 116 | if (version_compare($this->version(), '1.5.19', '>=')) { 117 | $raw = $this->runCommand('lru_crawler metadump all'); 118 | $lines = explode("\n", $raw); 119 | array_pop($lines); 120 | 121 | return $lines; 122 | } 123 | 124 | $slabs = $this->runCommand('stats items'); 125 | $lines = explode("\n", $slabs); 126 | $slab_ids = []; 127 | 128 | foreach ($lines as $line) { 129 | if (preg_match('/STAT items:(\d+):/', $line, $matches)) { 130 | $slab_ids[] = $matches[1]; 131 | } 132 | } 133 | 134 | $keys = []; 135 | 136 | foreach (array_unique($slab_ids) as $slab_id) { 137 | $dump = $this->runCommand('stats cachedump '.$slab_id.' 0'); 138 | $dump_lines = explode("\n", $dump); 139 | 140 | foreach ($dump_lines as $line) { 141 | if (preg_match('/ITEM (\S+) \[(\d+) b; (\d+) s\]/', $line, $matches)) { 142 | $exp = (int) $matches[3] === 0 ? -1 : (int) $matches[3]; 143 | // Intentionally formatted as lru_crawler output 144 | $keys[] = 'key='.$matches[1].' exp='.$exp.' la=0 cas=0 fetch=no cls=1 size='.$matches[2]; 145 | } 146 | } 147 | } 148 | 149 | return $keys; 150 | } 151 | 152 | /** 153 | * Convert raw key line to an array. 154 | * 155 | * @return array 156 | */ 157 | public function parseLine(string $line): array { 158 | $data = []; 159 | 160 | foreach (explode(' ', $line) as $part) { 161 | if ($part !== '') { 162 | [$key, $val] = explode('=', $part); 163 | $data[$key] = is_numeric($val) ? (int) $val : $val; 164 | } 165 | } 166 | 167 | return $data; 168 | } 169 | 170 | /** 171 | * Get raw key. 172 | * 173 | * @throws MemcachedException 174 | */ 175 | public function getKey(string $key): string|false { 176 | $raw = $this->runCommand('get '.$key); 177 | $data = explode("\r\n", $raw); 178 | 179 | if ($data[0] === 'END') { 180 | return false; 181 | } 182 | 183 | return !isset($data[1]) || $data[1] === 'N;' ? '' : $data[1]; 184 | } 185 | 186 | /** 187 | * Get key meta-data. 188 | * 189 | * This command requires Memcached server >= 1.5.19 190 | * 191 | * @link https://github.com/memcached/memcached/wiki/ReleaseNotes1519 192 | * 193 | * @return array 194 | * 195 | * @throws MemcachedException 196 | */ 197 | public function getKeyMeta(string $key): array { 198 | if (version_compare($this->version(), '1.5.19', '>=')) { 199 | $raw = $this->runCommand('me '.$key); 200 | 201 | if ($raw === 'ERROR') { 202 | return []; 203 | } 204 | 205 | $raw = preg_replace('/^ME\s+\S+\s+/', '', $raw); // Remove `ME keyname` 206 | 207 | return $this->parseLine($raw); 208 | } 209 | 210 | foreach ($this->getKeys() as $line) { 211 | $data = $this->parseLine($line); 212 | 213 | if ($data['key'] === $key) { 214 | return $data; 215 | } 216 | } 217 | 218 | return []; 219 | } 220 | 221 | /** 222 | * Check if the key exists. 223 | * 224 | * @throws MemcachedException 225 | */ 226 | public function exists(string $key): bool { 227 | return $this->getKey($key) !== false; 228 | } 229 | 230 | /** 231 | * @throws MemcachedException 232 | */ 233 | public function version(): string { 234 | return str_replace('VERSION ', '', $this->runCommand('version')); 235 | } 236 | 237 | /** 238 | * Run command. 239 | * 240 | * These commands should work but not guaranteed to work on any server: 241 | * 242 | * set|add|replace|append|prepend \r\n\r\n 243 | * cas \r\n 244 | * get|gets *\r\n 245 | * gat|gats *\r\n 246 | * delete \r\n 247 | * incr|decr \r\n 248 | * touch \r\n 249 | * me \r\n 250 | * mg *\r\n 251 | * ms *\r\n 252 | * md *\r\n 253 | * ma *\r\n 254 | * mn\r\n 255 | * slabs reassign \r\n 256 | * slabs automove <0|1|2>\r\n 257 | * lru