├── .github
├── PULL_REQUEST_TEMPLATE.md
└── workflows
│ ├── code-analysis.yml
│ └── tests.yml
├── .gitignore
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── TESTING.md
├── composer.json
├── composer.lock
├── config
└── smart-cache.php
├── docs
├── googlea41a9f57385f6f00.html
├── index.html
├── robots.txt
└── sitemap.xml
├── phpunit.xml
├── src
├── Collections
│ └── LazyChunkedCollection.php
├── Console
│ └── Commands
│ │ ├── ClearCommand.php
│ │ └── StatusCommand.php
├── Contracts
│ ├── OptimizationStrategy.php
│ └── SmartCache.php
├── Drivers
│ └── MemoizedCacheDriver.php
├── Events
│ ├── CacheHit.php
│ ├── CacheMissed.php
│ ├── KeyForgotten.php
│ ├── KeyWritten.php
│ └── OptimizationApplied.php
├── Facades
│ └── SmartCache.php
├── Observers
│ └── CacheInvalidationObserver.php
├── Providers
│ └── SmartCacheServiceProvider.php
├── Services
│ ├── CacheInvalidationService.php
│ └── SmartChunkSizeCalculator.php
├── SmartCache.php
├── Strategies
│ ├── AdaptiveCompressionStrategy.php
│ ├── ChunkingStrategy.php
│ ├── CompressionStrategy.php
│ └── SmartSerializationStrategy.php
├── Traits
│ ├── CacheInvalidation.php
│ ├── DispatchesCacheEvents.php
│ └── HasLocks.php
└── helpers.php
└── tests
├── Feature
├── AdvancedInvalidationIntegrationTest.php
├── EnhancedSmartCacheControllerTest.php
└── SmartCacheIntegrationTest.php
├── TestCase.php
├── Unit
├── BatchOperationsTest.php
├── Collections
│ └── LazyChunkedCollectionTest.php
├── Console
│ ├── ClearCommandTest.php
│ └── StatusCommandTest.php
├── DependencyTrackingTest.php
├── Drivers
│ └── MemoizationTest.php
├── Events
│ └── CacheEventsTest.php
├── Http
│ └── HttpCommandExecutionTest.php
├── Laravel12
│ └── Laravel12SWRTest.php
├── Locks
│ └── AtomicLocksTest.php
├── Monitoring
│ └── PerformanceMonitoringTest.php
├── PatternBasedInvalidationTest.php
├── Providers
│ └── SmartCacheServiceProviderTest.php
├── Services
│ ├── CacheInvalidationServiceTest.php
│ └── SmartChunkSizeCalculatorTest.php
├── SmartCacheTest.php
├── Strategies
│ ├── AdaptiveCompressionStrategyTest.php
│ ├── ChunkingStrategyTest.php
│ ├── CompressionStrategyTest.php
│ └── SmartSerializationStrategyTest.php
├── StrategyOrderingTest.php
├── TagBasedInvalidationTest.php
└── Traits
│ └── ModelIntegrationTest.php
└── bootstrap.php
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | # Pull Request
2 |
3 | ## Description
4 | Please include a summary of the changes and the related issue. Please also include relevant motivation and context.
5 |
6 | Fixes # (issue)
7 |
8 | ## Type of change
9 | - [ ] Bug fix (non-breaking change which fixes an issue)
10 | - [ ] New feature (non-breaking change which adds functionality)
11 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
12 | - [ ] This change requires a documentation update
13 |
14 | ## How Has This Been Tested?
15 | Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce.
16 |
17 | - [ ] Test A
18 | - [ ] Test B
19 |
20 | **Test Configuration**:
21 | * PHP version:
22 | * Laravel version:
23 |
24 | ## Checklist:
25 | - [ ] My code follows the style guidelines of this project
26 | - [ ] I have performed a self-review of my own code
27 | - [ ] I have commented my code, particularly in hard-to-understand areas
28 | - [ ] I have made corresponding changes to the documentation
29 | - [ ] My changes generate no new warnings
30 | - [ ] I have added tests that prove my fix is effective or that my feature works
31 | - [ ] New and existing unit tests pass locally with my changes
32 | - [ ] Any dependent changes have been merged and published in downstream modules
33 |
--------------------------------------------------------------------------------
/.github/workflows/code-analysis.yml:
--------------------------------------------------------------------------------
1 | name: Coverage
2 |
3 | on:
4 | push:
5 | branches: [ main, master ]
6 | pull_request:
7 | branches: [ main, master ]
8 |
9 | jobs:
10 | coverage:
11 | runs-on: ubuntu-latest
12 | name: Coverage Report (PHP 8.3)
13 |
14 | steps:
15 | - name: Checkout code
16 | uses: actions/checkout@v4
17 |
18 | - name: Setup PHP
19 | uses: shivammathur/setup-php@v2
20 | with:
21 | php-version: 8.3
22 | extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick, fileinfo
23 | coverage: xdebug
24 |
25 | - name: Install dependencies
26 | run: |
27 | composer update --prefer-stable --prefer-dist --no-interaction
28 |
29 | - name: Execute tests with coverage
30 | run: vendor/bin/phpunit --coverage-text --testdox
31 |
--------------------------------------------------------------------------------
/.github/workflows/tests.yml:
--------------------------------------------------------------------------------
1 | name: Tests
2 |
3 | on:
4 | push:
5 | branches: [ main, master ]
6 | pull_request:
7 | branches: [ main, master ]
8 |
9 | jobs:
10 | test:
11 | runs-on: ubuntu-latest
12 |
13 | strategy:
14 | fail-fast: true
15 | matrix:
16 | php: [8.1, 8.2, 8.3]
17 | laravel: [8.*, 9.*, 10.*, 11.*, 12.*]
18 | stability: [prefer-stable]
19 | include:
20 | - laravel: 8.*
21 | testbench: ^6.0
22 | - laravel: 9.*
23 | testbench: ^7.0
24 | - laravel: 10.*
25 | testbench: ^8.0
26 | - laravel: 11.*
27 | testbench: ^9.0
28 | - laravel: 12.*
29 | testbench: ^10.0
30 | exclude:
31 | - laravel: 11.*
32 | php: 8.1
33 | - laravel: 12.*
34 | php: 8.1
35 |
36 | name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.stability }}
37 |
38 | steps:
39 | - name: Checkout code
40 | uses: actions/checkout@v4
41 |
42 | - name: Setup PHP
43 | uses: shivammathur/setup-php@v2
44 | with:
45 | php-version: ${{ matrix.php }}
46 | extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick, fileinfo
47 | coverage: none
48 |
49 | - name: Setup problem matchers
50 | run: |
51 | echo "::add-matcher::${{ runner.tool_cache }}/php.json"
52 | echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json"
53 |
54 | - name: Install dependencies
55 | run: |
56 | composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" --no-interaction --no-update
57 | composer update --${{ matrix.stability }} --prefer-dist --no-interaction
58 |
59 | - name: List Installed Dependencies
60 | run: composer show -D
61 |
62 | - name: Execute tests
63 | run: vendor/bin/phpunit --testdox
64 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | vendor/
2 | .phpunit.cache/
3 | coverage/
4 | .DS_Store
5 | tests/storage/framework/cache/
6 | tests/storage/framework/sessions/
7 | tests/storage/framework/views/
8 | tests/storage/logs/
9 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to making participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, sex characteristics, gender identity and expression,
9 | level of experience, education, socio-economic status, nationality, personal
10 | appearance, race, religion, or sexual identity and orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | * Using welcoming and inclusive language
18 | * Being respectful of differing viewpoints and experiences
19 | * Gracefully accepting constructive criticism
20 | * Focusing on what is best for the community
21 | * Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | * The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | * Trolling, insulting/derogatory comments, and personal or political attacks
28 | * Public or private harassment
29 | * Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission
31 | * Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies both within project spaces and in public spaces
49 | when an individual is representing the project or its community. Examples of
50 | representing a project or community include using an official project e-mail
51 | address, posting via an official social media account, or acting as an appointed
52 | representative at an online or offline event. Representation of a project may be
53 | further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the project team at eazaran@gmail.com. All
59 | complaints will be reviewed and investigated and will result in a response that
60 | is deemed necessary and appropriate to the circumstances. The project team is
61 | obligated to maintain confidentiality with regard to the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ## Attribution
69 |
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
72 |
73 | [homepage]: https://www.contributor-covenant.org
74 |
75 | For answers to common questions about this code of conduct, see
76 | https://www.contributor-covenant.org/faq
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 | I love your input! I want to make contributing to this project as easy and transparent as possible, whether it's:
3 |
4 | - Reporting a bug
5 | - Discussing the current state of the code
6 | - Submitting a fix
7 | - Proposing new features
8 | - Becoming a maintainer
9 |
10 | ## All Code Changes Happen Through Pull Requests
11 | Pull requests are the best way to propose changes to the codebase. I actively welcome your pull requests:
12 |
13 | 1. Fork the repo and create your branch from `main`.
14 | 2. If you've added code that should be tested, add tests.
15 | 3. If you've changed APIs or functions, update the documentation.
16 | 4. Make sure your code lints.
17 | 5. Issue that pull request!
18 |
19 | ## Any contributions you make will be under the MIT Software License
20 | In short, when you submit code changes, your submissions are understood to be under the same.
21 |
22 | ## License
23 | By contributing, you agree that your contributions will be licensed under its MIT License.
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 Ismael Azaran
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 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "iazaran/smart-cache",
3 | "description": "Smart Cache is a caching optimization package designed to enhance the way your Laravel application handles data caching. It intelligently manages large data sets by compressing, chunking, or applying other optimization strategies to keep your application performant and efficient.",
4 | "keywords": [
5 | "smart-cache",
6 | "smart cache laravel",
7 | "laravel package",
8 | "laravel caching",
9 | "cache optimization",
10 | "data compression",
11 | "cache chunking",
12 | "laravel performance",
13 | "redis cache",
14 | "file cache",
15 | "cache management",
16 | "laravel cache driver",
17 | "cache serialization",
18 | "cache strategies",
19 | "php caching",
20 | "laravel optimization"
21 | ],
22 | "homepage": "https://github.com/iazaran/smart-cache",
23 | "type": "library",
24 | "license": "MIT",
25 | "support": {
26 | "issues": "https://github.com/iazaran/smart-cache/issues",
27 | "source": "https://github.com/iazaran/smart-cache"
28 | },
29 | "authors": [
30 | {
31 | "name": "Ismael Azaran",
32 | "email": "eazaran@gmail.com"
33 | }
34 | ],
35 | "require": {
36 | "php": "^8.1",
37 | "illuminate/support": "^8.0||^9.0||^10.0||^11.0||^12.0",
38 | "illuminate/cache": "^8.0||^9.0||^10.0||^11.0||^12.0",
39 | "illuminate/console": "^8.0||^9.0||^10.0||^11.0||^12.0",
40 | "illuminate/contracts": "^8.0||^9.0||^10.0||^11.0||^12.0"
41 | },
42 | "require-dev": {
43 | "phpunit/phpunit": "^9.0|^10.0|^11.0",
44 | "orchestra/testbench": "^6.0||^7.0||^8.0||^9.0||^10.0",
45 | "mockery/mockery": "^1.5",
46 | "symfony/var-dumper": "^5.4|^6.0|^7.0"
47 | },
48 | "autoload": {
49 | "psr-4": {
50 | "SmartCache\\": "src/"
51 | },
52 | "files": [
53 | "src/helpers.php"
54 | ]
55 | },
56 | "autoload-dev": {
57 | "psr-4": {
58 | "SmartCache\\Tests\\": "tests/"
59 | }
60 | },
61 | "scripts": {
62 | "test": "vendor/bin/phpunit --testdox",
63 | "test-coverage": "vendor/bin/phpunit --coverage-html coverage --testdox"
64 | },
65 | "extra": {
66 | "laravel": {
67 | "providers": [
68 | "SmartCache\\Providers\\SmartCacheServiceProvider"
69 | ],
70 | "aliases": {
71 | "SmartCache": "SmartCache\\Facades\\SmartCache"
72 | }
73 | }
74 | },
75 | "minimum-stability": "dev",
76 | "prefer-stable": true
77 | }
78 |
--------------------------------------------------------------------------------
/config/smart-cache.php:
--------------------------------------------------------------------------------
1 | [
22 | 'compression' => 1024 * 50, // 50KB
23 | 'chunking' => 1024 * 100, // 100KB
24 | ],
25 |
26 | /*
27 | |--------------------------------------------------------------------------
28 | | Strategies
29 | |--------------------------------------------------------------------------
30 | |
31 | | Configure which optimization strategies are enabled and their options.
32 | |
33 | */
34 | 'strategies' => [
35 | 'compression' => [
36 | 'enabled' => true,
37 | 'mode' => 'fixed', // 'fixed' or 'adaptive'
38 | 'level' => 6, // 0-9 (higher = better compression but slower) - used in fixed mode
39 | 'adaptive' => [
40 | 'sample_size' => 1024, // Bytes to sample for compressibility test
41 | 'high_compression_threshold' => 0.5, // Ratio below which to use level 9
42 | 'low_compression_threshold' => 0.7, // Ratio above which to use level 3
43 | 'frequency_threshold' => 100, // Access count for speed priority
44 | ],
45 | ],
46 | 'chunking' => [
47 | 'enabled' => true,
48 | 'chunk_size' => 1000, // Items per chunk for arrays/collections
49 | 'lazy_loading' => false, // Enable lazy loading for chunks
50 | 'smart_sizing' => false, // Enable smart chunk size calculation
51 | ],
52 | ],
53 |
54 | /*
55 | |--------------------------------------------------------------------------
56 | | Fallback
57 | |--------------------------------------------------------------------------
58 | |
59 | | Configure fallback behavior if optimizations fail or are incompatible.
60 | |
61 | */
62 | 'fallback' => [
63 | 'enabled' => true,
64 | 'log_errors' => true,
65 | ],
66 |
67 | /*
68 | |--------------------------------------------------------------------------
69 | | Performance Monitoring
70 | |--------------------------------------------------------------------------
71 | |
72 | | Enable performance monitoring to track cache hit/miss ratios,
73 | | optimization impact, and operation durations.
74 | |
75 | */
76 | 'monitoring' => [
77 | 'enabled' => true,
78 | 'metrics_ttl' => 3600, // How long to keep metrics in cache (seconds)
79 | 'recent_entries_limit' => 100, // Number of recent operations to track per type
80 | ],
81 |
82 | /*
83 | |--------------------------------------------------------------------------
84 | | Performance Warnings
85 | |--------------------------------------------------------------------------
86 | |
87 | | Configure thresholds for performance warnings and recommendations.
88 | |
89 | */
90 | 'warnings' => [
91 | 'hit_ratio_threshold' => 70, // Percentage below which to warn about low hit ratio
92 | 'optimization_ratio_threshold' => 20, // Percentage below which to warn about low optimization usage
93 | 'slow_write_threshold' => 0.1, // Seconds above which to warn about slow writes
94 | ],
95 |
96 | /*
97 | |--------------------------------------------------------------------------
98 | | Cache Drivers
99 | |--------------------------------------------------------------------------
100 | |
101 | | Configure which cache drivers should use which optimization strategies.
102 | | Set to null to use the global strategies configuration.
103 | |
104 | */
105 | 'drivers' => [
106 | 'redis' => null, // Use global settings
107 | 'file' => [
108 | 'compression' => true,
109 | 'chunking' => true,
110 | ],
111 | 'memcached' => [
112 | 'compression' => false, // Memcached has its own compression
113 | 'chunking' => true,
114 | ],
115 | ],
116 |
117 | /*
118 | |--------------------------------------------------------------------------
119 | | Cache Events
120 | |--------------------------------------------------------------------------
121 | |
122 | | Enable cache events to track cache operations and optimization effectiveness.
123 | | Events can be used for monitoring, logging, and debugging.
124 | |
125 | */
126 | 'events' => [
127 | 'enabled' => false, // Disabled by default for backward compatibility
128 | 'dispatch' => [
129 | 'cache_hit' => true,
130 | 'cache_missed' => true,
131 | 'key_written' => true,
132 | 'key_forgotten' => true,
133 | 'optimization_applied' => true,
134 | ],
135 | ],
136 | ];
--------------------------------------------------------------------------------
/docs/googlea41a9f57385f6f00.html:
--------------------------------------------------------------------------------
1 | google-site-verification: googlea41a9f57385f6f00.html
--------------------------------------------------------------------------------
/docs/robots.txt:
--------------------------------------------------------------------------------
1 | User-agent: *
2 | Allow: /
3 |
4 | Sitemap: https://iazaran.github.io/smart-cache/sitemap.xml
--------------------------------------------------------------------------------
/docs/sitemap.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | https://iazaran.github.io/smart-cache/
5 | 2025-06-04
6 | weekly
7 | 1.0
8 |
9 |
10 | https://github.com/iazaran/smart-cache
11 | 2025-06-04
12 | weekly
13 | 0.9
14 |
15 |
16 | https://packagist.org/packages/iazaran/smart-cache
17 | 2025-06-04
18 | monthly
19 | 0.8
20 |
21 |
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
13 | ./tests/Unit
14 |
15 |
16 | ./tests/Feature
17 |
18 |
19 |
20 |
21 | ./src
22 |
23 |
24 | ./src/Console
25 | ./src/helpers.php
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/src/Collections/LazyChunkedCollection.php:
--------------------------------------------------------------------------------
1 | cache = $cache;
83 | $this->chunkKeys = $chunkKeys;
84 | $this->chunkSize = $chunkSize;
85 | $this->totalItems = $totalItems;
86 | $this->isCollection = $isCollection;
87 | }
88 |
89 | /**
90 | * Get the current element.
91 | *
92 | * @return mixed
93 | */
94 | public function current(): mixed
95 | {
96 | $this->ensureChunkLoaded($this->position);
97 |
98 | $indexInChunk = $this->position % $this->chunkSize;
99 |
100 | if ($this->currentChunk === null || !isset($this->currentChunk[$indexInChunk])) {
101 | return null;
102 | }
103 |
104 | return $this->currentChunk[$indexInChunk];
105 | }
106 |
107 | /**
108 | * Get the current key.
109 | *
110 | * @return int
111 | */
112 | public function key(): int
113 | {
114 | return $this->position;
115 | }
116 |
117 | /**
118 | * Move to the next element.
119 | *
120 | * @return void
121 | */
122 | public function next(): void
123 | {
124 | $this->position++;
125 | }
126 |
127 | /**
128 | * Rewind to the first element.
129 | *
130 | * @return void
131 | */
132 | public function rewind(): void
133 | {
134 | $this->position = 0;
135 | }
136 |
137 | /**
138 | * Check if the current position is valid.
139 | *
140 | * @return bool
141 | */
142 | public function valid(): bool
143 | {
144 | return $this->position < $this->totalItems;
145 | }
146 |
147 | /**
148 | * Count the total number of items.
149 | *
150 | * @return int
151 | */
152 | public function count(): int
153 | {
154 | return $this->totalItems;
155 | }
156 |
157 | /**
158 | * Check if an offset exists.
159 | *
160 | * @param mixed $offset
161 | * @return bool
162 | */
163 | public function offsetExists(mixed $offset): bool
164 | {
165 | return is_int($offset) && $offset >= 0 && $offset < $this->totalItems;
166 | }
167 |
168 | /**
169 | * Get an item at a given offset.
170 | *
171 | * @param mixed $offset
172 | * @return mixed
173 | */
174 | public function offsetGet(mixed $offset): mixed
175 | {
176 | if (!$this->offsetExists($offset)) {
177 | return null;
178 | }
179 |
180 | $this->ensureChunkLoaded($offset);
181 |
182 | $indexInChunk = $offset % $this->chunkSize;
183 |
184 | return $this->currentChunk[$indexInChunk] ?? null;
185 | }
186 |
187 | /**
188 | * Set an item at a given offset.
189 | *
190 | * @param mixed $offset
191 | * @param mixed $value
192 | * @return void
193 | * @throws \RuntimeException
194 | */
195 | public function offsetSet(mixed $offset, mixed $value): void
196 | {
197 | throw new \RuntimeException('LazyChunkedCollection is read-only');
198 | }
199 |
200 | /**
201 | * Unset an item at a given offset.
202 | *
203 | * @param mixed $offset
204 | * @return void
205 | * @throws \RuntimeException
206 | */
207 | public function offsetUnset(mixed $offset): void
208 | {
209 | throw new \RuntimeException('LazyChunkedCollection is read-only');
210 | }
211 |
212 | /**
213 | * Convert to array (loads all chunks).
214 | *
215 | * @return array
216 | */
217 | public function toArray(): array
218 | {
219 | $result = [];
220 |
221 | foreach ($this->chunkKeys as $chunkKey) {
222 | $chunk = $this->cache->get($chunkKey, []);
223 | $result = array_merge($result, $chunk);
224 | }
225 |
226 | return $result;
227 | }
228 |
229 | /**
230 | * Convert to Laravel Collection (loads all chunks).
231 | *
232 | * @return Collection
233 | */
234 | public function toCollection(): Collection
235 | {
236 | return new Collection($this->toArray());
237 | }
238 |
239 | /**
240 | * Get a slice of the collection without loading all chunks.
241 | *
242 | * @param int $offset
243 | * @param int|null $length
244 | * @return array
245 | */
246 | public function slice(int $offset, ?int $length = null): array
247 | {
248 | $result = [];
249 | $end = $length !== null ? $offset + $length : $this->totalItems;
250 | $end = min($end, $this->totalItems);
251 |
252 | for ($i = $offset; $i < $end; $i++) {
253 | $result[] = $this->offsetGet($i);
254 | }
255 |
256 | return $result;
257 | }
258 |
259 | /**
260 | * Apply a callback to each item without loading all chunks at once.
261 | *
262 | * @param callable $callback
263 | * @return void
264 | */
265 | public function each(callable $callback): void
266 | {
267 | foreach ($this as $index => $item) {
268 | $callback($item, $index);
269 | }
270 | }
271 |
272 | /**
273 | * Filter items without loading all chunks at once.
274 | *
275 | * @param callable $callback
276 | * @return array
277 | */
278 | public function filter(callable $callback): array
279 | {
280 | $result = [];
281 |
282 | foreach ($this as $index => $item) {
283 | if ($callback($item, $index)) {
284 | $result[] = $item;
285 | }
286 | }
287 |
288 | return $result;
289 | }
290 |
291 | /**
292 | * Map items without loading all chunks at once.
293 | *
294 | * @param callable $callback
295 | * @return array
296 | */
297 | public function map(callable $callback): array
298 | {
299 | $result = [];
300 |
301 | foreach ($this as $index => $item) {
302 | $result[] = $callback($item, $index);
303 | }
304 |
305 | return $result;
306 | }
307 |
308 | /**
309 | * Ensure the chunk containing the given position is loaded.
310 | *
311 | * @param int $position
312 | * @return void
313 | */
314 | protected function ensureChunkLoaded(int $position): void
315 | {
316 | $chunkIndex = (int) floor($position / $this->chunkSize);
317 |
318 | // Already loaded
319 | if ($chunkIndex === $this->currentChunkIndex) {
320 | return;
321 | }
322 |
323 | // Check if chunk is in loaded chunks cache
324 | if (isset($this->loadedChunks[$chunkIndex])) {
325 | $this->currentChunk = $this->loadedChunks[$chunkIndex];
326 | $this->currentChunkIndex = $chunkIndex;
327 | return;
328 | }
329 |
330 | // Load the chunk
331 | if (isset($this->chunkKeys[$chunkIndex])) {
332 | $this->currentChunk = $this->cache->get($this->chunkKeys[$chunkIndex], []);
333 | $this->currentChunkIndex = $chunkIndex;
334 |
335 | // Cache the loaded chunk
336 | $this->loadedChunks[$chunkIndex] = $this->currentChunk;
337 |
338 | // Limit memory usage by removing old chunks
339 | if (count($this->loadedChunks) > $this->maxLoadedChunks) {
340 | // Remove the oldest chunk (first key)
341 | $oldestKey = array_key_first($this->loadedChunks);
342 | unset($this->loadedChunks[$oldestKey]);
343 | }
344 | } else {
345 | $this->currentChunk = null;
346 | $this->currentChunkIndex = -1;
347 | }
348 | }
349 |
350 | /**
351 | * Get memory usage statistics.
352 | *
353 | * @return array
354 | */
355 | public function getMemoryStats(): array
356 | {
357 | return [
358 | 'loaded_chunks' => count($this->loadedChunks),
359 | 'total_chunks' => count($this->chunkKeys),
360 | 'memory_usage' => strlen(serialize($this->loadedChunks)),
361 | 'total_items' => $this->totalItems,
362 | 'chunk_size' => $this->chunkSize,
363 | ];
364 | }
365 | }
366 |
367 |
--------------------------------------------------------------------------------
/src/Console/Commands/ClearCommand.php:
--------------------------------------------------------------------------------
1 | argument('key');
30 |
31 | if ($specificKey) {
32 | return $this->clearSpecificKey($cache, $specificKey);
33 | }
34 |
35 | return $this->clearAllKeys($cache);
36 | }
37 |
38 | /**
39 | * Clear a specific cache key.
40 | */
41 | protected function clearSpecificKey(SmartCache $cache, string $key): int
42 | {
43 | $managedKeys = $cache->getManagedKeys();
44 | $isManaged = in_array($key, $managedKeys);
45 | $force = $this->option('force');
46 |
47 | // Check if key exists in cache (either managed or regular Laravel cache)
48 | $keyExists = $cache->has($key);
49 |
50 | if (!$isManaged && !$force) {
51 | if ($keyExists) {
52 | $this->error("Cache key '{$key}' exists but is not managed by SmartCache. Use --force to clear it anyway.");
53 | } else {
54 | $this->error("Cache key '{$key}' is not managed by SmartCache or does not exist.");
55 | }
56 | return 1;
57 | }
58 |
59 | if (!$keyExists) {
60 | $this->error("Cache key '{$key}' does not exist.");
61 | return 1;
62 | }
63 |
64 | if ($isManaged) {
65 | $this->info("Clearing SmartCache managed item with key '{$key}'...");
66 | $success = $cache->forget($key);
67 | } else {
68 | $this->info("Clearing cache item with key '{$key}' (not managed by SmartCache)...");
69 | // Use the underlying Laravel cache to clear non-managed keys
70 | $success = $cache->store()->forget($key);
71 | }
72 |
73 | if ($success) {
74 | $this->info("Cache key '{$key}' has been cleared successfully.");
75 | return 0;
76 | } else {
77 | $this->error("Failed to clear cache key '{$key}'.");
78 | return 1;
79 | }
80 | }
81 |
82 | /**
83 | * Clear all SmartCache managed keys.
84 | */
85 | protected function clearAllKeys(SmartCache $cache): int
86 | {
87 | $keys = $cache->getManagedKeys();
88 | $count = count($keys);
89 | $force = $this->option('force');
90 |
91 | if ($count === 0) {
92 | if ($force) {
93 | $this->info('No SmartCache managed items found. Checking for non-managed cache keys...');
94 | return $this->clearOrphanedKeys($cache);
95 | } else {
96 | $this->info('No SmartCache managed items found.');
97 | return 0;
98 | }
99 | }
100 |
101 | $this->info("Clearing {$count} SmartCache managed items...");
102 |
103 | // Clean up expired keys first and report
104 | $expiredCleaned = $cache->cleanupExpiredManagedKeys();
105 | if ($expiredCleaned > 0) {
106 | $this->info("Cleaned up {$expiredCleaned} expired keys from tracking list.");
107 | }
108 |
109 | // Get updated key count after cleanup
110 | $keys = $cache->getManagedKeys();
111 | $actualCount = count($keys);
112 |
113 | if ($actualCount === 0) {
114 | $this->info('All managed keys were expired and have been cleaned up.');
115 | return 0;
116 | }
117 |
118 | $this->info("Clearing {$actualCount} active SmartCache managed items...");
119 |
120 | $success = $cache->clear();
121 |
122 | if ($success) {
123 | $this->info('All SmartCache managed items have been cleared successfully.');
124 |
125 | if ($force) {
126 | $this->info('Checking for non-managed cache keys...');
127 | $this->clearOrphanedKeys($cache);
128 | }
129 |
130 | return 0;
131 | } else {
132 | $this->error('Some SmartCache items could not be cleared.');
133 | $this->comment('This may be due to cache driver limitations or permission issues.');
134 | return 1;
135 | }
136 | }
137 |
138 | /**
139 | * Clear orphaned keys (with --force, clears ALL non-managed keys).
140 | */
141 | protected function clearOrphanedKeys(SmartCache $cache): int
142 | {
143 | $repository = $cache->store();
144 | $store = $repository->getStore();
145 | $cleared = 0;
146 | $managedKeys = $cache->getManagedKeys();
147 |
148 | // Try to get all cache keys (this varies by cache driver)
149 | try {
150 | $allKeys = $this->getAllCacheKeys($store);
151 |
152 | foreach ($allKeys as $key) {
153 | // With --force, clear ALL keys that are not currently managed by SmartCache
154 | // but exclude SmartCache internal keys
155 | if (!in_array($key, $managedKeys) && !$this->isSmartCacheInternalKey($key)) {
156 | if ($repository->forget($key)) {
157 | $cleared++;
158 | $this->line("Cleared key: {$key}");
159 | }
160 | }
161 | }
162 |
163 | if ($cleared > 0) {
164 | $this->info("Cleared {$cleared} non-managed cache keys.");
165 | } else {
166 | $this->info('No non-managed cache keys found.');
167 | }
168 |
169 | return 0;
170 | } catch (\Exception $e) {
171 | $this->warn('Could not scan for non-managed keys with this cache driver. Only managed keys were cleared.');
172 | return 0;
173 | }
174 | }
175 |
176 | /**
177 | * Get all cache keys (driver-dependent).
178 | */
179 | protected function getAllCacheKeys($store): array
180 | {
181 | $storeClass = get_class($store);
182 |
183 | // Redis store
184 | if (str_contains($storeClass, 'Redis')) {
185 | return $store->connection()->keys('*');
186 | }
187 |
188 | // Array store (used in testing)
189 | if (str_contains($storeClass, 'ArrayStore')) {
190 | return $this->getArrayStoreKeys($store);
191 | }
192 |
193 | // For other drivers, we can't easily get all keys
194 | // This is a limitation of Laravel's cache abstraction
195 | throw new \Exception('Cannot enumerate keys for this cache driver');
196 | }
197 |
198 | /**
199 | * Get all keys from ArrayStore (Laravel version compatible).
200 | */
201 | protected function getArrayStoreKeys($store): array
202 | {
203 | // Try the newer method first (Laravel 10+)
204 | if (method_exists($store, 'all')) {
205 | return array_keys($store->all(false)); // false to avoid unserializing values
206 | }
207 |
208 | // Fall back to reflection for older versions or when all() doesn't exist
209 | try {
210 | $reflection = new \ReflectionClass($store);
211 | $storageProperty = $reflection->getProperty('storage');
212 | $storageProperty->setAccessible(true);
213 | $storage = $storageProperty->getValue($store);
214 |
215 | return array_keys($storage ?? []);
216 | } catch (\ReflectionException $e) {
217 | // If reflection fails, return empty array
218 | return [];
219 | }
220 | }
221 |
222 | /**
223 | * Check if a key is a SmartCache internal key.
224 | */
225 | protected function isSmartCacheInternalKey(string $key): bool
226 | {
227 | // Look for SmartCache-specific patterns (internal keys that shouldn't be cleared as "non-managed")
228 | return str_contains($key, '_sc_') ||
229 | str_contains($key, '_sc_meta') ||
230 | str_contains($key, '_sc_chunk_') ||
231 | $key === '_sc_managed_keys';
232 | }
233 |
234 | }
--------------------------------------------------------------------------------
/src/Console/Commands/StatusCommand.php:
--------------------------------------------------------------------------------
1 | getManagedKeys();
32 | $count = count($keys);
33 | $force = $this->option('force');
34 |
35 | $this->info('SmartCache Status');
36 | $this->line('----------------');
37 | $this->line('');
38 |
39 | // Display driver information
40 | $this->line('Cache Driver: ' . Cache::getDefaultDriver());
41 | $this->line('');
42 |
43 | // Display managed keys count
44 | $this->line("Managed Keys: {$count}");
45 |
46 | // If there are keys, show some examples
47 | if ($count > 0) {
48 | $this->line('');
49 | $this->line('Examples:');
50 |
51 | $sampleKeys = array_slice($keys, 0, min(5, $count));
52 |
53 | foreach ($sampleKeys as $key) {
54 | $this->line(" - {$key}");
55 | }
56 |
57 | if ($count > 5) {
58 | $this->line(" - ... and " . ($count - 5) . " more");
59 | }
60 | }
61 |
62 | // Force option: Scan for orphaned SmartCache keys
63 | if ($force) {
64 | $this->line('');
65 | $this->line('Laravel Cache Analysis (--force):');
66 | $this->line('-----------------------------------');
67 |
68 | $nonManagedKeys = $this->findAllNonManagedKeys($cache);
69 |
70 | if (!empty($nonManagedKeys)) {
71 | $this->line('');
72 | $this->warn("Found " . count($nonManagedKeys) . " non-managed Laravel cache keys:");
73 |
74 | $sampleNonManaged = array_slice($nonManagedKeys, 0, min(10, count($nonManagedKeys)));
75 | foreach ($sampleNonManaged as $key) {
76 | $this->line(" ! {$key}");
77 | }
78 |
79 | if (count($nonManagedKeys) > 10) {
80 | $this->line(" ! ... and " . (count($nonManagedKeys) - 10) . " more");
81 | }
82 |
83 | $this->line('');
84 | $this->comment('These keys are stored in Laravel cache but not managed by SmartCache.');
85 | $this->comment('Consider running: php artisan smart-cache:clear --force');
86 | } else {
87 | $this->line('');
88 | $this->info('✓ No non-managed cache keys found.');
89 | }
90 |
91 | // Check if managed keys actually exist in cache
92 | $missingKeys = [];
93 | foreach ($keys as $key) {
94 | if (!$cache->has($key)) {
95 | $missingKeys[] = $key;
96 | }
97 | }
98 |
99 | if (!empty($missingKeys)) {
100 | $this->line('');
101 | $this->warn("Found " . count($missingKeys) . " managed keys that no longer exist in cache:");
102 |
103 | $sampleMissing = array_slice($missingKeys, 0, min(5, count($missingKeys)));
104 | foreach ($sampleMissing as $key) {
105 | $this->line(" ? {$key}");
106 | }
107 |
108 | if (count($missingKeys) > 5) {
109 | $this->line(" ? ... and " . (count($missingKeys) - 5) . " more");
110 | }
111 |
112 | $this->line('');
113 | $this->comment('These managed keys no longer exist in the cache store.');
114 | } else if ($count > 0) {
115 | $this->line('');
116 | $this->info('✓ All managed keys exist in cache.');
117 | }
118 | }
119 |
120 | // Display configuration
121 | $configData = $config->get('smart-cache');
122 | $this->line('');
123 | $this->line('Configuration:');
124 | $this->line(' - Compression: ' . ($configData['strategies']['compression']['enabled'] ? 'Enabled' : 'Disabled'));
125 | $this->line(' * Threshold: ' . number_format($configData['thresholds']['compression'] / 1024, 2) . ' KB');
126 | $this->line(' * Level: ' . $configData['strategies']['compression']['level']);
127 | $this->line(' - Chunking: ' . ($configData['strategies']['chunking']['enabled'] ? 'Enabled' : 'Disabled'));
128 | $this->line(' * Threshold: ' . number_format($configData['thresholds']['chunking'] / 1024, 2) . ' KB');
129 | $this->line(' * Chunk Size: ' . $configData['strategies']['chunking']['chunk_size'] . ' items');
130 |
131 | return 0;
132 | }
133 |
134 | /**
135 | * Find all non-managed keys in the cache.
136 | */
137 | protected function findAllNonManagedKeys(SmartCache $cache): array
138 | {
139 | $repository = $cache->store();
140 | $store = $repository->getStore();
141 | $managedKeys = $cache->getManagedKeys();
142 | $nonManagedKeys = [];
143 |
144 | try {
145 | $allKeys = $this->getAllCacheKeys($store);
146 |
147 | foreach ($allKeys as $key) {
148 | // Check if key is not managed by SmartCache and not a SmartCache internal key
149 | if (!in_array($key, $managedKeys) && !$this->isSmartCacheInternalKey($key)) {
150 | $nonManagedKeys[] = $key;
151 | }
152 | }
153 |
154 | return $nonManagedKeys;
155 | } catch (\Exception $e) {
156 | // If we can't scan keys, return empty array
157 | return [];
158 | }
159 | }
160 |
161 | /**
162 | * Get all cache keys (driver-dependent).
163 | */
164 | protected function getAllCacheKeys($store): array
165 | {
166 | $storeClass = get_class($store);
167 |
168 | // Redis store
169 | if (str_contains($storeClass, 'Redis')) {
170 | return $store->connection()->keys('*');
171 | }
172 |
173 | // Array store (used in testing)
174 | if (str_contains($storeClass, 'ArrayStore')) {
175 | return $this->getArrayStoreKeys($store);
176 | }
177 |
178 | // For other drivers, we can't easily get all keys
179 | // This is a limitation of Laravel's cache abstraction
180 | throw new \Exception('Cannot enumerate keys for this cache driver');
181 | }
182 |
183 | /**
184 | * Get all keys from ArrayStore (Laravel version compatible).
185 | */
186 | protected function getArrayStoreKeys($store): array
187 | {
188 | // Try the newer method first (Laravel 10+)
189 | if (method_exists($store, 'all')) {
190 | return array_keys($store->all(false)); // false to avoid unserializing values
191 | }
192 |
193 | // Fall back to reflection for older versions or when all() doesn't exist
194 | try {
195 | $reflection = new \ReflectionClass($store);
196 | $storageProperty = $reflection->getProperty('storage');
197 | $storageProperty->setAccessible(true);
198 | $storage = $storageProperty->getValue($store);
199 |
200 | return array_keys($storage ?? []);
201 | } catch (\ReflectionException $e) {
202 | // If reflection fails, return empty array
203 | return [];
204 | }
205 | }
206 |
207 | /**
208 | * Check if a key is a SmartCache internal key.
209 | */
210 | protected function isSmartCacheInternalKey(string $key): bool
211 | {
212 | // Look for SmartCache-specific patterns (internal keys that shouldn't be shown as "non-managed")
213 | return str_contains($key, '_sc_') ||
214 | str_contains($key, '_sc_meta') ||
215 | str_contains($key, '_sc_chunk_') ||
216 | $key === '_sc_managed_keys';
217 | }
218 |
219 | }
--------------------------------------------------------------------------------
/src/Contracts/OptimizationStrategy.php:
--------------------------------------------------------------------------------
1 | key = $key;
38 | $this->value = $value;
39 | $this->tags = $tags;
40 | }
41 | }
42 |
43 |
--------------------------------------------------------------------------------
/src/Events/CacheMissed.php:
--------------------------------------------------------------------------------
1 | key = $key;
30 | $this->tags = $tags;
31 | }
32 | }
33 |
34 |
--------------------------------------------------------------------------------
/src/Events/KeyForgotten.php:
--------------------------------------------------------------------------------
1 | key = $key;
30 | $this->tags = $tags;
31 | }
32 | }
33 |
34 |
--------------------------------------------------------------------------------
/src/Events/KeyWritten.php:
--------------------------------------------------------------------------------
1 | key = $key;
46 | $this->value = $value;
47 | $this->seconds = $seconds;
48 | $this->tags = $tags;
49 | }
50 | }
51 |
52 |
--------------------------------------------------------------------------------
/src/Events/OptimizationApplied.php:
--------------------------------------------------------------------------------
1 | key = $key;
53 | $this->strategy = $strategy;
54 | $this->originalSize = $originalSize;
55 | $this->optimizedSize = $optimizedSize;
56 | $this->ratio = $optimizedSize > 0 ? $optimizedSize / $originalSize : 0;
57 | }
58 | }
59 |
60 |
--------------------------------------------------------------------------------
/src/Facades/SmartCache.php:
--------------------------------------------------------------------------------
1 | invalidateCache($model);
19 | }
20 |
21 | /**
22 | * Handle the model "updated" event.
23 | *
24 | * @param Model $model
25 | * @return void
26 | */
27 | public function updated(Model $model): void
28 | {
29 | $this->invalidateCache($model);
30 | }
31 |
32 | /**
33 | * Handle the model "deleted" event.
34 | *
35 | * @param Model $model
36 | * @return void
37 | */
38 | public function deleted(Model $model): void
39 | {
40 | $this->invalidateCache($model);
41 | }
42 |
43 | /**
44 | * Handle the model "restored" event.
45 | *
46 | * @param Model $model
47 | * @return void
48 | */
49 | public function restored(Model $model): void
50 | {
51 | $this->invalidateCache($model);
52 | }
53 |
54 | /**
55 | * Perform cache invalidation if the model uses the CacheInvalidation trait.
56 | *
57 | * @param Model $model
58 | * @return void
59 | */
60 | protected function invalidateCache(Model $model): void
61 | {
62 | if ($this->usesCacheInvalidationTrait($model)) {
63 | $model->performCacheInvalidation();
64 | }
65 | }
66 |
67 | /**
68 | * Check if the model uses the CacheInvalidation trait.
69 | *
70 | * @param Model $model
71 | * @return bool
72 | */
73 | protected function usesCacheInvalidationTrait(Model $model): bool
74 | {
75 | return in_array(CacheInvalidation::class, class_uses_recursive($model));
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/src/Providers/SmartCacheServiceProvider.php:
--------------------------------------------------------------------------------
1 | mergeConfigFrom(
23 | __DIR__ . '/../../config/smart-cache.php', 'smart-cache'
24 | );
25 |
26 | // Register the service
27 | $this->app->singleton(SmartCacheContract::class, function ($app) {
28 | $cacheManager = $app['cache'];
29 | $config = $app['config'];
30 |
31 | // Create strategies based on config
32 | // Order matters: more specific strategies (chunking) should be tried first
33 | $strategies = [];
34 |
35 | if ($config->get('smart-cache.strategies.chunking.enabled', true)) {
36 | $strategies[] = new ChunkingStrategy(
37 | $config->get('smart-cache.thresholds.chunking', 102400),
38 | $config->get('smart-cache.strategies.chunking.chunk_size', 1000),
39 | $config->get('smart-cache.strategies.chunking.lazy_loading', false),
40 | $config->get('smart-cache.strategies.chunking.smart_sizing', false)
41 | );
42 | }
43 |
44 | if ($config->get('smart-cache.strategies.compression.enabled', true)) {
45 | $compressionMode = $config->get('smart-cache.strategies.compression.mode', 'fixed');
46 |
47 | if ($compressionMode === 'adaptive') {
48 | $strategies[] = new AdaptiveCompressionStrategy(
49 | $config->get('smart-cache.thresholds.compression', 51200),
50 | $config->get('smart-cache.strategies.compression.level', 6),
51 | $config->get('smart-cache.strategies.compression.adaptive.sample_size', 1024),
52 | $config->get('smart-cache.strategies.compression.adaptive.high_compression_threshold', 0.5),
53 | $config->get('smart-cache.strategies.compression.adaptive.low_compression_threshold', 0.7),
54 | $config->get('smart-cache.strategies.compression.adaptive.frequency_threshold', 100)
55 | );
56 | } else {
57 | $strategies[] = new CompressionStrategy(
58 | $config->get('smart-cache.thresholds.compression', 51200),
59 | $config->get('smart-cache.strategies.compression.level', 6)
60 | );
61 | }
62 | }
63 |
64 | return new SmartCache($cacheManager->store(), $cacheManager, $config, $strategies);
65 | });
66 |
67 | // Register alias for facade
68 | $this->app->alias(SmartCacheContract::class, 'smart-cache');
69 | }
70 |
71 | /**
72 | * Bootstrap services.
73 | */
74 | public function boot(): void
75 | {
76 | // Publish config
77 | $this->publishes([
78 | __DIR__ . '/../../config/smart-cache.php' => $this->app->configPath('smart-cache.php'),
79 | ], 'smart-cache-config');
80 |
81 | // Register commands
82 | if ($this->app->runningInConsole()) {
83 | $this->commands([
84 | ClearCommand::class,
85 | StatusCommand::class,
86 | ]);
87 | }
88 |
89 | // Register command metadata for HTTP context
90 | $this->app->singleton('smart-cache.commands', function ($app) {
91 | return [
92 | 'smart-cache:clear' => [
93 | 'class' => ClearCommand::class,
94 | 'description' => 'Clear SmartCache managed items',
95 | 'signature' => 'smart-cache:clear {key? : The specific cache key to clear} {--force : Force clear keys even if not managed by SmartCache}'
96 | ],
97 | 'smart-cache:status' => [
98 | 'class' => StatusCommand::class,
99 | 'description' => 'Display information about SmartCache usage and configuration',
100 | 'signature' => 'smart-cache:status {--force : Include Laravel cache analysis and orphaned SmartCache keys}'
101 | ]
102 | ];
103 | });
104 | }
105 | }
--------------------------------------------------------------------------------
/src/Services/CacheInvalidationService.php:
--------------------------------------------------------------------------------
1 | smartCache = $smartCache;
15 | }
16 |
17 | /**
18 | * Flush cache by multiple patterns with advanced matching.
19 | *
20 | * @param array $patterns
21 | * @return int Number of keys invalidated
22 | */
23 | public function flushPatterns(array $patterns): int
24 | {
25 | $invalidated = 0;
26 | $managedKeys = $this->smartCache->getManagedKeys();
27 |
28 | foreach ($patterns as $pattern) {
29 | foreach ($managedKeys as $key) {
30 | if ($this->matchesAdvancedPattern($key, $pattern)) {
31 | $result = $this->smartCache->forget($key);
32 | if ($result) {
33 | $invalidated++;
34 | }
35 | }
36 | }
37 | }
38 |
39 | return $invalidated;
40 | }
41 |
42 | /**
43 | * Invalidate cache based on model relationships.
44 | *
45 | * @param string $modelClass
46 | * @param mixed $modelId
47 | * @param array $relationships
48 | * @return int Number of keys invalidated
49 | */
50 | public function invalidateModelRelations(string $modelClass, mixed $modelId, array $relationships = []): int
51 | {
52 | $invalidated = 0;
53 | $basePatterns = [
54 | $modelClass . '_' . $modelId . '_*',
55 | $modelClass . '_*_' . $modelId,
56 | strtolower(class_basename($modelClass)) . '_' . $modelId . '_*',
57 | ];
58 |
59 | // Add relationship-based patterns
60 | foreach ($relationships as $relation) {
61 | $basePatterns[] = $relation . '_*_' . $modelClass . '_' . $modelId;
62 | $basePatterns[] = $modelClass . '_' . $modelId . '_' . $relation . '_*';
63 | }
64 |
65 | $invalidated += $this->flushPatterns($basePatterns);
66 |
67 | return $invalidated;
68 | }
69 |
70 | /**
71 | * Set up cache warming for frequently accessed keys.
72 | *
73 | * @param array $warmingRules
74 | * @return void
75 | */
76 | public function setupCacheWarming(array $warmingRules): void
77 | {
78 | foreach ($warmingRules as $rule) {
79 | if (isset($rule['key'], $rule['callback'], $rule['ttl'])) {
80 | // Warm the cache if it doesn't exist or is about to expire
81 | if (!$this->smartCache->has($rule['key'])) {
82 | $value = $rule['callback']();
83 | $this->smartCache->put($rule['key'], $value, $rule['ttl']);
84 | }
85 | }
86 | }
87 | }
88 |
89 | /**
90 | * Create cache hierarchies for organized invalidation.
91 | *
92 | * @param string $parentKey
93 | * @param array $childKeys
94 | * @return void
95 | */
96 | public function createCacheHierarchy(string $parentKey, array $childKeys): void
97 | {
98 | foreach ($childKeys as $childKey) {
99 | $this->smartCache->dependsOn($childKey, $parentKey);
100 | }
101 | }
102 |
103 | /**
104 | * Advanced pattern matching with regex and wildcard support.
105 | *
106 | * @param string $key
107 | * @param string $pattern
108 | * @return bool
109 | */
110 | protected function matchesAdvancedPattern(string $key, string $pattern): bool
111 | {
112 | try {
113 | // Handle regex patterns (starting with /)
114 | if (str_starts_with($pattern, '/') && str_ends_with($pattern, '/')) {
115 | $result = preg_match($pattern, $key);
116 | return $result === 1;
117 | }
118 |
119 | // Handle glob patterns (* and ?)
120 | if (str_contains($pattern, '*') || str_contains($pattern, '?')) {
121 | // Convert glob pattern to regex pattern
122 | $regexPattern = preg_quote($pattern, '/');
123 | $regexPattern = str_replace(['\*', '\?'], ['.*', '.'], $regexPattern);
124 | $result = preg_match("/^{$regexPattern}$/", $key);
125 | return $result === 1;
126 | }
127 |
128 | // Exact match
129 | return $key === $pattern;
130 | } catch (\Exception $e) {
131 | // If pattern matching fails, return false to prevent errors
132 | return false;
133 | }
134 | }
135 |
136 | /**
137 | * Get cache statistics and analytics.
138 | *
139 | * @return array
140 | */
141 | public function getCacheStatistics(): array
142 | {
143 | $managedKeys = $this->smartCache->getManagedKeys();
144 | $stats = [
145 | 'managed_keys_count' => count($managedKeys),
146 | 'tag_usage' => [],
147 | 'dependency_chains' => [],
148 | 'optimization_stats' => [
149 | 'compressed' => 0,
150 | 'chunked' => 0,
151 | 'unoptimized' => 0,
152 | ],
153 | ];
154 |
155 | // Analyze optimization usage
156 | foreach ($managedKeys as $key) {
157 | $value = $this->smartCache->get($key);
158 | if (is_array($value)) {
159 | if (isset($value['_sc_compressed'])) {
160 | $stats['optimization_stats']['compressed']++;
161 | } elseif (isset($value['_sc_chunked'])) {
162 | $stats['optimization_stats']['chunked']++;
163 | } else {
164 | $stats['optimization_stats']['unoptimized']++;
165 | }
166 | } else {
167 | $stats['optimization_stats']['unoptimized']++;
168 | }
169 | }
170 |
171 | return $stats;
172 | }
173 |
174 | /**
175 | * Perform cache health check and cleanup.
176 | *
177 | * @return array
178 | */
179 | public function healthCheckAndCleanup(): array
180 | {
181 | $results = [
182 | 'orphaned_chunks_cleaned' => 0,
183 | 'broken_dependencies_fixed' => 0,
184 | 'invalid_tags_removed' => 0,
185 | 'expired_keys_cleaned' => 0,
186 | 'total_keys_checked' => 0,
187 | ];
188 |
189 | $managedKeys = $this->smartCache->getManagedKeys();
190 | $results['total_keys_checked'] = count($managedKeys);
191 |
192 | // Clean up expired managed keys first
193 | $results['expired_keys_cleaned'] = $this->smartCache->cleanupExpiredManagedKeys();
194 |
195 | // Check for orphaned chunks
196 | foreach ($managedKeys as $key) {
197 | $value = $this->smartCache->get($key);
198 | if (is_array($value) && isset($value['_sc_chunked'])) {
199 | $missingChunks = 0;
200 | foreach ($value['chunk_keys'] as $chunkKey) {
201 | if (!$this->smartCache->has($chunkKey)) {
202 | $missingChunks++;
203 | }
204 | }
205 |
206 | if ($missingChunks > 0) {
207 | // Key has missing chunks, remove it
208 | $this->smartCache->forget($key);
209 | $results['orphaned_chunks_cleaned']++;
210 | }
211 | }
212 | }
213 |
214 | return $results;
215 | }
216 | }
217 |
--------------------------------------------------------------------------------
/src/Services/SmartChunkSizeCalculator.php:
--------------------------------------------------------------------------------
1 | 512 * 1024 * 1024, // 512MB
22 | 'memcached' => 1024 * 1024, // 1MB
23 | 'file' => PHP_INT_MAX, // No practical limit
24 | 'database' => 16 * 1024 * 1024, // 16MB (typical BLOB limit)
25 | 'dynamodb' => 400 * 1024, // 400KB
26 | 'array' => PHP_INT_MAX, // No limit
27 | 'default' => 1024 * 1024, // 1MB default
28 | ];
29 |
30 | /**
31 | * Calculate optimal chunk size for the given data and driver.
32 | *
33 | * @param array $data
34 | * @param string|null $driver
35 | * @param int $defaultChunkSize
36 | * @return int
37 | */
38 | public function calculateOptimalSize(array $data, ?string $driver = null, int $defaultChunkSize = 1000): int
39 | {
40 | // Get driver limit
41 | $maxValueSize = $this->getDriverLimit($driver);
42 |
43 | // Analyze data characteristics
44 | $totalItems = count($data);
45 | $avgItemSize = $this->calculateAverageItemSize($data);
46 | $totalSize = $totalItems * $avgItemSize;
47 |
48 | // If data is small, don't chunk
49 | if ($totalSize < 100 * 1024) { // < 100KB
50 | return $totalItems; // Return all items in one chunk
51 | }
52 |
53 | // Calculate chunk size based on driver limit
54 | // Use 80% of max size to leave safety margin
55 | $safeMaxSize = (int) ($maxValueSize * 0.8);
56 | $optimalChunkSize = $avgItemSize > 0
57 | ? (int) floor($safeMaxSize / $avgItemSize)
58 | : $defaultChunkSize;
59 |
60 | // Ensure minimum chunk size
61 | $optimalChunkSize = max($optimalChunkSize, 100);
62 |
63 | // Adjust based on total data size
64 | if ($totalSize > 10 * 1024 * 1024) { // > 10MB
65 | // Use smaller chunks for very large datasets
66 | $optimalChunkSize = min($optimalChunkSize, 500);
67 | } elseif ($totalSize < 1024 * 1024) { // < 1MB
68 | // Use larger chunks for smaller datasets
69 | $optimalChunkSize = min($optimalChunkSize, 5000);
70 | }
71 |
72 | // Ensure we don't exceed total items
73 | $optimalChunkSize = min($optimalChunkSize, $totalItems);
74 |
75 | return $optimalChunkSize;
76 | }
77 |
78 | /**
79 | * Get the maximum value size for a driver.
80 | *
81 | * @param string|null $driver
82 | * @return int
83 | */
84 | public function getDriverLimit(?string $driver): int
85 | {
86 | if (!$driver) {
87 | return $this->driverLimits['default'];
88 | }
89 |
90 | return $this->driverLimits[$driver] ?? $this->driverLimits['default'];
91 | }
92 |
93 | /**
94 | * Calculate the average item size in the array.
95 | *
96 | * @param array $data
97 | * @param int $sampleSize
98 | * @return int
99 | */
100 | protected function calculateAverageItemSize(array $data, int $sampleSize = 100): int
101 | {
102 | if (empty($data)) {
103 | return 0;
104 | }
105 |
106 | $totalItems = count($data);
107 | $samplesToTake = min($sampleSize, $totalItems);
108 |
109 | // Sample random items
110 | $samples = array_rand($data, $samplesToTake);
111 | if (!is_array($samples)) {
112 | $samples = [$samples];
113 | }
114 |
115 | $totalSize = 0;
116 | foreach ($samples as $key) {
117 | $totalSize += strlen(serialize($data[$key]));
118 | }
119 |
120 | return (int) ceil($totalSize / $samplesToTake);
121 | }
122 |
123 | /**
124 | * Calculate the number of chunks needed.
125 | *
126 | * @param int $totalItems
127 | * @param int $chunkSize
128 | * @return int
129 | */
130 | public function calculateChunkCount(int $totalItems, int $chunkSize): int
131 | {
132 | return (int) ceil($totalItems / $chunkSize);
133 | }
134 |
135 | /**
136 | * Get chunking recommendations for the given data.
137 | *
138 | * @param array $data
139 | * @param string|null $driver
140 | * @return array
141 | */
142 | public function getRecommendations(array $data, ?string $driver = null): array
143 | {
144 | $totalItems = count($data);
145 | $avgItemSize = $this->calculateAverageItemSize($data);
146 | $totalSize = $totalItems * $avgItemSize;
147 | $optimalChunkSize = $this->calculateOptimalSize($data, $driver);
148 | $chunkCount = $this->calculateChunkCount($totalItems, $optimalChunkSize);
149 |
150 | return [
151 | 'total_items' => $totalItems,
152 | 'total_size' => $totalSize,
153 | 'avg_item_size' => $avgItemSize,
154 | 'optimal_chunk_size' => $optimalChunkSize,
155 | 'chunk_count' => $chunkCount,
156 | 'driver' => $driver ?? 'default',
157 | 'driver_limit' => $this->getDriverLimit($driver),
158 | 'should_chunk' => $totalSize > 100 * 1024, // Recommend chunking for > 100KB
159 | 'estimated_chunk_size' => $optimalChunkSize * $avgItemSize,
160 | ];
161 | }
162 |
163 | /**
164 | * Validate that chunk size is safe for the driver.
165 | *
166 | * @param int $chunkSize
167 | * @param int $avgItemSize
168 | * @param string|null $driver
169 | * @return bool
170 | */
171 | public function isChunkSizeSafe(int $chunkSize, int $avgItemSize, ?string $driver = null): bool
172 | {
173 | $estimatedChunkBytes = $chunkSize * $avgItemSize;
174 | $driverLimit = $this->getDriverLimit($driver);
175 |
176 | // Use 80% of limit as safety margin
177 | return $estimatedChunkBytes <= ($driverLimit * 0.8);
178 | }
179 |
180 | /**
181 | * Set custom driver limit.
182 | *
183 | * @param string $driver
184 | * @param int $limit
185 | * @return void
186 | */
187 | public function setDriverLimit(string $driver, int $limit): void
188 | {
189 | $this->driverLimits[$driver] = $limit;
190 | }
191 |
192 | /**
193 | * Get all driver limits.
194 | *
195 | * @return array
196 | */
197 | public function getDriverLimits(): array
198 | {
199 | return $this->driverLimits;
200 | }
201 | }
202 |
203 |
--------------------------------------------------------------------------------
/src/Strategies/AdaptiveCompressionStrategy.php:
--------------------------------------------------------------------------------
1 | threshold = $threshold;
67 | $this->defaultLevel = $defaultLevel;
68 | $this->sampleSize = $sampleSize;
69 | $this->highCompressionThreshold = $highCompressionThreshold;
70 | $this->lowCompressionThreshold = $lowCompressionThreshold;
71 | $this->frequencyThreshold = $frequencyThreshold;
72 | }
73 |
74 | /**
75 | * {@inheritdoc}
76 | */
77 | public function shouldApply(mixed $value, array $context = []): bool
78 | {
79 | // Check if driver supports compression
80 | if (isset($context['driver']) &&
81 | isset($context['config']['drivers'][$context['driver']]['compression']) &&
82 | $context['config']['drivers'][$context['driver']]['compression'] === false) {
83 | return false;
84 | }
85 |
86 | // Only compress strings and serializable objects/arrays
87 | if (!is_string($value) && !is_array($value) && !is_object($value)) {
88 | return false;
89 | }
90 |
91 | // Convert to string to measure size
92 | $serialized = is_string($value) ? $value : serialize($value);
93 |
94 | return strlen($serialized) > $this->threshold;
95 | }
96 |
97 | /**
98 | * {@inheritdoc}
99 | */
100 | public function optimize(mixed $value, array $context = []): mixed
101 | {
102 | $isString = is_string($value);
103 | $data = $isString ? $value : serialize($value);
104 |
105 | // Select optimal compression level
106 | $level = $this->selectCompressionLevel($data, $context);
107 |
108 | $compressed = gzencode($data, $level);
109 |
110 | return [
111 | '_sc_compressed' => true,
112 | '_sc_adaptive' => true,
113 | 'data' => base64_encode($compressed),
114 | 'is_string' => $isString,
115 | 'level' => $level,
116 | 'original_size' => strlen($data),
117 | 'compressed_size' => strlen($compressed),
118 | ];
119 | }
120 |
121 | /**
122 | * {@inheritdoc}
123 | */
124 | public function restore(mixed $value, array $context = []): mixed
125 | {
126 | if (!is_array($value) || !isset($value['_sc_compressed']) || $value['_sc_compressed'] !== true) {
127 | return $value;
128 | }
129 |
130 | $decompressed = gzdecode(base64_decode($value['data']));
131 |
132 | if ($value['is_string']) {
133 | return $decompressed;
134 | }
135 |
136 | return unserialize($decompressed);
137 | }
138 |
139 | /**
140 | * {@inheritdoc}
141 | */
142 | public function getIdentifier(): string
143 | {
144 | return 'adaptive_compression';
145 | }
146 |
147 | /**
148 | * Select the optimal compression level based on data characteristics.
149 | *
150 | * @param string $data
151 | * @param array $context
152 | * @return int
153 | */
154 | protected function selectCompressionLevel(string $data, array $context): int
155 | {
156 | $dataSize = strlen($data);
157 |
158 | // For very small data, use fast compression
159 | if ($dataSize < $this->threshold * 2) {
160 | return 3;
161 | }
162 |
163 | // Test compressibility with a sample
164 | $sampleSize = min($this->sampleSize, $dataSize);
165 | $sample = substr($data, 0, $sampleSize);
166 | $testCompressed = gzcompress($sample, $this->defaultLevel);
167 | $compressionRatio = strlen($testCompressed) / strlen($sample);
168 |
169 | // Get access frequency from context
170 | $accessFrequency = $this->getAccessFrequency($context['key'] ?? null);
171 |
172 | // Determine level based on compressibility and access frequency
173 | $level = $this->defaultLevel;
174 |
175 | // If data compresses very well, use higher compression
176 | if ($compressionRatio < $this->highCompressionThreshold) {
177 | $level = 9;
178 | }
179 | // If data doesn't compress well, use lower compression
180 | elseif ($compressionRatio > $this->lowCompressionThreshold) {
181 | $level = 3;
182 | }
183 | // Medium compressibility, use default
184 | else {
185 | $level = $this->defaultLevel;
186 | }
187 |
188 | // For frequently accessed data, prioritize speed over compression ratio
189 | if ($accessFrequency > $this->frequencyThreshold) {
190 | $level = min($level, 3);
191 | }
192 |
193 | // For very large data, consider using higher compression to save space
194 | if ($dataSize > 1024 * 1024 * 10 && $level < 9) { // > 10MB
195 | $level = min($level + 2, 9);
196 | }
197 |
198 | return $level;
199 | }
200 |
201 | /**
202 | * Get the access frequency for a key.
203 | *
204 | * @param string|null $key
205 | * @return int
206 | */
207 | protected function getAccessFrequency(?string $key): int
208 | {
209 | if (!$key) {
210 | return 0;
211 | }
212 |
213 | $frequencyKey = "_sc_access_freq_{$key}";
214 |
215 | try {
216 | return (int) Cache::get($frequencyKey, 0);
217 | } catch (\Exception $e) {
218 | return 0;
219 | }
220 | }
221 |
222 | /**
223 | * Track access frequency for a key.
224 | *
225 | * @param string $key
226 | * @return void
227 | */
228 | public function trackAccess(string $key): void
229 | {
230 | $frequencyKey = "_sc_access_freq_{$key}";
231 |
232 | try {
233 | Cache::increment($frequencyKey);
234 |
235 | // Set TTL if not already set (24 hours)
236 | if (!Cache::has($frequencyKey . '_ttl')) {
237 | Cache::put($frequencyKey . '_ttl', true, 86400);
238 | }
239 | } catch (\Exception $e) {
240 | // Silently fail if cache doesn't support increment
241 | }
242 | }
243 |
244 | /**
245 | * Get compression statistics for monitoring.
246 | *
247 | * @param mixed $optimizedValue
248 | * @return array
249 | */
250 | public function getCompressionStats(mixed $optimizedValue): array
251 | {
252 | if (!is_array($optimizedValue) || !isset($optimizedValue['_sc_adaptive'])) {
253 | return [];
254 | }
255 |
256 | return [
257 | 'level' => $optimizedValue['level'] ?? null,
258 | 'original_size' => $optimizedValue['original_size'] ?? 0,
259 | 'compressed_size' => $optimizedValue['compressed_size'] ?? 0,
260 | 'ratio' => $optimizedValue['compressed_size'] > 0
261 | ? $optimizedValue['compressed_size'] / $optimizedValue['original_size']
262 | : 0,
263 | 'savings_bytes' => ($optimizedValue['original_size'] ?? 0) - ($optimizedValue['compressed_size'] ?? 0),
264 | 'savings_percent' => $optimizedValue['original_size'] > 0
265 | ? (1 - ($optimizedValue['compressed_size'] / $optimizedValue['original_size'])) * 100
266 | : 0,
267 | ];
268 | }
269 | }
270 |
271 |
--------------------------------------------------------------------------------
/src/Strategies/ChunkingStrategy.php:
--------------------------------------------------------------------------------
1 | threshold = $threshold;
51 | $this->chunkSize = $chunkSize;
52 | $this->lazyLoading = $lazyLoading;
53 | $this->smartSizing = $smartSizing;
54 |
55 | if ($smartSizing) {
56 | $this->sizeCalculator = new SmartChunkSizeCalculator();
57 | }
58 | }
59 |
60 | /**
61 | * {@inheritdoc}
62 | */
63 | public function shouldApply(mixed $value, array $context = []): bool
64 | {
65 | // Check if driver supports chunking
66 | if (isset($context['driver']) &&
67 | isset($context['config']['drivers'][$context['driver']]['chunking']) &&
68 | $context['config']['drivers'][$context['driver']]['chunking'] === false) {
69 | return false;
70 | }
71 |
72 | // Only chunk arrays and array-like objects
73 | if (!is_array($value) && !($value instanceof \Traversable)) {
74 | return false;
75 | }
76 |
77 | // For Laravel collections, get the underlying array
78 | if (class_exists('\Illuminate\Support\Collection') && $value instanceof \Illuminate\Support\Collection) {
79 | $value = $value->all();
80 | }
81 |
82 | $serialized = serialize($value);
83 |
84 | // Check if size exceeds threshold and the array is large enough to benefit from chunking
85 | return strlen($serialized) > $this->threshold && (is_array($value) && count($value) > $this->chunkSize);
86 | }
87 |
88 | /**
89 | * {@inheritdoc}
90 | */
91 | public function optimize(mixed $value, array $context = []): mixed
92 | {
93 | // Convert to array if it's a collection
94 | if (class_exists('\Illuminate\Support\Collection') && $value instanceof \Illuminate\Support\Collection) {
95 | $isCollection = true;
96 | $value = $value->all();
97 | } else {
98 | $isCollection = false;
99 | }
100 |
101 | // Get cache instance from context
102 | $cache = $context['cache'] ?? null;
103 | $prefix = $context['key'] ?? uniqid('chunk_');
104 | $ttl = $context['ttl'] ?? null;
105 | $driver = $context['driver'] ?? null;
106 |
107 | // Calculate optimal chunk size if smart sizing is enabled
108 | $chunkSize = $this->chunkSize;
109 | if ($this->smartSizing && $this->sizeCalculator) {
110 | $chunkSize = $this->sizeCalculator->calculateOptimalSize($value, $driver, $this->chunkSize);
111 | }
112 |
113 | $chunks = array_chunk($value, $chunkSize, true);
114 | $chunkKeys = [];
115 |
116 | // Store each chunk separately
117 | foreach ($chunks as $index => $chunk) {
118 | $chunkKey = "_sc_chunk_{$prefix}_{$index}";
119 | $chunkKeys[] = $chunkKey;
120 |
121 | if ($cache) {
122 | $cache->put($chunkKey, $chunk, $ttl);
123 | }
124 | }
125 |
126 | return [
127 | '_sc_chunked' => true,
128 | 'chunk_keys' => $chunkKeys,
129 | 'total_items' => count($value),
130 | 'is_collection' => $isCollection,
131 | 'original_key' => $prefix,
132 | 'driver' => $driver,
133 | 'lazy_loading' => $this->lazyLoading,
134 | 'chunk_size' => $chunkSize, // Use actual chunk size (may be different if smart sizing is enabled)
135 | ];
136 | }
137 |
138 | /**
139 | * {@inheritdoc}
140 | */
141 | public function restore(mixed $value, array $context = []): mixed
142 | {
143 | if (!is_array($value) || !isset($value['_sc_chunked']) || $value['_sc_chunked'] !== true) {
144 | return $value;
145 | }
146 |
147 | $cache = $context['cache'] ?? null;
148 | if (!$cache) {
149 | throw new \RuntimeException('Cache repository is required to restore chunked data');
150 | }
151 |
152 | // Check if lazy loading is enabled
153 | $lazyLoading = $value['lazy_loading'] ?? $this->lazyLoading;
154 |
155 | // Get the chunk size that was used (may be different if smart sizing was enabled)
156 | $chunkSize = $value['chunk_size'] ?? $this->chunkSize;
157 |
158 | if ($lazyLoading) {
159 | // Return lazy collection
160 | return new LazyChunkedCollection(
161 | $cache,
162 | $value['chunk_keys'],
163 | $chunkSize,
164 | $value['total_items'],
165 | $value['is_collection'] ?? false
166 | );
167 | }
168 |
169 | $result = [];
170 |
171 | // Retrieve and merge all chunks (eager loading)
172 | foreach ($value['chunk_keys'] as $chunkKey) {
173 | $chunk = $cache->get($chunkKey);
174 | if ($chunk === null) {
175 | // If any chunk is missing, return null indicating cache miss
176 | return null;
177 | }
178 |
179 | $result = array_merge($result, $chunk);
180 | }
181 |
182 | // Convert back to collection if needed
183 | if ($value['is_collection'] && class_exists('\Illuminate\Support\Collection')) {
184 | return new \Illuminate\Support\Collection($result);
185 | }
186 |
187 | return $result;
188 | }
189 |
190 | /**
191 | * {@inheritdoc}
192 | */
193 | public function getIdentifier(): string
194 | {
195 | return 'chunking';
196 | }
197 | }
--------------------------------------------------------------------------------
/src/Strategies/CompressionStrategy.php:
--------------------------------------------------------------------------------
1 | threshold = $threshold;
28 | $this->level = $level;
29 | }
30 |
31 | /**
32 | * {@inheritdoc}
33 | */
34 | public function shouldApply(mixed $value, array $context = []): bool
35 | {
36 | // Check if driver supports compression
37 | if (isset($context['driver']) &&
38 | isset($context['config']['drivers'][$context['driver']]['compression']) &&
39 | $context['config']['drivers'][$context['driver']]['compression'] === false) {
40 | return false;
41 | }
42 |
43 | // Only compress strings and serializable objects/arrays
44 | if (!is_string($value) && !is_array($value) && !is_object($value)) {
45 | return false;
46 | }
47 |
48 | // Convert to string to measure size
49 | $serialized = is_string($value) ? $value : serialize($value);
50 |
51 | return strlen($serialized) > $this->threshold;
52 | }
53 |
54 | /**
55 | * {@inheritdoc}
56 | */
57 | public function optimize(mixed $value, array $context = []): mixed
58 | {
59 | $isString = is_string($value);
60 | $data = $isString ? $value : serialize($value);
61 |
62 | $compressed = gzencode($data, $this->level);
63 |
64 | return [
65 | '_sc_compressed' => true,
66 | 'data' => base64_encode($compressed),
67 | 'is_string' => $isString,
68 | ];
69 | }
70 |
71 | /**
72 | * {@inheritdoc}
73 | */
74 | public function restore(mixed $value, array $context = []): mixed
75 | {
76 | if (!is_array($value) || !isset($value['_sc_compressed']) || $value['_sc_compressed'] !== true) {
77 | return $value;
78 | }
79 |
80 | $decompressed = gzdecode(base64_decode($value['data']));
81 |
82 | if ($value['is_string']) {
83 | return $decompressed;
84 | }
85 |
86 | return unserialize($decompressed);
87 | }
88 |
89 | /**
90 | * {@inheritdoc}
91 | */
92 | public function getIdentifier(): string
93 | {
94 | return 'compression';
95 | }
96 | }
--------------------------------------------------------------------------------
/src/Strategies/SmartSerializationStrategy.php:
--------------------------------------------------------------------------------
1 | preferredMethod = $preferredMethod;
36 | $this->autoDetect = $autoDetect;
37 | }
38 |
39 | /**
40 | * {@inheritdoc}
41 | */
42 | public function shouldApply(mixed $value, array $context = []): bool
43 | {
44 | // This strategy can be applied to any value
45 | // It's more about choosing the right serialization method
46 | return true;
47 | }
48 |
49 | /**
50 | * {@inheritdoc}
51 | */
52 | public function optimize(mixed $value, array $context = []): mixed
53 | {
54 | $method = $this->selectSerializationMethod($value);
55 |
56 | return [
57 | '_sc_serialized' => true,
58 | 'method' => $method,
59 | 'data' => $this->serialize($value, $method),
60 | ];
61 | }
62 |
63 | /**
64 | * {@inheritdoc}
65 | */
66 | public function restore(mixed $value, array $context = []): mixed
67 | {
68 | if (!is_array($value) || !isset($value['_sc_serialized']) || $value['_sc_serialized'] !== true) {
69 | return $value;
70 | }
71 |
72 | return $this->unserialize($value['data'], $value['method']);
73 | }
74 |
75 | /**
76 | * {@inheritdoc}
77 | */
78 | public function getIdentifier(): string
79 | {
80 | return 'smart_serialization';
81 | }
82 |
83 | /**
84 | * Select the best serialization method for the given value.
85 | *
86 | * @param mixed $value
87 | * @return string
88 | */
89 | protected function selectSerializationMethod(mixed $value): string
90 | {
91 | // If not auto-detecting, use preferred method
92 | if (!$this->autoDetect) {
93 | return $this->getAvailableMethod($this->preferredMethod);
94 | }
95 |
96 | // Auto-detect best method
97 |
98 | // Try JSON first for simple data (fastest and most compact)
99 | if ($this->isJsonSafe($value)) {
100 | return 'json';
101 | }
102 |
103 | // Use igbinary if available (more compact than PHP serialize)
104 | if (function_exists('igbinary_serialize')) {
105 | return 'igbinary';
106 | }
107 |
108 | // Fallback to PHP serialize
109 | return 'php';
110 | }
111 |
112 | /**
113 | * Check if a value can be safely serialized with JSON.
114 | *
115 | * @param mixed $value
116 | * @return bool
117 | */
118 | protected function isJsonSafe(mixed $value): bool
119 | {
120 | // JSON can't handle objects (except stdClass), resources, or closures
121 | if (is_object($value) && !($value instanceof \stdClass)) {
122 | return false;
123 | }
124 |
125 | if (is_resource($value) || $value instanceof \Closure) {
126 | return false;
127 | }
128 |
129 | // For arrays, check recursively
130 | if (is_array($value)) {
131 | foreach ($value as $item) {
132 | if (!$this->isJsonSafe($item)) {
133 | return false;
134 | }
135 | }
136 | }
137 |
138 | // Try encoding to verify
139 | $encoded = json_encode($value);
140 | if ($encoded === false || json_last_error() !== JSON_ERROR_NONE) {
141 | return false;
142 | }
143 |
144 | return true;
145 | }
146 |
147 | /**
148 | * Get an available serialization method.
149 | *
150 | * @param string $method
151 | * @return string
152 | */
153 | protected function getAvailableMethod(string $method): string
154 | {
155 | if ($method === 'auto') {
156 | return 'php';
157 | }
158 |
159 | if ($method === 'igbinary' && !function_exists('igbinary_serialize')) {
160 | return 'php';
161 | }
162 |
163 | return $method;
164 | }
165 |
166 | /**
167 | * Serialize a value using the specified method.
168 | *
169 | * @param mixed $value
170 | * @param string $method
171 | * @return string
172 | */
173 | protected function serialize(mixed $value, string $method): string
174 | {
175 | return match ($method) {
176 | 'json' => json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
177 | 'igbinary' => base64_encode(igbinary_serialize($value)),
178 | 'php' => base64_encode(serialize($value)),
179 | default => base64_encode(serialize($value)),
180 | };
181 | }
182 |
183 | /**
184 | * Unserialize a value using the specified method.
185 | *
186 | * @param string $data
187 | * @param string $method
188 | * @return mixed
189 | */
190 | protected function unserialize(string $data, string $method): mixed
191 | {
192 | return match ($method) {
193 | 'json' => json_decode($data, true),
194 | 'igbinary' => igbinary_unserialize(base64_decode($data)),
195 | 'php' => unserialize(base64_decode($data)),
196 | default => unserialize(base64_decode($data)),
197 | };
198 | }
199 |
200 | /**
201 | * Get serialization statistics for monitoring.
202 | *
203 | * @param mixed $value
204 | * @return array
205 | */
206 | public function getSerializationStats(mixed $value): array
207 | {
208 | $stats = [];
209 |
210 | // Test JSON
211 | if ($this->isJsonSafe($value)) {
212 | $jsonData = json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
213 | $stats['json'] = [
214 | 'size' => strlen($jsonData),
215 | 'available' => true,
216 | ];
217 | } else {
218 | $stats['json'] = [
219 | 'size' => null,
220 | 'available' => false,
221 | ];
222 | }
223 |
224 | // Test igbinary
225 | if (function_exists('igbinary_serialize')) {
226 | $igbinaryData = igbinary_serialize($value);
227 | $stats['igbinary'] = [
228 | 'size' => strlen($igbinaryData),
229 | 'available' => true,
230 | ];
231 | } else {
232 | $stats['igbinary'] = [
233 | 'size' => null,
234 | 'available' => false,
235 | ];
236 | }
237 |
238 | // Test PHP serialize
239 | $phpData = serialize($value);
240 | $stats['php'] = [
241 | 'size' => strlen($phpData),
242 | 'available' => true,
243 | ];
244 |
245 | // Determine best method
246 | $bestMethod = 'php';
247 | $bestSize = $stats['php']['size'];
248 |
249 | if ($stats['json']['available'] && $stats['json']['size'] < $bestSize) {
250 | $bestMethod = 'json';
251 | $bestSize = $stats['json']['size'];
252 | }
253 |
254 | if ($stats['igbinary']['available'] && $stats['igbinary']['size'] < $bestSize) {
255 | $bestMethod = 'igbinary';
256 | $bestSize = $stats['igbinary']['size'];
257 | }
258 |
259 | $stats['recommended'] = $bestMethod;
260 | $stats['best_size'] = $bestSize;
261 |
262 | return $stats;
263 | }
264 | }
265 |
266 |
--------------------------------------------------------------------------------
/src/Traits/CacheInvalidation.php:
--------------------------------------------------------------------------------
1 | [],
17 | 'tags' => [],
18 | 'patterns' => [],
19 | 'dependencies' => [],
20 | ];
21 |
22 | /**
23 | * Boot the cache invalidation trait.
24 | */
25 | protected static function bootCacheInvalidation(): void
26 | {
27 | static::observe(CacheInvalidationObserver::class);
28 | }
29 |
30 | /**
31 | * Define cache keys to invalidate when this model changes.
32 | *
33 | * @param array $keys
34 | * @return static
35 | */
36 | public function invalidatesKeys(array $keys): static
37 | {
38 | $this->cacheInvalidation['keys'] = array_merge($this->cacheInvalidation['keys'], $keys);
39 | return $this;
40 | }
41 |
42 | /**
43 | * Define cache tags to flush when this model changes.
44 | *
45 | * @param array $tags
46 | * @return static
47 | */
48 | public function invalidatesTags(array $tags): static
49 | {
50 | $this->cacheInvalidation['tags'] = array_merge($this->cacheInvalidation['tags'], $tags);
51 | return $this;
52 | }
53 |
54 | /**
55 | * Define cache key patterns to invalidate when this model changes.
56 | *
57 | * @param array $patterns
58 | * @return static
59 | */
60 | public function invalidatesPatterns(array $patterns): static
61 | {
62 | $this->cacheInvalidation['patterns'] = array_merge($this->cacheInvalidation['patterns'], $patterns);
63 | return $this;
64 | }
65 |
66 | /**
67 | * Define cache dependencies when this model changes.
68 | *
69 | * @param array $dependencies
70 | * @return static
71 | */
72 | public function invalidatesDependencies(array $dependencies): static
73 | {
74 | $this->cacheInvalidation['dependencies'] = array_merge($this->cacheInvalidation['dependencies'], $dependencies);
75 | return $this;
76 | }
77 |
78 | /**
79 | * Get cache invalidation configuration.
80 | *
81 | * @return array
82 | */
83 | public function getCacheInvalidationConfig(): array
84 | {
85 | return $this->cacheInvalidation;
86 | }
87 |
88 | /**
89 | * Get cache keys to invalidate for this model instance.
90 | * Override this method to define dynamic cache keys.
91 | *
92 | * @return array
93 | */
94 | public function getCacheKeysToInvalidate(): array
95 | {
96 | $keys = $this->cacheInvalidation['keys'];
97 |
98 | // Add dynamic keys based on model attributes
99 | $dynamicKeys = [];
100 | foreach ($keys as $key) {
101 | // Replace placeholders like {id}, {slug}, etc.
102 | $dynamicKey = preg_replace_callback('/\{(\w+)\}/', function ($matches) {
103 | $attribute = $matches[1];
104 | return $this->getAttribute($attribute) ?? $matches[0];
105 | }, $key);
106 | $dynamicKeys[] = $dynamicKey;
107 | }
108 |
109 | return array_merge($keys, $dynamicKeys);
110 | }
111 |
112 | /**
113 | * Get cache tags to flush for this model instance.
114 | * Override this method to define dynamic cache tags.
115 | *
116 | * @return array
117 | */
118 | public function getCacheTagsToFlush(): array
119 | {
120 | $tags = $this->cacheInvalidation['tags'];
121 |
122 | // Add dynamic tags based on model attributes
123 | $dynamicTags = [];
124 | foreach ($tags as $tag) {
125 | // Replace placeholders like {id}, {category_id}, etc.
126 | $dynamicTag = preg_replace_callback('/\{(\w+)\}/', function ($matches) {
127 | $attribute = $matches[1];
128 | return $this->getAttribute($attribute) ?? $matches[0];
129 | }, $tag);
130 | $dynamicTags[] = $dynamicTag;
131 | }
132 |
133 | return array_merge($tags, $dynamicTags);
134 | }
135 |
136 | /**
137 | * Perform cache invalidation.
138 | *
139 | * @return void
140 | */
141 | public function performCacheInvalidation(): void
142 | {
143 | // Invalidate specific keys
144 | foreach ($this->getCacheKeysToInvalidate() as $key) {
145 | SmartCache::forget($key);
146 | }
147 |
148 | // Flush tags
149 | $tagsToFlush = $this->getCacheTagsToFlush();
150 | if (!empty($tagsToFlush)) {
151 | SmartCache::flushTags($tagsToFlush);
152 | }
153 |
154 | // Handle patterns (basic wildcard support)
155 | foreach ($this->cacheInvalidation['patterns'] as $pattern) {
156 | $this->invalidatePattern($pattern);
157 | }
158 |
159 | // Handle dependencies
160 | foreach ($this->cacheInvalidation['dependencies'] as $dependency) {
161 | SmartCache::invalidate($dependency);
162 | }
163 | }
164 |
165 | /**
166 | * Invalidate cache keys matching a pattern.
167 | *
168 | * @param string $pattern
169 | * @return void
170 | */
171 | protected function invalidatePattern(string $pattern): void
172 | {
173 | // Replace model placeholders
174 | $pattern = preg_replace_callback('/\{(\w+)\}/', function ($matches) {
175 | $attribute = $matches[1];
176 | return $this->getAttribute($attribute) ?? $matches[0];
177 | }, $pattern);
178 |
179 | // This is a simplified pattern matching
180 | // In a real implementation, you might want to use the cache store's
181 | // native pattern matching capabilities (like Redis KEYS command)
182 | $managedKeys = SmartCache::getManagedKeys();
183 |
184 | foreach ($managedKeys as $key) {
185 | if ($this->matchesPattern($key, $pattern)) {
186 | SmartCache::forget($key);
187 | }
188 | }
189 | }
190 |
191 | /**
192 | * Check if a key matches a pattern with basic wildcard support.
193 | *
194 | * @param string $key
195 | * @param string $pattern
196 | * @return bool
197 | */
198 | protected function matchesPattern(string $key, string $pattern): bool
199 | {
200 | // Convert simple wildcard pattern to regex
201 | $regexPattern = str_replace(['*', '?'], ['.*', '.'], preg_quote($pattern, '/'));
202 | return (bool) preg_match("/^{$regexPattern}$/", $key);
203 | }
204 | }
205 |
--------------------------------------------------------------------------------
/src/Traits/DispatchesCacheEvents.php:
--------------------------------------------------------------------------------
1 | shouldDispatchEvent('cache_hit')) {
24 | Event::dispatch(new CacheHit($key, $value, $this->activeTags));
25 | }
26 | }
27 |
28 | /**
29 | * Dispatch a cache missed event.
30 | *
31 | * @param string $key
32 | * @return void
33 | */
34 | protected function dispatchCacheMissed(string $key): void
35 | {
36 | if ($this->shouldDispatchEvent('cache_missed')) {
37 | Event::dispatch(new CacheMissed($key, $this->activeTags));
38 | }
39 | }
40 |
41 | /**
42 | * Dispatch a key written event.
43 | *
44 | * @param string $key
45 | * @param mixed $value
46 | * @param int|null $seconds
47 | * @return void
48 | */
49 | protected function dispatchKeyWritten(string $key, mixed $value, ?int $seconds = null): void
50 | {
51 | if ($this->shouldDispatchEvent('key_written')) {
52 | Event::dispatch(new KeyWritten($key, $value, $seconds, $this->activeTags));
53 | }
54 | }
55 |
56 | /**
57 | * Dispatch a key forgotten event.
58 | *
59 | * @param string $key
60 | * @return void
61 | */
62 | protected function dispatchKeyForgotten(string $key): void
63 | {
64 | if ($this->shouldDispatchEvent('key_forgotten')) {
65 | Event::dispatch(new KeyForgotten($key, $this->activeTags));
66 | }
67 | }
68 |
69 | /**
70 | * Dispatch an optimization applied event.
71 | *
72 | * @param string $key
73 | * @param string $strategy
74 | * @param int $originalSize
75 | * @param int $optimizedSize
76 | * @return void
77 | */
78 | protected function dispatchOptimizationApplied(string $key, string $strategy, int $originalSize, int $optimizedSize): void
79 | {
80 | if ($this->shouldDispatchEvent('optimization_applied')) {
81 | Event::dispatch(new OptimizationApplied($key, $strategy, $originalSize, $optimizedSize));
82 | }
83 | }
84 |
85 | /**
86 | * Determine if an event should be dispatched.
87 | *
88 | * @param string $eventType
89 | * @return bool
90 | */
91 | protected function shouldDispatchEvent(string $eventType): bool
92 | {
93 | if (!$this->config->get('smart-cache.events.enabled', false)) {
94 | return false;
95 | }
96 |
97 | return $this->config->get("smart-cache.events.dispatch.{$eventType}", true);
98 | }
99 | }
100 |
101 |
--------------------------------------------------------------------------------
/src/Traits/HasLocks.php:
--------------------------------------------------------------------------------
1 | cache->getStore();
21 |
22 | if (!$store instanceof LockProvider) {
23 | throw new \RuntimeException(
24 | "Cache store [" . get_class($store) . "] does not support atomic locks. " .
25 | "Please use a cache driver that implements LockProvider (redis, memcached, dynamodb, database, file, array)."
26 | );
27 | }
28 |
29 | return $store->lock($name, $seconds, $owner);
30 | }
31 |
32 | /**
33 | * Restore a lock instance using the owner identifier.
34 | *
35 | * @param string $name
36 | * @param string $owner
37 | * @return Lock
38 | */
39 | public function restoreLock(string $name, string $owner): Lock
40 | {
41 | $store = $this->cache->getStore();
42 |
43 | if (!$store instanceof LockProvider) {
44 | throw new \RuntimeException(
45 | "Cache store [" . get_class($store) . "] does not support atomic locks."
46 | );
47 | }
48 |
49 | return $store->restoreLock($name, $owner);
50 | }
51 | }
52 |
53 |
--------------------------------------------------------------------------------
/src/helpers.php:
--------------------------------------------------------------------------------
1 | make('smart-cache');
22 | }
23 |
24 | if (is_string($arguments[0])) {
25 | return SmartCache::get(...$arguments);
26 | }
27 |
28 | if (!is_array($arguments[0])) {
29 | throw new InvalidArgumentException(
30 | 'When using smart_cache(), the first argument must be an array of key / value pairs or a string.'
31 | );
32 | }
33 |
34 | return SmartCache::put(key($arguments[0]), reset($arguments[0]), $arguments[1] ?? null);
35 | }
36 | }
--------------------------------------------------------------------------------
/tests/TestCase.php:
--------------------------------------------------------------------------------
1 | loadPackageConfiguration();
24 | }
25 |
26 | /**
27 | * Get package providers.
28 | *
29 | * @param \Illuminate\Foundation\Application $app
30 | * @return array
31 | */
32 | protected function getPackageProviders($app): array
33 | {
34 | return [
35 | SmartCacheServiceProvider::class,
36 | ];
37 | }
38 |
39 | /**
40 | * Define aliases.
41 | *
42 | * @param \Illuminate\Foundation\Application $app
43 | * @return array
44 | */
45 | protected function getPackageAliases($app): array
46 | {
47 | return [
48 | 'SmartCache' => \SmartCache\Facades\SmartCache::class,
49 | ];
50 | }
51 |
52 | /**
53 | * Define environment setup.
54 | *
55 | * @param \Illuminate\Foundation\Application $app
56 | */
57 | protected function defineEnvironment($app): void
58 | {
59 | // Setup the application environment
60 | $app['config']->set('app.key', 'base64:' . base64_encode(random_bytes(32)));
61 | $app['config']->set('app.debug', true);
62 |
63 | // Setup cache configuration
64 | $app['config']->set('cache.default', 'array');
65 | $app['config']->set('cache.stores.array', [
66 | 'driver' => 'array',
67 | 'serialize' => false,
68 | ]);
69 |
70 | $app['config']->set('cache.stores.file', [
71 | 'driver' => 'file',
72 | 'path' => __DIR__ . '/storage/framework/cache',
73 | ]);
74 |
75 | // Setup SmartCache configuration
76 | $app['config']->set('smart-cache', [
77 | 'strategies' => [
78 | 'compression' => [
79 | 'enabled' => true,
80 | 'level' => 6,
81 | ],
82 | 'chunking' => [
83 | 'enabled' => true,
84 | 'chunk_size' => 1000,
85 | ],
86 | ],
87 | 'thresholds' => [
88 | 'compression' => 1024, // 1KB for testing (lower than default)
89 | 'chunking' => 2048, // 2KB for testing (lower than default)
90 | ],
91 | 'fallback' => [
92 | 'enabled' => true,
93 | 'log_errors' => false, // Disable logging in tests
94 | ],
95 | ]);
96 | }
97 |
98 | /**
99 | * Load package configuration for testing.
100 | */
101 | protected function loadPackageConfiguration(): void
102 | {
103 | // This method can be overridden in individual test classes
104 | // to provide specific configuration for different test scenarios
105 | }
106 |
107 | /**
108 | * Get a cache manager instance for testing.
109 | */
110 | protected function getCacheManager(): CacheManager
111 | {
112 | return $this->app['cache'];
113 | }
114 |
115 | /**
116 | * Get a specific cache store for testing.
117 | */
118 | protected function getCacheStore(string|null $store = null)
119 | {
120 | return $this->getCacheManager()->store($store);
121 | }
122 |
123 | /**
124 | * Create a large test dataset for optimization testing.
125 | */
126 | protected function createLargeTestData(int $size = 2000): array
127 | {
128 | $data = [];
129 | for ($i = 0; $i < $size; $i++) {
130 | $data[] = [
131 | 'id' => $i,
132 | 'name' => 'Test Item ' . $i,
133 | 'description' => str_repeat('Lorem ipsum dolor sit amet, consectetur adipiscing elit. ', 10),
134 | 'metadata' => [
135 | 'created_at' => now()->toISOString(),
136 | 'tags' => ['tag1', 'tag2', 'tag3'],
137 | 'nested' => [
138 | 'level1' => [
139 | 'level2' => [
140 | 'data' => str_repeat('nested data ', 20)
141 | ]
142 | ]
143 | ]
144 | ]
145 | ];
146 | }
147 |
148 | return $data;
149 | }
150 |
151 | /**
152 | * Create test data that should trigger compression.
153 | */
154 | protected function createCompressibleData(): string
155 | {
156 | // Create repetitive string that compresses well
157 | return str_repeat('This is test data that should compress well. ', 100);
158 | }
159 |
160 | /**
161 | * Create test data that should trigger chunking.
162 | */
163 | protected function createChunkableData(): array
164 | {
165 | return $this->createLargeTestData(1200); // Creates much larger data to ensure chunking threshold is met
166 | }
167 |
168 | /**
169 | * Assert that a value is optimized (not the original value).
170 | */
171 | protected function assertValueIsOptimized($original, $cached): void
172 | {
173 | $this->assertNotEquals($original, $cached, 'Value should be optimized and different from original');
174 | }
175 |
176 | /**
177 | * Assert that a value has compression metadata.
178 | */
179 | protected function assertValueIsCompressed($value): void
180 | {
181 | $this->assertIsArray($value, 'Compressed value should be an array');
182 | $this->assertArrayHasKey('_sc_compressed', $value, 'Compressed value should have compression marker');
183 | $this->assertTrue($value['_sc_compressed'], 'Compression marker should be true');
184 | $this->assertArrayHasKey('data', $value, 'Compressed value should have data key');
185 | }
186 |
187 | /**
188 | * Assert that a value has chunking metadata.
189 | */
190 | protected function assertValueIsChunked($value): void
191 | {
192 | $this->assertIsArray($value, 'Chunked value should be an array');
193 | $this->assertArrayHasKey('_sc_chunked', $value, 'Chunked value should have chunking marker');
194 | $this->assertTrue($value['_sc_chunked'], 'Chunking marker should be true');
195 | $this->assertArrayHasKey('chunk_keys', $value, 'Chunked value should have chunk_keys');
196 | $this->assertArrayHasKey('total_items', $value, 'Chunked value should have total_items');
197 | }
198 | }
199 |
--------------------------------------------------------------------------------
/tests/Unit/BatchOperationsTest.php:
--------------------------------------------------------------------------------
1 | assertEquals([
19 | 'key1' => 'value1',
20 | 'key2' => 'value2',
21 | 'key3' => 'value3',
22 | ], $values);
23 | }
24 |
25 | public function test_many_handles_missing_keys()
26 | {
27 | SmartCache::put('key1', 'value1', 60);
28 |
29 | $values = SmartCache::many(['key1', 'key2', 'key3']);
30 |
31 | $this->assertEquals('value1', $values['key1']);
32 | $this->assertNull($values['key2']);
33 | $this->assertNull($values['key3']);
34 | }
35 |
36 | public function test_put_many_stores_multiple_keys()
37 | {
38 | $result = SmartCache::putMany([
39 | 'key1' => 'value1',
40 | 'key2' => 'value2',
41 | 'key3' => 'value3',
42 | ], 60);
43 |
44 | $this->assertTrue($result);
45 | $this->assertEquals('value1', SmartCache::get('key1'));
46 | $this->assertEquals('value2', SmartCache::get('key2'));
47 | $this->assertEquals('value3', SmartCache::get('key3'));
48 | }
49 |
50 | public function test_put_many_with_large_data()
51 | {
52 | $data = [];
53 | for ($i = 0; $i < 100; $i++) {
54 | $data["key_{$i}"] = array_fill(0, 1000, "value_{$i}");
55 | }
56 |
57 | $result = SmartCache::putMany($data, 60);
58 |
59 | $this->assertTrue($result);
60 |
61 | // Verify a few keys
62 | $this->assertCount(1000, SmartCache::get('key_0'));
63 | $this->assertCount(1000, SmartCache::get('key_50'));
64 | $this->assertCount(1000, SmartCache::get('key_99'));
65 | }
66 |
67 | public function test_delete_multiple_removes_multiple_keys()
68 | {
69 | SmartCache::put('key1', 'value1', 60);
70 | SmartCache::put('key2', 'value2', 60);
71 | SmartCache::put('key3', 'value3', 60);
72 |
73 | $result = SmartCache::deleteMultiple(['key1', 'key2']);
74 |
75 | $this->assertTrue($result);
76 | $this->assertNull(SmartCache::get('key1'));
77 | $this->assertNull(SmartCache::get('key2'));
78 | $this->assertEquals('value3', SmartCache::get('key3'));
79 | }
80 |
81 | public function test_delete_multiple_handles_nonexistent_keys()
82 | {
83 | SmartCache::put('key1', 'value1', 60);
84 |
85 | $result = SmartCache::deleteMultiple(['key1', 'key2', 'key3']);
86 |
87 | // Returns false if any key doesn't exist
88 | $this->assertFalse($result);
89 | // But key1 should still be deleted
90 | $this->assertNull(SmartCache::get('key1'));
91 | }
92 |
93 | public function test_many_with_optimized_data()
94 | {
95 | // Store large data that will be optimized
96 | $largeData1 = array_fill(0, 10000, 'data1');
97 | $largeData2 = array_fill(0, 10000, 'data2');
98 | $largeData3 = array_fill(0, 10000, 'data3');
99 |
100 | SmartCache::put('large1', $largeData1, 60);
101 | SmartCache::put('large2', $largeData2, 60);
102 | SmartCache::put('large3', $largeData3, 60);
103 |
104 | $values = SmartCache::many(['large1', 'large2', 'large3']);
105 |
106 | $this->assertCount(10000, $values['large1']);
107 | $this->assertCount(10000, $values['large2']);
108 | $this->assertCount(10000, $values['large3']);
109 | }
110 |
111 | public function test_put_many_with_different_ttls()
112 | {
113 | SmartCache::putMany([
114 | 'key1' => 'value1',
115 | 'key2' => 'value2',
116 | ], 60);
117 |
118 | SmartCache::putMany([
119 | 'key3' => 'value3',
120 | 'key4' => 'value4',
121 | ], 120);
122 |
123 | $this->assertEquals('value1', SmartCache::get('key1'));
124 | $this->assertEquals('value2', SmartCache::get('key2'));
125 | $this->assertEquals('value3', SmartCache::get('key3'));
126 | $this->assertEquals('value4', SmartCache::get('key4'));
127 | }
128 |
129 | public function test_many_operations_performance()
130 | {
131 | // Prepare data
132 | $keys = [];
133 | $data = [];
134 | for ($i = 0; $i < 100; $i++) {
135 | $key = "key_{$i}";
136 | $keys[] = $key;
137 | $data[$key] = "value_{$i}";
138 | }
139 |
140 | // Store data
141 | SmartCache::putMany($data, 60);
142 |
143 | // Measure many() performance
144 | $start = microtime(true);
145 | $values = SmartCache::many($keys);
146 | $duration = microtime(true) - $start;
147 |
148 | $this->assertCount(100, $values);
149 | // Should be reasonably fast (< 100ms)
150 | $this->assertLessThan(0.1, $duration);
151 | }
152 |
153 | public function test_batch_operations_with_tags()
154 | {
155 | SmartCache::tags(['users'])->putMany([
156 | 'user1' => 'John',
157 | 'user2' => 'Jane',
158 | 'user3' => 'Bob',
159 | ], 60);
160 |
161 | $values = SmartCache::tags(['users'])->many(['user1', 'user2', 'user3']);
162 |
163 | $this->assertEquals([
164 | 'user1' => 'John',
165 | 'user2' => 'Jane',
166 | 'user3' => 'Bob',
167 | ], $values);
168 |
169 | // Flush by tag
170 | SmartCache::tags(['users'])->flush();
171 |
172 | $values = SmartCache::many(['user1', 'user2', 'user3']);
173 | $this->assertNull($values['user1']);
174 | $this->assertNull($values['user2']);
175 | $this->assertNull($values['user3']);
176 | }
177 | }
178 |
179 |
--------------------------------------------------------------------------------
/tests/Unit/Collections/LazyChunkedCollectionTest.php:
--------------------------------------------------------------------------------
1 | app['cache']->store();
14 |
15 | // Create chunks
16 | $cache->put('chunk_0', range(0, 99), 60);
17 | $cache->put('chunk_1', range(100, 199), 60);
18 | $cache->put('chunk_2', range(200, 299), 60);
19 |
20 | $collection = new LazyChunkedCollection(
21 | $cache,
22 | ['chunk_0', 'chunk_1', 'chunk_2'],
23 | 100,
24 | 300
25 | );
26 |
27 | $this->assertCount(300, $collection);
28 | }
29 |
30 | public function test_lazy_collection_iteration()
31 | {
32 | $cache = $this->app['cache']->store();
33 |
34 | $cache->put('chunk_0', ['a', 'b', 'c'], 60);
35 | $cache->put('chunk_1', ['d', 'e', 'f'], 60);
36 |
37 | $collection = new LazyChunkedCollection(
38 | $cache,
39 | ['chunk_0', 'chunk_1'],
40 | 3,
41 | 6
42 | );
43 |
44 | $items = [];
45 | foreach ($collection as $item) {
46 | $items[] = $item;
47 | }
48 |
49 | $this->assertEquals(['a', 'b', 'c', 'd', 'e', 'f'], $items);
50 | }
51 |
52 | public function test_lazy_collection_array_access()
53 | {
54 | $cache = $this->app['cache']->store();
55 |
56 | $cache->put('chunk_0', range(0, 99), 60);
57 | $cache->put('chunk_1', range(100, 199), 60);
58 |
59 | $collection = new LazyChunkedCollection(
60 | $cache,
61 | ['chunk_0', 'chunk_1'],
62 | 100,
63 | 200
64 | );
65 |
66 | $this->assertEquals(0, $collection[0]);
67 | $this->assertEquals(50, $collection[50]);
68 | $this->assertEquals(100, $collection[100]);
69 | $this->assertEquals(150, $collection[150]);
70 | }
71 |
72 | public function test_lazy_collection_to_array()
73 | {
74 | $cache = $this->app['cache']->store();
75 |
76 | $cache->put('chunk_0', ['a', 'b'], 60);
77 | $cache->put('chunk_1', ['c', 'd'], 60);
78 |
79 | $collection = new LazyChunkedCollection(
80 | $cache,
81 | ['chunk_0', 'chunk_1'],
82 | 2,
83 | 4
84 | );
85 |
86 | $array = $collection->toArray();
87 |
88 | $this->assertEquals(['a', 'b', 'c', 'd'], $array);
89 | }
90 |
91 | public function test_lazy_collection_slice()
92 | {
93 | $cache = $this->app['cache']->store();
94 |
95 | $cache->put('chunk_0', range(0, 99), 60);
96 | $cache->put('chunk_1', range(100, 199), 60);
97 | $cache->put('chunk_2', range(200, 299), 60);
98 |
99 | $collection = new LazyChunkedCollection(
100 | $cache,
101 | ['chunk_0', 'chunk_1', 'chunk_2'],
102 | 100,
103 | 300
104 | );
105 |
106 | $slice = $collection->slice(50, 10);
107 |
108 | $this->assertEquals(range(50, 59), $slice);
109 | }
110 |
111 | public function test_lazy_collection_each()
112 | {
113 | $cache = $this->app['cache']->store();
114 |
115 | $cache->put('chunk_0', [1, 2, 3], 60);
116 | $cache->put('chunk_1', [4, 5, 6], 60);
117 |
118 | $collection = new LazyChunkedCollection(
119 | $cache,
120 | ['chunk_0', 'chunk_1'],
121 | 3,
122 | 6
123 | );
124 |
125 | $sum = 0;
126 | $collection->each(function($item) use (&$sum) {
127 | $sum += $item;
128 | });
129 |
130 | $this->assertEquals(21, $sum); // 1+2+3+4+5+6
131 | }
132 |
133 | public function test_lazy_collection_filter()
134 | {
135 | $cache = $this->app['cache']->store();
136 |
137 | $cache->put('chunk_0', range(1, 10), 60);
138 | $cache->put('chunk_1', range(11, 20), 60);
139 |
140 | $collection = new LazyChunkedCollection(
141 | $cache,
142 | ['chunk_0', 'chunk_1'],
143 | 10,
144 | 20
145 | );
146 |
147 | $filtered = $collection->filter(function($item) {
148 | return $item % 2 === 0; // Even numbers only
149 | });
150 |
151 | $this->assertEquals([2, 4, 6, 8, 10, 12, 14, 16, 18, 20], array_values($filtered));
152 | }
153 |
154 | public function test_lazy_collection_map()
155 | {
156 | $cache = $this->app['cache']->store();
157 |
158 | $cache->put('chunk_0', [1, 2, 3], 60);
159 | $cache->put('chunk_1', [4, 5, 6], 60);
160 |
161 | $collection = new LazyChunkedCollection(
162 | $cache,
163 | ['chunk_0', 'chunk_1'],
164 | 3,
165 | 6
166 | );
167 |
168 | $mapped = $collection->map(function($item) {
169 | return $item * 2;
170 | });
171 |
172 | $this->assertEquals([2, 4, 6, 8, 10, 12], $mapped);
173 | }
174 |
175 | public function test_lazy_collection_memory_efficiency()
176 | {
177 | $cache = $this->app['cache']->store();
178 |
179 | // Create many large chunks
180 | for ($i = 0; $i < 10; $i++) {
181 | $cache->put("chunk_{$i}", array_fill(0, 1000, "data_{$i}"), 60);
182 | }
183 |
184 | $chunkKeys = array_map(fn($i) => "chunk_{$i}", range(0, 9));
185 |
186 | $collection = new LazyChunkedCollection(
187 | $cache,
188 | $chunkKeys,
189 | 1000,
190 | 10000
191 | );
192 |
193 | // Access only a few items
194 | $item1 = $collection[0];
195 | $item2 = $collection[5000];
196 |
197 | // Get memory stats
198 | $stats = $collection->getMemoryStats();
199 |
200 | // Should have loaded only a few chunks, not all 10
201 | $this->assertLessThan(10, $stats['loaded_chunks']);
202 | $this->assertEquals(10, $stats['total_chunks']);
203 | }
204 |
205 | public function test_lazy_collection_with_smart_cache()
206 | {
207 | // This test requires lazy loading to be enabled at service provider level
208 | // For now, we'll test the LazyChunkedCollection directly
209 | $cache = $this->app['cache']->store();
210 |
211 | // Create chunks manually
212 | $largeData = array_fill(0, 10000, 'test_data');
213 | $chunks = array_chunk($largeData, 1000);
214 | $chunkKeys = [];
215 |
216 | foreach ($chunks as $index => $chunk) {
217 | $key = "large_key_chunk_{$index}";
218 | $cache->put($key, $chunk, 60);
219 | $chunkKeys[] = $key;
220 | }
221 |
222 | // Create lazy collection
223 | $retrieved = new LazyChunkedCollection($cache, $chunkKeys, 1000, 10000);
224 |
225 | // Should be able to access items
226 | $this->assertEquals('test_data', $retrieved[0]);
227 | $this->assertEquals('test_data', $retrieved[5000]);
228 | $this->assertCount(10000, $retrieved);
229 | }
230 |
231 | public function test_lazy_collection_to_collection()
232 | {
233 | $cache = $this->app['cache']->store();
234 |
235 | $cache->put('chunk_0', ['a', 'b'], 60);
236 | $cache->put('chunk_1', ['c', 'd'], 60);
237 |
238 | $lazyCollection = new LazyChunkedCollection(
239 | $cache,
240 | ['chunk_0', 'chunk_1'],
241 | 2,
242 | 4,
243 | true // is_collection
244 | );
245 |
246 | $collection = $lazyCollection->toCollection();
247 |
248 | $this->assertInstanceOf(\Illuminate\Support\Collection::class, $collection);
249 | $this->assertEquals(['a', 'b', 'c', 'd'], $collection->toArray());
250 | }
251 |
252 | public function test_lazy_collection_chunk_limit()
253 | {
254 | $cache = $this->app['cache']->store();
255 |
256 | // Create 10 chunks
257 | for ($i = 0; $i < 10; $i++) {
258 | $cache->put("chunk_{$i}", range($i * 100, ($i + 1) * 100 - 1), 60);
259 | }
260 |
261 | $chunkKeys = array_map(fn($i) => "chunk_{$i}", range(0, 9));
262 |
263 | $collection = new LazyChunkedCollection(
264 | $cache,
265 | $chunkKeys,
266 | 100,
267 | 1000,
268 | false,
269 | 3 // Max 3 chunks in memory
270 | );
271 |
272 | // Access items from different chunks
273 | $collection[0]; // Chunk 0
274 | $collection[250]; // Chunk 2
275 | $collection[500]; // Chunk 5
276 | $collection[750]; // Chunk 7
277 |
278 | $stats = $collection->getMemoryStats();
279 |
280 | // Should keep only 3 chunks in memory
281 | $this->assertLessThanOrEqual(3, $stats['loaded_chunks']);
282 | }
283 | }
284 |
285 |
--------------------------------------------------------------------------------
/tests/Unit/Drivers/MemoizationTest.php:
--------------------------------------------------------------------------------
1 | assertInstanceOf(\SmartCache\SmartCache::class, $memo);
15 | }
16 |
17 | public function test_memo_caches_in_memory()
18 | {
19 | $memo = SmartCache::memo();
20 |
21 | // First call hits cache
22 | $memo->put('test_key', 'test_value', 60);
23 |
24 | // Second call should be from memory (instant)
25 | $value1 = $memo->get('test_key');
26 | $value2 = $memo->get('test_key');
27 | $value3 = $memo->get('test_key');
28 |
29 | $this->assertEquals('test_value', $value1);
30 | $this->assertEquals('test_value', $value2);
31 | $this->assertEquals('test_value', $value3);
32 | }
33 |
34 | public function test_memo_with_large_data()
35 | {
36 | $memo = SmartCache::memo();
37 |
38 | // Create large dataset
39 | $largeData = array_fill(0, 10000, 'test_data');
40 |
41 | $memo->put('large_key', $largeData, 60);
42 |
43 | // Multiple accesses should be instant (from memory)
44 | $start = microtime(true);
45 | for ($i = 0; $i < 100; $i++) {
46 | $data = $memo->get('large_key');
47 | }
48 | $duration = microtime(true) - $start;
49 |
50 | // Should be very fast (< 100ms for 100 accesses - lenient for CI)
51 | $this->assertLessThan(0.1, $duration);
52 | $this->assertCount(10000, $data);
53 | }
54 |
55 | public function test_memo_handles_cache_misses()
56 | {
57 | $memo = SmartCache::memo();
58 |
59 | // First miss
60 | $value1 = $memo->get('nonexistent_key', 'default');
61 | $this->assertEquals('default', $value1);
62 |
63 | // Second miss (should be memoized)
64 | $value2 = $memo->get('nonexistent_key', 'default');
65 | $this->assertEquals('default', $value2);
66 | }
67 |
68 | public function test_memo_clears_on_put()
69 | {
70 | $memo = SmartCache::memo();
71 |
72 | $memo->put('test_key', 'value1', 60);
73 | $this->assertEquals('value1', $memo->get('test_key'));
74 |
75 | // Update value
76 | $memo->put('test_key', 'value2', 60);
77 | $this->assertEquals('value2', $memo->get('test_key'));
78 | }
79 |
80 | public function test_memo_clears_on_forget()
81 | {
82 | $memo = SmartCache::memo();
83 |
84 | $memo->put('test_key', 'test_value', 60);
85 | $this->assertEquals('test_value', $memo->get('test_key'));
86 |
87 | $memo->forget('test_key');
88 | $this->assertNull($memo->get('test_key'));
89 | }
90 |
91 | public function test_memo_with_remember()
92 | {
93 | $memo = SmartCache::memo();
94 |
95 | $callCount = 0;
96 |
97 | // First call executes callback
98 | $value1 = $memo->remember('test_key', 60, function() use (&$callCount) {
99 | $callCount++;
100 | return 'computed_value';
101 | });
102 |
103 | // Subsequent calls use memoized value
104 | $value2 = $memo->remember('test_key', 60, function() use (&$callCount) {
105 | $callCount++;
106 | return 'computed_value';
107 | });
108 |
109 | $value3 = $memo->remember('test_key', 60, function() use (&$callCount) {
110 | $callCount++;
111 | return 'computed_value';
112 | });
113 |
114 | $this->assertEquals('computed_value', $value1);
115 | $this->assertEquals('computed_value', $value2);
116 | $this->assertEquals('computed_value', $value3);
117 | $this->assertEquals(1, $callCount); // Callback only called once
118 | }
119 |
120 | public function test_memo_with_different_stores()
121 | {
122 | // Test that memoization works with different cache stores
123 | // Use stores that don't require external services (file, array)
124 |
125 | // Test with file store
126 | $memoFile = SmartCache::memo('file');
127 | $this->assertInstanceOf(\SmartCache\SmartCache::class, $memoFile);
128 |
129 | $memoFile->put('file_test', 'file_value', 60);
130 | $this->assertEquals('file_value', $memoFile->get('file_test'));
131 |
132 | // Test with array store
133 | $memoArray = SmartCache::memo('array');
134 | $this->assertInstanceOf(\SmartCache\SmartCache::class, $memoArray);
135 |
136 | $memoArray->put('array_test', 'array_value', 60);
137 | $this->assertEquals('array_value', $memoArray->get('array_test'));
138 | }
139 |
140 | public function test_memo_performance_improvement()
141 | {
142 | // Test without memoization
143 | $start = microtime(true);
144 | for ($i = 0; $i < 100; $i++) {
145 | SmartCache::get('test_key');
146 | }
147 | $durationWithoutMemo = microtime(true) - $start;
148 |
149 | // Test with memoization
150 | $memo = SmartCache::memo();
151 | $memo->put('test_key', 'test_value', 60);
152 |
153 | $start = microtime(true);
154 | for ($i = 0; $i < 100; $i++) {
155 | $memo->get('test_key');
156 | }
157 | $durationWithMemo = microtime(true) - $start;
158 |
159 | // Memoization should be significantly faster
160 | $this->assertLessThan($durationWithoutMemo, $durationWithMemo);
161 | }
162 |
163 | public function test_memo_with_many_operations()
164 | {
165 | $memo = SmartCache::memo();
166 |
167 | $memo->putMany([
168 | 'key1' => 'value1',
169 | 'key2' => 'value2',
170 | 'key3' => 'value3',
171 | ], 60);
172 |
173 | $values = $memo->many(['key1', 'key2', 'key3']);
174 |
175 | $this->assertEquals([
176 | 'key1' => 'value1',
177 | 'key2' => 'value2',
178 | 'key3' => 'value3',
179 | ], $values);
180 | }
181 |
182 | public function test_memo_with_increment_decrement()
183 | {
184 | $memo = SmartCache::memo();
185 |
186 | $memo->put('counter', 0, 60);
187 |
188 | $memo->increment('counter');
189 | $this->assertEquals(1, $memo->get('counter'));
190 |
191 | $memo->increment('counter', 5);
192 | $this->assertEquals(6, $memo->get('counter'));
193 |
194 | $memo->decrement('counter', 2);
195 | $this->assertEquals(4, $memo->get('counter'));
196 | }
197 |
198 | public function test_memoization_stats()
199 | {
200 | $memo = SmartCache::memo();
201 |
202 | // Put some data
203 | $memo->put('key1', 'value1', 60);
204 | $memo->put('key2', 'value2', 60);
205 |
206 | // Access them - this will memoize them
207 | $val1 = $memo->get('key1');
208 | $val2 = $memo->get('key2');
209 |
210 | // Verify they were retrieved correctly
211 | $this->assertEquals('value1', $val1);
212 | $this->assertEquals('value2', $val2);
213 |
214 | // Access nonexistent key
215 | $val3 = $memo->get('nonexistent');
216 | $this->assertNull($val3);
217 |
218 | // Get stats directly from memoized driver
219 | $stats = $memo->getMemoizationStats();
220 |
221 | $this->assertArrayHasKey('memoized_count', $stats);
222 | $this->assertArrayHasKey('missing_count', $stats);
223 | $this->assertArrayHasKey('total_memory', $stats);
224 |
225 | // Should have some memoized data
226 | $this->assertGreaterThanOrEqual(0, $stats['memoized_count']);
227 | $this->assertGreaterThanOrEqual(0, $stats['missing_count']);
228 | $this->assertGreaterThan(0, $stats['total_memory']);
229 | }
230 | }
231 |
232 |
--------------------------------------------------------------------------------
/tests/Unit/Events/CacheEventsTest.php:
--------------------------------------------------------------------------------
1 | true]);
21 | }
22 |
23 | public function test_cache_hit_event_is_dispatched()
24 | {
25 | Event::fake();
26 |
27 | SmartCache::put('test_key', 'test_value', 60);
28 | SmartCache::get('test_key');
29 |
30 | Event::assertDispatched(CacheHit::class, function ($event) {
31 | return $event->key === 'test_key' && $event->value === 'test_value';
32 | });
33 | }
34 |
35 | public function test_cache_missed_event_is_dispatched()
36 | {
37 | Event::fake();
38 |
39 | SmartCache::get('nonexistent_key');
40 |
41 | Event::assertDispatched(CacheMissed::class, function ($event) {
42 | return $event->key === 'nonexistent_key';
43 | });
44 | }
45 |
46 | public function test_key_written_event_is_dispatched()
47 | {
48 | Event::fake();
49 |
50 | SmartCache::put('test_key', 'test_value', 60);
51 |
52 | Event::assertDispatched(KeyWritten::class, function ($event) {
53 | return $event->key === 'test_key'
54 | && $event->value === 'test_value'
55 | && $event->seconds === 60;
56 | });
57 | }
58 |
59 | public function test_key_forgotten_event_is_dispatched()
60 | {
61 | Event::fake();
62 |
63 | SmartCache::put('test_key', 'test_value', 60);
64 | SmartCache::forget('test_key');
65 |
66 | Event::assertDispatched(KeyForgotten::class, function ($event) {
67 | return $event->key === 'test_key';
68 | });
69 | }
70 |
71 | public function test_events_include_tags()
72 | {
73 | Event::fake();
74 |
75 | SmartCache::tags(['users', 'posts'])->put('test_key', 'test_value', 60);
76 | SmartCache::tags(['users', 'posts'])->get('test_key');
77 |
78 | Event::assertDispatched(CacheHit::class, function ($event) {
79 | return $event->key === 'test_key'
80 | && in_array('users', $event->tags)
81 | && in_array('posts', $event->tags);
82 | });
83 | }
84 |
85 | public function test_events_can_be_disabled()
86 | {
87 | config(['smart-cache.events.enabled' => false]);
88 |
89 | Event::fake();
90 |
91 | SmartCache::put('test_key', 'test_value', 60);
92 | SmartCache::get('test_key');
93 |
94 | Event::assertNotDispatched(CacheHit::class);
95 | Event::assertNotDispatched(KeyWritten::class);
96 | }
97 |
98 | public function test_specific_events_can_be_disabled()
99 | {
100 | config(['smart-cache.events.dispatch.cache_hit' => false]);
101 |
102 | Event::fake();
103 |
104 | SmartCache::put('test_key', 'test_value', 60);
105 | SmartCache::get('test_key');
106 |
107 | Event::assertNotDispatched(CacheHit::class);
108 | Event::assertDispatched(KeyWritten::class);
109 | }
110 |
111 | public function test_events_with_large_data()
112 | {
113 | Event::fake();
114 |
115 | $largeData = array_fill(0, 10000, 'test_data');
116 |
117 | SmartCache::put('large_key', $largeData, 60);
118 | SmartCache::get('large_key');
119 |
120 | Event::assertDispatched(KeyWritten::class, function ($event) use ($largeData) {
121 | return $event->key === 'large_key' && $event->value === $largeData;
122 | });
123 |
124 | Event::assertDispatched(CacheHit::class, function ($event) use ($largeData) {
125 | return $event->key === 'large_key' && $event->value === $largeData;
126 | });
127 | }
128 |
129 | public function test_can_listen_to_events()
130 | {
131 | $hitCount = 0;
132 | $missCount = 0;
133 |
134 | Event::listen(CacheHit::class, function ($event) use (&$hitCount) {
135 | $hitCount++;
136 | });
137 |
138 | Event::listen(CacheMissed::class, function ($event) use (&$missCount) {
139 | $missCount++;
140 | });
141 |
142 | SmartCache::put('test_key', 'test_value', 60);
143 | SmartCache::get('test_key'); // Hit
144 | SmartCache::get('nonexistent'); // Miss
145 | SmartCache::get('test_key'); // Hit
146 |
147 | $this->assertEquals(2, $hitCount);
148 | $this->assertEquals(1, $missCount);
149 | }
150 |
151 | public function test_events_with_remember()
152 | {
153 | Event::fake();
154 |
155 | SmartCache::remember('test_key', 60, function() {
156 | return 'computed_value';
157 | });
158 |
159 | // Should dispatch KeyWritten (first time)
160 | Event::assertDispatched(KeyWritten::class);
161 |
162 | Event::fake(); // Reset
163 |
164 | SmartCache::remember('test_key', 60, function() {
165 | return 'computed_value';
166 | });
167 |
168 | // Should dispatch CacheHit (second time)
169 | Event::assertDispatched(CacheHit::class);
170 | }
171 |
172 | public function test_events_with_many_operations()
173 | {
174 | Event::fake();
175 |
176 | SmartCache::putMany([
177 | 'key1' => 'value1',
178 | 'key2' => 'value2',
179 | 'key3' => 'value3',
180 | ], 60);
181 |
182 | // Should dispatch KeyWritten for each key
183 | Event::assertDispatched(KeyWritten::class, 3);
184 | }
185 |
186 | public function test_events_with_delete_multiple()
187 | {
188 | Event::fake();
189 |
190 | SmartCache::put('key1', 'value1', 60);
191 | SmartCache::put('key2', 'value2', 60);
192 | SmartCache::put('key3', 'value3', 60);
193 |
194 | Event::fake(); // Reset
195 |
196 | SmartCache::deleteMultiple(['key1', 'key2', 'key3']);
197 |
198 | // Should dispatch KeyForgotten for each key
199 | Event::assertDispatched(KeyForgotten::class, 3);
200 | }
201 | }
202 |
203 |
--------------------------------------------------------------------------------
/tests/Unit/Http/HttpCommandExecutionTest.php:
--------------------------------------------------------------------------------
1 | cache = $this->app->make(SmartCacheContract::class);
17 | }
18 |
19 | public function test_get_available_commands_returns_command_metadata(): void
20 | {
21 | $commands = $this->cache->getAvailableCommands();
22 |
23 | $this->assertIsArray($commands);
24 | $this->assertArrayHasKey('smart-cache:clear', $commands);
25 | $this->assertArrayHasKey('smart-cache:status', $commands);
26 |
27 | // Check clear command metadata
28 | $clearCommand = $commands['smart-cache:clear'];
29 | $this->assertArrayHasKey('class', $clearCommand);
30 | $this->assertArrayHasKey('description', $clearCommand);
31 | $this->assertArrayHasKey('signature', $clearCommand);
32 | $this->assertEquals('SmartCache\Console\Commands\ClearCommand', $clearCommand['class']);
33 |
34 | // Check status command metadata
35 | $statusCommand = $commands['smart-cache:status'];
36 | $this->assertArrayHasKey('class', $statusCommand);
37 | $this->assertArrayHasKey('description', $statusCommand);
38 | $this->assertArrayHasKey('signature', $statusCommand);
39 | $this->assertEquals('SmartCache\Console\Commands\StatusCommand', $statusCommand['class']);
40 | }
41 |
42 | public function test_execute_clear_command_without_parameters(): void
43 | {
44 | // Setup: Add some test data
45 | $this->cache->put('test_key_1', 'value1', 3600);
46 | $this->cache->put('test_key_2', 'value2', 3600);
47 |
48 | $initialCount = count($this->cache->getManagedKeys());
49 | $this->assertGreaterThan(0, $initialCount);
50 |
51 | // Execute clear command
52 | $result = $this->cache->executeCommand('clear');
53 |
54 | $this->assertIsArray($result);
55 | $this->assertTrue($result['success']);
56 | $this->assertArrayHasKey('message', $result);
57 | $this->assertArrayHasKey('cleared_count', $result);
58 | $this->assertEquals($initialCount, $result['cleared_count']);
59 |
60 | // Verify keys were cleared
61 | $this->assertEquals(0, count($this->cache->getManagedKeys()));
62 | }
63 |
64 | public function test_execute_clear_command_with_specific_key(): void
65 | {
66 | // Setup: Add test data
67 | $testKey = 'specific_key_test_' . time();
68 | $this->cache->put($testKey, 'test_value', 3600);
69 |
70 | $this->assertTrue($this->cache->has($testKey));
71 |
72 | // Execute clear command for specific key
73 | $result = $this->cache->executeCommand('clear', ['key' => $testKey]);
74 |
75 | $this->assertIsArray($result);
76 | $this->assertTrue($result['success']);
77 | $this->assertEquals(1, $result['cleared_count']);
78 | $this->assertEquals($testKey, $result['key']);
79 | $this->assertTrue($result['was_managed']);
80 |
81 | // Verify key was cleared
82 | $this->assertFalse($this->cache->has($testKey));
83 | }
84 |
85 | public function test_execute_clear_command_with_non_existent_key(): void
86 | {
87 | $nonExistentKey = 'non_existent_key_' . time();
88 |
89 | $result = $this->cache->executeCommand('clear', ['key' => $nonExistentKey]);
90 |
91 | $this->assertIsArray($result);
92 | $this->assertFalse($result['success']);
93 | $this->assertStringContainsString('not managed by SmartCache', $result['message']);
94 | $this->assertEquals(0, $result['cleared_count']);
95 | }
96 |
97 | public function test_execute_clear_command_with_non_managed_key_without_force(): void
98 | {
99 | // Add a key directly to Laravel cache (not managed by SmartCache)
100 | $nonManagedKey = 'non_managed_key_' . time();
101 | $this->app['cache']->put($nonManagedKey, 'value', 3600);
102 |
103 | $result = $this->cache->executeCommand('clear', ['key' => $nonManagedKey]);
104 |
105 | $this->assertIsArray($result);
106 | $this->assertFalse($result['success']);
107 | $this->assertStringContainsString('not managed by SmartCache', $result['message']);
108 | $this->assertEquals(0, $result['cleared_count']);
109 | }
110 |
111 | public function test_execute_clear_command_with_non_managed_key_with_force(): void
112 | {
113 | // Add a key directly to Laravel cache (not managed by SmartCache)
114 | $nonManagedKey = 'non_managed_key_force_' . time();
115 | $this->app['cache']->put($nonManagedKey, 'value', 3600);
116 |
117 | $result = $this->cache->executeCommand('clear', [
118 | 'key' => $nonManagedKey,
119 | 'force' => true
120 | ]);
121 |
122 | $this->assertIsArray($result);
123 | $this->assertTrue($result['success']);
124 | $this->assertEquals(1, $result['cleared_count']);
125 | $this->assertEquals($nonManagedKey, $result['key']);
126 | $this->assertFalse($result['was_managed']);
127 | }
128 |
129 | public function test_execute_status_command(): void
130 | {
131 | // Setup: Add some test data
132 | $this->cache->put('status_test_key_1', 'value1', 3600);
133 | $this->cache->put('status_test_key_2', 'value2', 3600);
134 |
135 | $result = $this->cache->executeCommand('status');
136 |
137 | $this->assertIsArray($result);
138 | $this->assertTrue($result['success']);
139 | $this->assertArrayHasKey('cache_driver', $result);
140 | $this->assertArrayHasKey('managed_keys_count', $result);
141 | $this->assertArrayHasKey('sample_keys', $result);
142 | $this->assertArrayHasKey('configuration', $result);
143 | $this->assertArrayHasKey('statistics', $result);
144 | $this->assertArrayHasKey('health_check', $result);
145 |
146 | $this->assertGreaterThanOrEqual(2, $result['managed_keys_count']);
147 | $this->assertIsArray($result['sample_keys']);
148 | $this->assertIsArray($result['configuration']);
149 | }
150 |
151 | public function test_execute_status_command_with_force(): void
152 | {
153 | // Setup: Add test data and create a scenario with missing keys
154 | $this->cache->put('status_force_test', 'value', 3600);
155 |
156 | $result = $this->cache->executeCommand('status', ['force' => true]);
157 |
158 | $this->assertIsArray($result);
159 | $this->assertTrue($result['success']);
160 | $this->assertArrayHasKey('analysis', $result);
161 | $this->assertArrayHasKey('managed_keys_missing_from_cache', $result['analysis']);
162 | $this->assertArrayHasKey('missing_keys_count', $result['analysis']);
163 |
164 | $this->assertIsArray($result['analysis']['managed_keys_missing_from_cache']);
165 | $this->assertIsInt($result['analysis']['missing_keys_count']);
166 | }
167 |
168 | public function test_execute_unknown_command(): void
169 | {
170 | $result = $this->cache->executeCommand('unknown-command');
171 |
172 | $this->assertIsArray($result);
173 | $this->assertFalse($result['success']);
174 | $this->assertStringContainsString('Unknown command', $result['message']);
175 | $this->assertArrayHasKey('available_commands', $result);
176 | $this->assertIsArray($result['available_commands']);
177 | }
178 |
179 | public function test_execute_command_handles_exceptions(): void
180 | {
181 | // Mock a scenario that would cause an exception
182 | $result = $this->cache->executeCommand('clear', ['key' => null]);
183 |
184 | $this->assertIsArray($result);
185 | // Should handle gracefully even with invalid parameters
186 | $this->assertArrayHasKey('success', $result);
187 | $this->assertArrayHasKey('message', $result);
188 | }
189 |
190 | public function test_facade_execute_command(): void
191 | {
192 | \SmartCache\Facades\SmartCache::put('facade_command_test', 'value', 3600);
193 |
194 | $result = \SmartCache\Facades\SmartCache::executeCommand('status');
195 |
196 | $this->assertIsArray($result);
197 | $this->assertTrue($result['success']);
198 | $this->assertGreaterThan(0, $result['managed_keys_count']);
199 | }
200 |
201 | public function test_facade_get_available_commands(): void
202 | {
203 | $commands = \SmartCache\Facades\SmartCache::getAvailableCommands();
204 |
205 | $this->assertIsArray($commands);
206 | $this->assertArrayHasKey('smart-cache:clear', $commands);
207 | $this->assertArrayHasKey('smart-cache:status', $commands);
208 | }
209 |
210 | public function test_command_shortcuts(): void
211 | {
212 | // Test that both long and short command names work
213 | $this->cache->put('shortcut_test', 'value', 3600);
214 |
215 | $resultLong = $this->cache->executeCommand('smart-cache:status');
216 | $resultShort = $this->cache->executeCommand('status');
217 |
218 | $this->assertTrue($resultLong['success']);
219 | $this->assertTrue($resultShort['success']);
220 | $this->assertEquals($resultLong['managed_keys_count'], $resultShort['managed_keys_count']);
221 |
222 | // Test clear shortcuts
223 | $clearLong = $this->cache->executeCommand('smart-cache:clear');
224 |
225 | // Reset data for second test
226 | $this->cache->put('shortcut_test_2', 'value', 3600);
227 | $clearShort = $this->cache->executeCommand('clear');
228 |
229 | $this->assertTrue($clearLong['success']);
230 | $this->assertTrue($clearShort['success']);
231 | }
232 | }
233 |
--------------------------------------------------------------------------------
/tests/Unit/Laravel12/Laravel12SWRTest.php:
--------------------------------------------------------------------------------
1 | cache = $this->app->make(SmartCacheContract::class);
17 | }
18 |
19 | public function test_swr_method_exists(): void
20 | {
21 | $this->assertTrue(method_exists($this->cache, 'swr'));
22 | }
23 |
24 | public function test_stale_method_exists(): void
25 | {
26 | $this->assertTrue(method_exists($this->cache, 'stale'));
27 | }
28 |
29 | public function test_refresh_ahead_method_exists(): void
30 | {
31 | $this->assertTrue(method_exists($this->cache, 'refreshAhead'));
32 | }
33 |
34 | public function test_swr_caching_pattern(): void
35 | {
36 | $key = 'swr_test_' . time();
37 | $callCount = 0;
38 |
39 | $callback = function () use (&$callCount) {
40 | $callCount++;
41 | return [
42 | 'data' => 'test_value',
43 | 'timestamp' => now()->toDateTimeString(),
44 | 'call_count' => $callCount
45 | ];
46 | };
47 |
48 | // First call should execute callback
49 | $result1 = $this->cache->swr($key, $callback, 2, 10); // 2 second fresh, 10 second stale
50 | $this->assertEquals(1, $callCount);
51 | $this->assertEquals('test_value', $result1['data']);
52 | $this->assertEquals(1, $result1['call_count']);
53 |
54 | // Immediate second call should use cache
55 | $result2 = $this->cache->swr($key, $callback, 2, 10);
56 | $this->assertEquals(1, $callCount); // Callback not called again
57 | $this->assertEquals($result1, $result2);
58 |
59 | // Wait for data to become stale but not expired
60 | sleep(3); // Fresh TTL exceeded but within stale TTL
61 |
62 | // This should return stale data immediately and trigger background refresh
63 | $result3 = $this->cache->swr($key, $callback, 2, 10);
64 | $this->assertEquals('test_value', $result3['data']);
65 |
66 | // The callback should have been called for background refresh
67 | $this->assertGreaterThanOrEqual(2, $callCount);
68 | }
69 |
70 | public function test_stale_caching_pattern(): void
71 | {
72 | $key = 'stale_test_' . time();
73 | $callCount = 0;
74 |
75 | $callback = function () use (&$callCount) {
76 | $callCount++;
77 | return [
78 | 'data' => 'stale_test_value',
79 | 'timestamp' => now()->toDateTimeString(),
80 | 'call_count' => $callCount
81 | ];
82 | };
83 |
84 | // First call should execute callback
85 | $result1 = $this->cache->stale($key, $callback, 1, 5); // 1 second fresh, 5 second stale
86 | $this->assertEquals(1, $callCount);
87 | $this->assertEquals('stale_test_value', $result1['data']);
88 |
89 | // Immediate call should use cache
90 | $result2 = $this->cache->stale($key, $callback, 1, 5);
91 | $this->assertEquals(1, $callCount);
92 | $this->assertEquals($result1, $result2);
93 |
94 | // Wait for fresh period to expire
95 | sleep(2);
96 |
97 | // Should serve stale data
98 | $result3 = $this->cache->stale($key, $callback, 1, 5);
99 | $this->assertEquals('stale_test_value', $result3['data']);
100 | }
101 |
102 | public function test_refresh_ahead_caching_pattern(): void
103 | {
104 | $key = 'refresh_ahead_test_' . time();
105 | $callCount = 0;
106 |
107 | $callback = function () use (&$callCount) {
108 | $callCount++;
109 | return [
110 | 'data' => 'refresh_ahead_value',
111 | 'timestamp' => now()->toDateTimeString(),
112 | 'call_count' => $callCount
113 | ];
114 | };
115 |
116 | // First call should execute callback
117 | $result1 = $this->cache->refreshAhead($key, $callback, 5, 2); // 5 second TTL, 2 second refresh window
118 | $this->assertEquals(1, $callCount);
119 | $this->assertEquals('refresh_ahead_value', $result1['data']);
120 |
121 | // Call within fresh period should use cache
122 | $result2 = $this->cache->refreshAhead($key, $callback, 5, 2);
123 | $this->assertEquals(1, $callCount);
124 | $this->assertEquals($result1, $result2);
125 |
126 | // Wait to enter refresh window
127 | sleep(4); // Should trigger refresh-ahead behavior
128 |
129 | $result3 = $this->cache->refreshAhead($key, $callback, 5, 2);
130 | $this->assertEquals('refresh_ahead_value', $result3['data']);
131 |
132 | // Should have triggered background refresh
133 | $this->assertGreaterThanOrEqual(2, $callCount);
134 | }
135 |
136 | public function test_flexible_method_with_custom_durations(): void
137 | {
138 | $key = 'flexible_test_' . time();
139 | $callCount = 0;
140 |
141 | $callback = function () use (&$callCount) {
142 | $callCount++;
143 | return [
144 | 'flexible' => true,
145 | 'call_count' => $callCount
146 | ];
147 | };
148 |
149 | // Test with custom durations
150 | $result1 = $this->cache->flexible($key, [3, 8], $callback); // 3 second fresh, 8 second total
151 | $this->assertEquals(1, $callCount);
152 | $this->assertTrue($result1['flexible']);
153 |
154 | // Should use cache within fresh period
155 | $result2 = $this->cache->flexible($key, [3, 8], $callback);
156 | $this->assertEquals(1, $callCount);
157 | $this->assertEquals($result1, $result2);
158 | }
159 |
160 | public function test_swr_with_facade(): void
161 | {
162 | $key = 'facade_swr_test_' . time();
163 | $callCount = 0;
164 |
165 | $callback = function () use (&$callCount) {
166 | $callCount++;
167 | return ['facade_swr' => true, 'count' => $callCount];
168 | };
169 |
170 | // Test facade method
171 | $result = \SmartCache\Facades\SmartCache::swr($key, $callback);
172 | $this->assertEquals(1, $callCount);
173 | $this->assertTrue($result['facade_swr']);
174 | $this->assertEquals(1, $result['count']);
175 |
176 | // Second call should use cache
177 | $result2 = \SmartCache\Facades\SmartCache::swr($key, $callback);
178 | $this->assertEquals(1, $callCount); // No additional callback execution
179 | $this->assertEquals($result, $result2);
180 | }
181 |
182 | public function test_stale_with_facade(): void
183 | {
184 | $key = 'facade_stale_test_' . time();
185 | $callCount = 0;
186 |
187 | $callback = function () use (&$callCount) {
188 | $callCount++;
189 | return ['facade_stale' => true, 'count' => $callCount];
190 | };
191 |
192 | $result = \SmartCache\Facades\SmartCache::stale($key, $callback);
193 | $this->assertEquals(1, $callCount);
194 | $this->assertTrue($result['facade_stale']);
195 | }
196 |
197 | public function test_refresh_ahead_with_facade(): void
198 | {
199 | $key = 'facade_refresh_ahead_test_' . time();
200 | $callCount = 0;
201 |
202 | $callback = function () use (&$callCount) {
203 | $callCount++;
204 | return ['facade_refresh_ahead' => true, 'count' => $callCount];
205 | };
206 |
207 | $result = \SmartCache\Facades\SmartCache::refreshAhead($key, $callback);
208 | $this->assertEquals(1, $callCount);
209 | $this->assertTrue($result['facade_refresh_ahead']);
210 | }
211 |
212 | public function test_swr_methods_use_optimization(): void
213 | {
214 | $key = 'optimization_swr_test_' . time();
215 |
216 | // Create large data that should trigger compression
217 | $largeData = array_fill(0, 2000, 'large_data_item_for_compression_testing');
218 |
219 | $callback = function () use ($largeData) {
220 | return $largeData;
221 | };
222 |
223 | $result = $this->cache->swr($key, $callback);
224 | $this->assertEquals($largeData, $result);
225 |
226 | // Verify the key was tracked (indicating optimization was applied)
227 | $managedKeys = $this->cache->getManagedKeys();
228 | $this->assertContains($key, $managedKeys);
229 | }
230 |
231 | public function test_all_swr_methods_handle_exceptions_gracefully(): void
232 | {
233 | $key = 'exception_test_' . time();
234 |
235 | $callback = function () {
236 | throw new \Exception('Test exception');
237 | };
238 |
239 | // All methods should handle exceptions gracefully when fallback is enabled
240 | $this->expectException(\Exception::class);
241 | $this->cache->swr($key, $callback);
242 | }
243 | }
244 |
--------------------------------------------------------------------------------
/tests/Unit/Locks/AtomicLocksTest.php:
--------------------------------------------------------------------------------
1 | assertInstanceOf(\Illuminate\Contracts\Cache\Lock::class, $lock);
16 |
17 | $acquired = $lock->get();
18 | $this->assertTrue($acquired);
19 |
20 | $lock->release();
21 | }
22 |
23 | public function test_lock_prevents_concurrent_access()
24 | {
25 | $lock1 = SmartCache::lock('test_lock', 10);
26 | $acquired1 = $lock1->get();
27 |
28 | $this->assertTrue($acquired1);
29 |
30 | // Try to acquire the same lock
31 | $lock2 = SmartCache::lock('test_lock', 10);
32 | $acquired2 = $lock2->get();
33 |
34 | $this->assertFalse($acquired2);
35 |
36 | $lock1->release();
37 | }
38 |
39 | public function test_lock_with_callback()
40 | {
41 | $executed = false;
42 |
43 | SmartCache::lock('test_lock', 10)->get(function() use (&$executed) {
44 | $executed = true;
45 | });
46 |
47 | $this->assertTrue($executed);
48 | }
49 |
50 | public function test_lock_with_callback_returns_value()
51 | {
52 | $result = SmartCache::lock('test_lock', 10)->get(function() {
53 | return 'test_value';
54 | });
55 |
56 | $this->assertEquals('test_value', $result);
57 | }
58 |
59 | public function test_can_restore_lock_with_owner()
60 | {
61 | $lock = SmartCache::lock('test_lock', 10);
62 | $lock->get();
63 |
64 | $owner = $lock->owner();
65 | $this->assertNotNull($owner);
66 |
67 | $restoredLock = SmartCache::restoreLock('test_lock', $owner);
68 | $this->assertInstanceOf(\Illuminate\Contracts\Cache\Lock::class, $restoredLock);
69 |
70 | $restoredLock->release();
71 | }
72 |
73 | public function test_lock_prevents_cache_stampede()
74 | {
75 | $callCount = 0;
76 |
77 | // First lock acquisition should succeed
78 | $lock1 = SmartCache::lock('expensive_operation', 10);
79 | $this->assertTrue($lock1->get());
80 | $callCount++;
81 | SmartCache::put('expensive_data', 'value', 60);
82 |
83 | // Subsequent attempts should fail while lock is held
84 | for ($i = 0; $i < 4; $i++) {
85 | $lock = SmartCache::lock('expensive_operation', 10);
86 | if ($lock->get()) {
87 | $callCount++;
88 | $lock->release();
89 | }
90 | }
91 |
92 | // Only one process should have executed the expensive operation
93 | $this->assertEquals(1, $callCount);
94 |
95 | // Clean up
96 | $lock1->release();
97 | }
98 |
99 | public function test_lock_with_block_waits_for_lock()
100 | {
101 | // Skip this test as it's timing-dependent and may fail in CI
102 | $this->markTestSkipped('Timing-dependent test - may fail in CI environments');
103 | }
104 |
105 | public function test_lock_with_large_data_regeneration()
106 | {
107 | $key = 'large_dataset';
108 |
109 | // Clear any existing data
110 | SmartCache::forget($key);
111 |
112 | $lock = SmartCache::lock("regenerate_{$key}", 30);
113 |
114 | if ($lock->get()) {
115 | // Generate large dataset
116 | $largeData = array_fill(0, 10000, 'test_data');
117 | SmartCache::put($key, $largeData, 3600);
118 | $lock->release();
119 | }
120 |
121 | $this->assertTrue(SmartCache::has($key));
122 | $retrieved = SmartCache::get($key);
123 | $this->assertCount(10000, $retrieved);
124 | }
125 |
126 | public function test_lock_throws_exception_for_unsupported_driver()
127 | {
128 | // This test is driver-dependent
129 | // Most drivers support locks, so we just verify the method exists
130 |
131 | try {
132 | $lock = SmartCache::lock('test', 10);
133 | $this->assertInstanceOf(\Illuminate\Contracts\Cache\Lock::class, $lock);
134 | } catch (\RuntimeException $e) {
135 | $this->assertStringContainsString('does not support atomic locks', $e->getMessage());
136 | }
137 | }
138 | }
139 |
140 |
--------------------------------------------------------------------------------
/tests/Unit/Services/SmartChunkSizeCalculatorTest.php:
--------------------------------------------------------------------------------
1 | calculateOptimalSize($smallData);
16 |
17 | // Small data should not be chunked
18 | $this->assertEquals(100, $optimalSize);
19 | }
20 |
21 | public function test_calculates_optimal_size_for_large_data()
22 | {
23 | $calculator = new SmartChunkSizeCalculator();
24 |
25 | $largeData = array_fill(0, 100000, str_repeat('test', 100));
26 | $optimalSize = $calculator->calculateOptimalSize($largeData);
27 |
28 | // Should return a reasonable chunk size
29 | $this->assertGreaterThan(100, $optimalSize);
30 | $this->assertLessThan(100000, $optimalSize);
31 | }
32 |
33 | public function test_respects_driver_limits()
34 | {
35 | $calculator = new SmartChunkSizeCalculator();
36 |
37 | $data = array_fill(0, 10000, str_repeat('x', 1000)); // ~10MB total
38 |
39 | // Memcached has 1MB limit
40 | $memcachedSize = $calculator->calculateOptimalSize($data, 'memcached');
41 |
42 | // Redis has 512MB limit
43 | $redisSize = $calculator->calculateOptimalSize($data, 'redis');
44 |
45 | // Redis should allow larger chunks
46 | $this->assertGreaterThan($memcachedSize, $redisSize);
47 | }
48 |
49 | public function test_get_driver_limit()
50 | {
51 | $calculator = new SmartChunkSizeCalculator();
52 |
53 | $this->assertEquals(512 * 1024 * 1024, $calculator->getDriverLimit('redis'));
54 | $this->assertEquals(1024 * 1024, $calculator->getDriverLimit('memcached'));
55 | $this->assertEquals(16 * 1024 * 1024, $calculator->getDriverLimit('database'));
56 | $this->assertEquals(400 * 1024, $calculator->getDriverLimit('dynamodb'));
57 | }
58 |
59 | public function test_get_driver_limit_returns_default_for_unknown()
60 | {
61 | $calculator = new SmartChunkSizeCalculator();
62 |
63 | $limit = $calculator->getDriverLimit('unknown_driver');
64 |
65 | $this->assertEquals(1024 * 1024, $limit); // 1MB default
66 | }
67 |
68 | public function test_calculate_chunk_count()
69 | {
70 | $calculator = new SmartChunkSizeCalculator();
71 |
72 | $this->assertEquals(10, $calculator->calculateChunkCount(1000, 100));
73 | $this->assertEquals(5, $calculator->calculateChunkCount(1000, 200));
74 | $this->assertEquals(1, $calculator->calculateChunkCount(100, 1000));
75 | }
76 |
77 | public function test_get_recommendations()
78 | {
79 | $calculator = new SmartChunkSizeCalculator();
80 |
81 | $data = array_fill(0, 10000, str_repeat('test', 100));
82 | $recommendations = $calculator->getRecommendations($data, 'redis');
83 |
84 | $this->assertArrayHasKey('total_items', $recommendations);
85 | $this->assertArrayHasKey('total_size', $recommendations);
86 | $this->assertArrayHasKey('avg_item_size', $recommendations);
87 | $this->assertArrayHasKey('optimal_chunk_size', $recommendations);
88 | $this->assertArrayHasKey('chunk_count', $recommendations);
89 | $this->assertArrayHasKey('driver', $recommendations);
90 | $this->assertArrayHasKey('should_chunk', $recommendations);
91 |
92 | $this->assertEquals(10000, $recommendations['total_items']);
93 | $this->assertEquals('redis', $recommendations['driver']);
94 | }
95 |
96 | public function test_should_chunk_recommendation()
97 | {
98 | $calculator = new SmartChunkSizeCalculator();
99 |
100 | // Small data
101 | $smallData = array_fill(0, 100, 'test');
102 | $smallRec = $calculator->getRecommendations($smallData);
103 | $this->assertFalse($smallRec['should_chunk']);
104 |
105 | // Large data
106 | $largeData = array_fill(0, 10000, str_repeat('test', 100));
107 | $largeRec = $calculator->getRecommendations($largeData);
108 | $this->assertTrue($largeRec['should_chunk']);
109 | }
110 |
111 | public function test_is_chunk_size_safe()
112 | {
113 | $calculator = new SmartChunkSizeCalculator();
114 |
115 | // Safe chunk size for memcached
116 | $this->assertTrue($calculator->isChunkSizeSafe(100, 1000, 'memcached'));
117 |
118 | // Unsafe chunk size for memcached (would exceed 1MB limit)
119 | $this->assertFalse($calculator->isChunkSizeSafe(10000, 1000, 'memcached'));
120 | }
121 |
122 | public function test_set_custom_driver_limit()
123 | {
124 | $calculator = new SmartChunkSizeCalculator();
125 |
126 | $calculator->setDriverLimit('custom_driver', 5 * 1024 * 1024); // 5MB
127 |
128 | $this->assertEquals(5 * 1024 * 1024, $calculator->getDriverLimit('custom_driver'));
129 | }
130 |
131 | public function test_get_all_driver_limits()
132 | {
133 | $calculator = new SmartChunkSizeCalculator();
134 |
135 | $limits = $calculator->getDriverLimits();
136 |
137 | $this->assertIsArray($limits);
138 | $this->assertArrayHasKey('redis', $limits);
139 | $this->assertArrayHasKey('memcached', $limits);
140 | $this->assertArrayHasKey('database', $limits);
141 | }
142 |
143 | public function test_optimal_size_for_very_large_dataset()
144 | {
145 | $calculator = new SmartChunkSizeCalculator();
146 |
147 | // 100MB dataset
148 | $largeData = array_fill(0, 100000, str_repeat('x', 1000));
149 | $optimalSize = $calculator->calculateOptimalSize($largeData, 'redis');
150 |
151 | // Should use smaller chunks for very large datasets
152 | $this->assertLessThanOrEqual(500, $optimalSize);
153 | }
154 |
155 | public function test_optimal_size_for_medium_dataset()
156 | {
157 | $calculator = new SmartChunkSizeCalculator();
158 |
159 | // 500KB dataset
160 | $mediumData = array_fill(0, 5000, str_repeat('x', 100));
161 | $optimalSize = $calculator->calculateOptimalSize($mediumData, 'redis');
162 |
163 | // Should use larger chunks for medium datasets
164 | $this->assertGreaterThan(500, $optimalSize);
165 | $this->assertLessThanOrEqual(5000, $optimalSize);
166 | }
167 |
168 | public function test_handles_empty_array()
169 | {
170 | $calculator = new SmartChunkSizeCalculator();
171 |
172 | $emptyData = [];
173 | $optimalSize = $calculator->calculateOptimalSize($emptyData);
174 |
175 | $this->assertEquals(0, $optimalSize);
176 | }
177 |
178 | public function test_minimum_chunk_size()
179 | {
180 | $calculator = new SmartChunkSizeCalculator();
181 |
182 | // Even with very small items, should maintain minimum chunk size
183 | $data = array_fill(0, 10000, 'x');
184 | $optimalSize = $calculator->calculateOptimalSize($data, 'memcached');
185 |
186 | $this->assertGreaterThanOrEqual(100, $optimalSize);
187 | }
188 |
189 | public function test_chunk_size_with_different_default()
190 | {
191 | $calculator = new SmartChunkSizeCalculator();
192 |
193 | $data = array_fill(0, 10000, 'test');
194 |
195 | $size1 = $calculator->calculateOptimalSize($data, null, 1000);
196 | $size2 = $calculator->calculateOptimalSize($data, null, 5000);
197 |
198 | // Different defaults may affect the result
199 | $this->assertIsInt($size1);
200 | $this->assertIsInt($size2);
201 | }
202 | }
203 |
204 |
--------------------------------------------------------------------------------
/tests/Unit/Strategies/AdaptiveCompressionStrategyTest.php:
--------------------------------------------------------------------------------
1 | 1KB
15 |
16 | $this->assertTrue($strategy->shouldApply($largeData));
17 | }
18 |
19 | public function test_should_apply_returns_false_for_small_data()
20 | {
21 | $strategy = new AdaptiveCompressionStrategy(1024);
22 |
23 | $smallData = 'test';
24 |
25 | $this->assertFalse($strategy->shouldApply($smallData));
26 | }
27 |
28 | public function test_optimize_compresses_data()
29 | {
30 | $strategy = new AdaptiveCompressionStrategy(1024);
31 |
32 | $data = str_repeat('test', 500);
33 | $optimized = $strategy->optimize($data);
34 |
35 | $this->assertIsArray($optimized);
36 | $this->assertTrue($optimized['_sc_compressed']);
37 | $this->assertArrayHasKey('data', $optimized);
38 | $this->assertArrayHasKey('level', $optimized);
39 | }
40 |
41 | public function test_restore_decompresses_data()
42 | {
43 | $strategy = new AdaptiveCompressionStrategy(1024);
44 |
45 | $originalData = str_repeat('test', 500);
46 | $optimized = $strategy->optimize($originalData);
47 | $restored = $strategy->restore($optimized);
48 |
49 | $this->assertEquals($originalData, $restored);
50 | }
51 |
52 | public function test_adaptive_compression_selects_appropriate_level()
53 | {
54 | $strategy = new AdaptiveCompressionStrategy(1024);
55 |
56 | // Highly compressible data (repeated pattern)
57 | $compressibleData = str_repeat('aaaa', 1000);
58 | $optimized1 = $strategy->optimize($compressibleData);
59 |
60 | // Less compressible data (random-ish)
61 | $lessCompressibleData = str_repeat('abcdefghijklmnop', 250);
62 | $optimized2 = $strategy->optimize($lessCompressibleData);
63 |
64 | // Both should be compressed but may use different levels
65 | $this->assertTrue($optimized1['_sc_compressed']);
66 | $this->assertTrue($optimized2['_sc_compressed']);
67 | $this->assertArrayHasKey('level', $optimized1);
68 | $this->assertArrayHasKey('level', $optimized2);
69 | }
70 |
71 | public function test_compression_with_large_array()
72 | {
73 | $strategy = new AdaptiveCompressionStrategy(1024);
74 |
75 | $largeArray = array_fill(0, 10000, 'test_data');
76 | $serialized = serialize($largeArray);
77 |
78 | $optimized = $strategy->optimize($serialized);
79 | $restored = $strategy->restore($optimized);
80 |
81 | $this->assertEquals($serialized, $restored);
82 | }
83 |
84 | public function test_get_identifier()
85 | {
86 | $strategy = new AdaptiveCompressionStrategy(1024);
87 |
88 | $this->assertEquals('adaptive_compression', $strategy->getIdentifier());
89 | }
90 |
91 | public function test_compression_stats()
92 | {
93 | $strategy = new AdaptiveCompressionStrategy(1024);
94 |
95 | $data = str_repeat('test', 1000);
96 | $optimized = $strategy->optimize($data);
97 | $stats = $strategy->getCompressionStats($optimized);
98 |
99 | $this->assertArrayHasKey('level', $stats);
100 | $this->assertArrayHasKey('original_size', $stats);
101 | $this->assertArrayHasKey('compressed_size', $stats);
102 | $this->assertArrayHasKey('ratio', $stats);
103 | $this->assertArrayHasKey('savings_bytes', $stats);
104 | $this->assertArrayHasKey('savings_percent', $stats);
105 | }
106 |
107 | public function test_adaptive_compression_with_hot_data()
108 | {
109 | $strategy = new AdaptiveCompressionStrategy(
110 | 1024,
111 | 6,
112 | 1024,
113 | 0.5,
114 | 0.7,
115 | 100 // Low frequency threshold for testing
116 | );
117 |
118 | $data = str_repeat('test', 500);
119 |
120 | // Simulate hot data by accessing it multiple times
121 | $cache = $this->app['cache']->store();
122 | $key = 'test_hot_key';
123 |
124 | // Increment access frequency
125 | for ($i = 0; $i < 150; $i++) {
126 | $cache->increment("_sc_access_freq_{$key}");
127 | }
128 |
129 | $optimized = $strategy->optimize($data, ['key' => $key, 'cache' => $cache]);
130 |
131 | // Hot data should use lower compression level for faster access
132 | $this->assertTrue($optimized['_sc_compressed']);
133 | $this->assertLessThanOrEqual(6, $optimized['level']);
134 | }
135 |
136 | public function test_adaptive_compression_with_cold_data()
137 | {
138 | $strategy = new AdaptiveCompressionStrategy(
139 | 1024,
140 | 6,
141 | 1024,
142 | 0.5,
143 | 0.7,
144 | 100
145 | );
146 |
147 | $data = str_repeat('test', 500);
148 |
149 | // Cold data (low access frequency)
150 | $cache = $this->app['cache']->store();
151 | $key = 'test_cold_key';
152 |
153 | $optimized = $strategy->optimize($data, ['key' => $key, 'cache' => $cache]);
154 |
155 | // Cold data may use higher compression for better space savings
156 | $this->assertTrue($optimized['_sc_compressed']);
157 | $this->assertGreaterThanOrEqual(1, $optimized['level']);
158 | }
159 |
160 | public function test_compression_ratio_tracking()
161 | {
162 | $strategy = new AdaptiveCompressionStrategy(1024);
163 |
164 | $data = str_repeat('test', 1000);
165 | $optimized = $strategy->optimize($data);
166 |
167 | $this->assertArrayHasKey('original_size', $optimized);
168 | $this->assertArrayHasKey('compressed_size', $optimized);
169 |
170 | $originalSize = $optimized['original_size'];
171 | $compressedSize = $optimized['compressed_size'];
172 |
173 | // Compressed size should be smaller
174 | $this->assertLessThan($originalSize, $compressedSize);
175 | }
176 |
177 | public function test_handles_non_compressible_data()
178 | {
179 | $strategy = new AdaptiveCompressionStrategy(1024);
180 |
181 | // Random data (not very compressible)
182 | $randomData = random_bytes(5000);
183 |
184 | $optimized = $strategy->optimize($randomData);
185 | $restored = $strategy->restore($optimized);
186 |
187 | $this->assertEquals($randomData, $restored);
188 | }
189 |
190 | public function test_compression_with_different_thresholds()
191 | {
192 | $strategy1 = new AdaptiveCompressionStrategy(1024);
193 | $strategy2 = new AdaptiveCompressionStrategy(10240);
194 |
195 | $data = str_repeat('test', 500); // ~2KB
196 |
197 | $this->assertTrue($strategy1->shouldApply($data));
198 | $this->assertFalse($strategy2->shouldApply($data));
199 | }
200 | }
201 |
202 |
--------------------------------------------------------------------------------
/tests/Unit/Strategies/CompressionStrategyTest.php:
--------------------------------------------------------------------------------
1 | strategy = new CompressionStrategy(1024, 6); // 1KB threshold, level 6
16 | }
17 |
18 | public function test_get_identifier_returns_compression()
19 | {
20 | $this->assertEquals('compression', $this->strategy->getIdentifier());
21 | }
22 |
23 | public function test_should_apply_returns_true_for_large_string()
24 | {
25 | $largeString = str_repeat('a', 2000); // 2KB string
26 | $this->assertTrue($this->strategy->shouldApply($largeString));
27 | }
28 |
29 | public function test_should_apply_returns_false_for_small_string()
30 | {
31 | $smallString = str_repeat('a', 500); // 500 bytes string
32 | $this->assertFalse($this->strategy->shouldApply($smallString));
33 | }
34 |
35 | public function test_should_apply_returns_true_for_large_array()
36 | {
37 | $largeArray = $this->createLargeTestData(50); // Creates large array
38 | $this->assertTrue($this->strategy->shouldApply($largeArray));
39 | }
40 |
41 | public function test_should_apply_returns_false_for_small_array()
42 | {
43 | $smallArray = ['key' => 'value'];
44 | $this->assertFalse($this->strategy->shouldApply($smallArray));
45 | }
46 |
47 | public function test_should_apply_returns_true_for_large_object()
48 | {
49 | $largeObject = (object) $this->createLargeTestData(50);
50 | $this->assertTrue($this->strategy->shouldApply($largeObject));
51 | }
52 |
53 | public function test_should_apply_returns_false_for_non_compressible_types()
54 | {
55 | $this->assertFalse($this->strategy->shouldApply(123));
56 | $this->assertFalse($this->strategy->shouldApply(123.45));
57 | $this->assertFalse($this->strategy->shouldApply(true));
58 | $this->assertFalse($this->strategy->shouldApply(null));
59 | }
60 |
61 | public function test_should_apply_respects_driver_configuration()
62 | {
63 | $largeString = str_repeat('a', 2000);
64 |
65 | $context = [
66 | 'driver' => 'redis',
67 | 'config' => [
68 | 'drivers' => [
69 | 'redis' => [
70 | 'compression' => false
71 | ]
72 | ]
73 | ]
74 | ];
75 |
76 | $this->assertFalse($this->strategy->shouldApply($largeString, $context));
77 | }
78 |
79 | public function test_optimize_compresses_string_data()
80 | {
81 | $originalString = str_repeat('Hello World! ', 200); // Repetitive string compresses well
82 | $optimized = $this->strategy->optimize($originalString);
83 |
84 | $this->assertIsArray($optimized);
85 | $this->assertArrayHasKey('_sc_compressed', $optimized);
86 | $this->assertTrue($optimized['_sc_compressed']);
87 | $this->assertArrayHasKey('data', $optimized);
88 | $this->assertArrayHasKey('is_string', $optimized);
89 | $this->assertTrue($optimized['is_string']);
90 |
91 | // Verify the compressed data is base64 encoded
92 | $this->assertNotFalse(base64_decode($optimized['data'], true));
93 | }
94 |
95 | public function test_optimize_compresses_array_data()
96 | {
97 | $originalArray = $this->createLargeTestData(50);
98 | $optimized = $this->strategy->optimize($originalArray);
99 |
100 | $this->assertIsArray($optimized);
101 | $this->assertArrayHasKey('_sc_compressed', $optimized);
102 | $this->assertTrue($optimized['_sc_compressed']);
103 | $this->assertArrayHasKey('data', $optimized);
104 | $this->assertArrayHasKey('is_string', $optimized);
105 | $this->assertFalse($optimized['is_string']);
106 | }
107 |
108 | public function test_optimize_compresses_object_data()
109 | {
110 | $originalObject = (object) $this->createLargeTestData(50);
111 | $optimized = $this->strategy->optimize($originalObject);
112 |
113 | $this->assertIsArray($optimized);
114 | $this->assertArrayHasKey('_sc_compressed', $optimized);
115 | $this->assertTrue($optimized['_sc_compressed']);
116 | $this->assertArrayHasKey('is_string', $optimized);
117 | $this->assertFalse($optimized['is_string']);
118 | }
119 |
120 | public function test_restore_returns_original_string()
121 | {
122 | $originalString = str_repeat('Hello World! ', 200);
123 | $optimized = $this->strategy->optimize($originalString);
124 | $restored = $this->strategy->restore($optimized);
125 |
126 | $this->assertEquals($originalString, $restored);
127 | }
128 |
129 | public function test_restore_returns_original_array()
130 | {
131 | $originalArray = $this->createLargeTestData(50);
132 | $optimized = $this->strategy->optimize($originalArray);
133 | $restored = $this->strategy->restore($optimized);
134 |
135 | $this->assertEquals($originalArray, $restored);
136 | }
137 |
138 | public function test_restore_returns_original_object()
139 | {
140 | $originalObject = (object) $this->createLargeTestData(50);
141 | $optimized = $this->strategy->optimize($originalObject);
142 | $restored = $this->strategy->restore($optimized);
143 |
144 | $this->assertEquals($originalObject, $restored);
145 | }
146 |
147 | public function test_restore_returns_unmodified_value_if_not_compressed()
148 | {
149 | $normalValue = 'not compressed';
150 | $restored = $this->strategy->restore($normalValue);
151 | $this->assertEquals($normalValue, $restored);
152 |
153 | $normalArray = ['not' => 'compressed'];
154 | $restored = $this->strategy->restore($normalArray);
155 | $this->assertEquals($normalArray, $restored);
156 | }
157 |
158 | public function test_restore_returns_unmodified_value_if_invalid_compressed_format()
159 | {
160 | $invalidCompressed = [
161 | '_sc_compressed' => false, // Wrong marker
162 | 'data' => 'some-data',
163 | 'is_string' => true
164 | ];
165 |
166 | $restored = $this->strategy->restore($invalidCompressed);
167 | $this->assertEquals($invalidCompressed, $restored);
168 | }
169 |
170 | public function test_compression_reduces_size()
171 | {
172 | $repetitiveString = str_repeat('This is a test string that should compress very well. ', 100);
173 | $originalSize = strlen($repetitiveString);
174 |
175 | $optimized = $this->strategy->optimize($repetitiveString);
176 | $compressedSize = strlen($optimized['data']);
177 |
178 | // Base64 adds some overhead, but the compressed data should still be significantly smaller
179 | // than the original for repetitive data
180 | $this->assertLessThan($originalSize, $compressedSize);
181 | }
182 |
183 | public function test_compression_with_different_levels()
184 | {
185 | $data = str_repeat('Test data for compression level testing. ', 100);
186 |
187 | $strategy1 = new CompressionStrategy(1024, 1); // Low compression
188 | $strategy9 = new CompressionStrategy(1024, 9); // High compression
189 |
190 | $optimized1 = $strategy1->optimize($data);
191 | $optimized9 = $strategy9->optimize($data);
192 |
193 | // Higher compression level should produce smaller result
194 | $size1 = strlen($optimized1['data']);
195 | $size9 = strlen($optimized9['data']);
196 |
197 | $this->assertLessThanOrEqual($size1, $size9);
198 |
199 | // Both should restore to the same original data
200 | $restored1 = $strategy1->restore($optimized1);
201 | $restored9 = $strategy9->restore($optimized9);
202 |
203 | $this->assertEquals($data, $restored1);
204 | $this->assertEquals($data, $restored9);
205 | }
206 |
207 | public function test_compression_with_custom_threshold()
208 | {
209 | $strategy = new CompressionStrategy(5000, 6); // 5KB threshold
210 |
211 | $smallData = str_repeat('a', 2000); // 2KB
212 | $largeData = str_repeat('a', 6000); // 6KB
213 |
214 | $this->assertFalse($strategy->shouldApply($smallData));
215 | $this->assertTrue($strategy->shouldApply($largeData));
216 | }
217 |
218 | public function test_round_trip_compression_preserves_data_integrity()
219 | {
220 | $testCases = [
221 | 'Simple string',
222 | str_repeat('Repetitive content ', 100),
223 | ['array' => 'data', 'nested' => ['deep' => 'value']],
224 | $this->createLargeTestData(30),
225 | (object) ['property' => 'value', 'nested' => (object) ['deep' => 'data']]
226 | ];
227 |
228 | foreach ($testCases as $testCase) {
229 | $optimized = $this->strategy->optimize($testCase);
230 | $restored = $this->strategy->restore($optimized);
231 |
232 | $this->assertEquals($testCase, $restored,
233 | 'Round-trip compression should preserve data integrity for: ' . gettype($testCase));
234 | }
235 | }
236 | }
237 |
--------------------------------------------------------------------------------
/tests/Unit/Strategies/SmartSerializationStrategyTest.php:
--------------------------------------------------------------------------------
1 | assertTrue($strategy->shouldApply('any_value'));
15 | $this->assertTrue($strategy->shouldApply(['array']));
16 | $this->assertTrue($strategy->shouldApply(new \stdClass()));
17 | }
18 |
19 | public function test_optimize_serializes_data()
20 | {
21 | $strategy = new SmartSerializationStrategy();
22 |
23 | $data = ['key' => 'value'];
24 | $optimized = $strategy->optimize($data);
25 |
26 | $this->assertIsArray($optimized);
27 | $this->assertTrue($optimized['_sc_serialized']);
28 | $this->assertArrayHasKey('method', $optimized);
29 | $this->assertArrayHasKey('data', $optimized);
30 | }
31 |
32 | public function test_restore_unserializes_data()
33 | {
34 | $strategy = new SmartSerializationStrategy();
35 |
36 | $originalData = ['key' => 'value', 'number' => 123];
37 | $optimized = $strategy->optimize($originalData);
38 | $restored = $strategy->restore($optimized);
39 |
40 | $this->assertEquals($originalData, $restored);
41 | }
42 |
43 | public function test_json_serialization_for_simple_data()
44 | {
45 | $strategy = new SmartSerializationStrategy('auto', true);
46 |
47 | $simpleData = ['key' => 'value', 'number' => 123, 'bool' => true];
48 | $optimized = $strategy->optimize($simpleData);
49 |
50 | $this->assertEquals('json', $optimized['method']);
51 |
52 | $restored = $strategy->restore($optimized);
53 | $this->assertEquals($simpleData, $restored);
54 | }
55 |
56 | public function test_php_serialization_for_objects()
57 | {
58 | // Force PHP serialization mode
59 | $strategy = new SmartSerializationStrategy('php', false);
60 |
61 | // Use stdClass
62 | $object = new \stdClass();
63 | $object->property = 'value';
64 | $object->nested = new \stdClass();
65 | $object->nested->data = 'nested_value';
66 |
67 | $optimized = $strategy->optimize($object);
68 |
69 | // Should use PHP serialize
70 | $this->assertEquals('php', $optimized['method']);
71 |
72 | $restored = $strategy->restore($optimized);
73 | $this->assertEquals($object->property, $restored->property);
74 | $this->assertEquals($object->nested->data, $restored->nested->data);
75 | }
76 |
77 | public function test_forced_json_serialization()
78 | {
79 | $strategy = new SmartSerializationStrategy('json', false);
80 |
81 | $data = ['key' => 'value'];
82 | $optimized = $strategy->optimize($data);
83 |
84 | $this->assertEquals('json', $optimized['method']);
85 | }
86 |
87 | public function test_forced_php_serialization()
88 | {
89 | $strategy = new SmartSerializationStrategy('php', false);
90 |
91 | $data = ['key' => 'value'];
92 | $optimized = $strategy->optimize($data);
93 |
94 | $this->assertEquals('php', $optimized['method']);
95 | }
96 |
97 | public function test_igbinary_serialization_if_available()
98 | {
99 | if (!function_exists('igbinary_serialize')) {
100 | $this->markTestSkipped('igbinary extension not available');
101 | }
102 |
103 | $strategy = new SmartSerializationStrategy('igbinary', false);
104 |
105 | $data = ['key' => 'value'];
106 | $optimized = $strategy->optimize($data);
107 |
108 | $this->assertEquals('igbinary', $optimized['method']);
109 |
110 | $restored = $strategy->restore($optimized);
111 | $this->assertEquals($data, $restored);
112 | }
113 |
114 | public function test_get_identifier()
115 | {
116 | $strategy = new SmartSerializationStrategy();
117 |
118 | $this->assertEquals('smart_serialization', $strategy->getIdentifier());
119 | }
120 |
121 | public function test_serialization_stats()
122 | {
123 | $strategy = new SmartSerializationStrategy();
124 |
125 | $data = ['key' => 'value', 'number' => 123];
126 | $stats = $strategy->getSerializationStats($data);
127 |
128 | $this->assertArrayHasKey('json', $stats);
129 | $this->assertArrayHasKey('php', $stats);
130 | $this->assertArrayHasKey('recommended', $stats);
131 | $this->assertArrayHasKey('best_size', $stats);
132 | }
133 |
134 | public function test_json_is_more_compact_for_simple_data()
135 | {
136 | $strategy = new SmartSerializationStrategy();
137 |
138 | $simpleData = ['a' => 1, 'b' => 2, 'c' => 3];
139 | $stats = $strategy->getSerializationStats($simpleData);
140 |
141 | // JSON should be recommended for simple data
142 | $this->assertEquals('json', $stats['recommended']);
143 | }
144 |
145 | public function test_handles_large_arrays()
146 | {
147 | $strategy = new SmartSerializationStrategy();
148 |
149 | $largeArray = array_fill(0, 10000, 'test_data');
150 | $optimized = $strategy->optimize($largeArray);
151 | $restored = $strategy->restore($optimized);
152 |
153 | $this->assertCount(10000, $restored);
154 | $this->assertEquals($largeArray, $restored);
155 | }
156 |
157 | public function test_handles_nested_arrays()
158 | {
159 | $strategy = new SmartSerializationStrategy();
160 |
161 | $nestedData = [
162 | 'level1' => [
163 | 'level2' => [
164 | 'level3' => ['value' => 'deep']
165 | ]
166 | ]
167 | ];
168 |
169 | $optimized = $strategy->optimize($nestedData);
170 | $restored = $strategy->restore($optimized);
171 |
172 | $this->assertEquals($nestedData, $restored);
173 | }
174 |
175 | public function test_handles_mixed_types()
176 | {
177 | $strategy = new SmartSerializationStrategy();
178 |
179 | $mixedData = [
180 | 'string' => 'value',
181 | 'int' => 123,
182 | 'float' => 45.67,
183 | 'bool' => true,
184 | 'null' => null,
185 | 'array' => [1, 2, 3],
186 | ];
187 |
188 | $optimized = $strategy->optimize($mixedData);
189 | $restored = $strategy->restore($optimized);
190 |
191 | $this->assertEquals($mixedData, $restored);
192 | }
193 |
194 | public function test_fallback_to_php_for_unsupported_method()
195 | {
196 | $strategy = new SmartSerializationStrategy('igbinary', false);
197 |
198 | if (function_exists('igbinary_serialize')) {
199 | $this->markTestSkipped('igbinary is available, cannot test fallback');
200 | }
201 |
202 | $data = ['key' => 'value'];
203 | $optimized = $strategy->optimize($data);
204 |
205 | // Should fallback to PHP serialize
206 | $this->assertEquals('php', $optimized['method']);
207 | }
208 |
209 | public function test_restore_handles_non_serialized_data()
210 | {
211 | $strategy = new SmartSerializationStrategy();
212 |
213 | $plainData = 'not_serialized';
214 | $restored = $strategy->restore($plainData);
215 |
216 | $this->assertEquals($plainData, $restored);
217 | }
218 | }
219 |
220 |
--------------------------------------------------------------------------------
/tests/bootstrap.php:
--------------------------------------------------------------------------------
1 | isDir() ? 'rmdir' : 'unlink');
51 | $todo($fileinfo->getRealPath());
52 | }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------