diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index c68765b..6b23e5b 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1 +1 @@ -github: :vendor_name +github: Mahmoud217TR diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 96701be..735068f 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,11 +1,11 @@ blank_issues_enabled: false contact_links: - name: Ask a question - url: https://github.com/:vendor_name/:package_name/discussions/new?category=q-a + url: https://github.com/Mahmoud217TR/cacheable/discussions/new?category=q-a about: Ask the community for help - name: Request a feature - url: https://github.com/:vendor_name/:package_name/discussions/new?category=ideas + url: https://github.com/Mahmoud217TR/cacheable/discussions/new?category=ideas about: Share ideas for new features - name: Report a security issue - url: https://github.com/:vendor_name/:package_name/security/policy + url: https://github.com/Mahmoud217TR/cacheable/security/policy about: Learn how to notify us for sensitive bugs diff --git a/CHANGELOG.md b/CHANGELOG.md index 87b3242..2eb6862 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,3 @@ # Changelog -All notable changes to `:package_name` will be documented in this file. +All notable changes to `cacheable` will be documented in this file. diff --git a/LICENSE.md b/LICENSE.md index 58c9ad4..a6c5838 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) :vendor_name +Copyright (c) Mahmoud217TR Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 375da96..6e2105d 100644 --- a/README.md +++ b/README.md @@ -1,68 +1,614 @@ -# :package_description +# Cacheable for Laravel -[![Latest Version on Packagist](https://img.shields.io/packagist/v/:vendor_slug/:package_slug.svg?style=flat-square)](https://packagist.org/packages/:vendor_slug/:package_slug) -[![GitHub Tests Action Status](https://img.shields.io/github/actions/workflow/status/:vendor_slug/:package_slug/run-tests.yml?branch=main&label=tests&style=flat-square)](https://github.com/:vendor_slug/:package_slug/actions?query=workflow%3Arun-tests+branch%3Amain) -[![GitHub Code Style Action Status](https://img.shields.io/github/actions/workflow/status/:vendor_slug/:package_slug/fix-php-code-style-issues.yml?branch=main&label=code%20style&style=flat-square)](https://github.com/:vendor_slug/:package_slug/actions?query=workflow%3A"Fix+PHP+code+style+issues"+branch%3Amain) -[![Total Downloads](https://img.shields.io/packagist/dt/:vendor_slug/:package_slug.svg?style=flat-square)](https://packagist.org/packages/:vendor_slug/:package_slug) - ---- -This repo can be used to scaffold a Laravel package. Follow these steps to get started: +[![Latest Version on Packagist](https://img.shields.io/packagist/v/mahmoud217tr/cacheable.svg?style=flat-square)](https://packagist.org/packages/mahmoud217tr/cacheable) +[![GitHub Tests Action Status](https://img.shields.io/github/actions/workflow/status/mahmoud217tr/cacheable/run-tests.yml?branch=main&label=tests&style=flat-square)](https://github.com/mahmoud217tr/cacheable/actions?query=workflow%3Arun-tests+branch%3Amain) +[![GitHub Code Style Action Status](https://img.shields.io/github/actions/workflow/status/mahmoud217tr/cacheable/fix-php-code-style-issues.yml?branch=main&label=code%20style&style=flat-square)](https://github.com/mahmoud217tr/cacheable/actions?query=workflow%3A"Fix+PHP+code+style+issues"+branch%3Amain) +[![Total Downloads](https://img.shields.io/packagist/dt/mahmoud217tr/cacheable.svg?style=flat-square)](https://packagist.org/packages/mahmoud217tr/cacheable) -1. Press the "Use this template" button at the top of this repo to create a new repo with the contents of this skeleton. -2. Run "php ./configure.php" to run a script that will replace all placeholders throughout all the files. -3. Have fun creating your package. -4. If you need help creating a package, consider picking up our Laravel Package Training video course. ---- - -This is where your description should go. Limit it to a paragraph or two. Consider adding a small example. +**Effortless and Enhanced Caching for Models and Classes** -## Support us +Laravel package that provides a streamlined and powerful solution for implementing caching within your application. This package simplifies the process of caching Eloquent models and other classes, ensuring improved performance and scalability for your Laravel application. -[](https://spatie.be/github-ad-click/:package_name) +![logo](/assets/cacheable.svg) -We invest a lot of resources into creating [best in class open source packages](https://spatie.be/open-source). You can support us by [buying one of our paid products](https://spatie.be/open-source/support-us). +## Table of Contents + +- [Installation](#installation) +- [Usage](#usage) + 1. [Cacheable Models](#1-cacheable-models) + 1. [Easy Caching for Model Records and Collections](#1-easy-caching-for-model-records-and-collections) + 2. [Auto-Caching Model Records](#2-auto-caching-model-records) + 3. [Cached Route Model Binding](#3-cached-route-model-binding) + 2. [Cacheable Interface & Trait](#2-cacheable-interface--trait) + 3. [Cacheable Facade](#3-cacheable-facade) + 4. [Helper Functions](#4-helper-functions) +- [Publishing](#publishing) +- [Testing](#testing) +- [Changelog](#changelog) +- [Security Vulnerabilities](#security-vulnerabilities) +- [Credits](#credits) +- [License](#license) -We highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using. You'll find our address on [our contact page](https://spatie.be/about-us). We publish all received postcards on [our virtual postcard wall](https://spatie.be/open-source/postcards). ## Installation -You can install the package via composer: +To install the package to your Laravel project via Composer: ```bash -composer require :vendor_slug/:package_slug +composer require mahmoud217tr/cacheable ``` -You can publish and run the migrations with: +And once the installation is complete, the package will be ready up and ready for usage. -```bash -php artisan vendor:publish --tag=":package_slug-migrations" -php artisan migrate +## Usage + +There is a lot of features and usecases we will summerize them with the following: + +### 1. Cacheable Models + +You can make the model cacheable by making it implement `CacheableModel` interface and use the `CacheableModelTrait` trait as the following example: + +```php + Upon doing that your model will be using the `Cacheable Collection` as a collection which extends the `Eloquent Collection`, preserving the same logic and providing the model with caching features. -```bash -php artisan vendor:publish --tag=":package_slug-config" +Making a model cacheable will provide you with caching features: + +#### 1. Easy Caching for Model Records and Collections + +You can easily cache individual model records or collections of records using the `cache` method as follows: + +```php +cache('first_post', 120); + +# Caching published posts indefinitely +Post::whereNotNull('published_at') + ->get() + ->cache('published_posts'); + +# Retrieve cached data using the Cache facade +Cache::get('first_post'); +Cache::get('published_posts'); + +# Or using the Cacheable facade +Cacheable::get('first_post'); +Cacheable::get('published_posts'); ``` -This is the contents of the published config file: +#### 2. Auto-Caching Model Records + +Models can be auto-cached, meaning all model records will be cached and synchronized upon creation, updating, or deletion. + +> **CAUTION**: This behavior may be unsuitable for large models or models with frequent changes. Use it wisely based on your use case. + +To enable auto-caching, override the `isAutoCacheSyncEnabled` method in your model to return `true`: ```php -return [ -]; + **IMPORTANT**: If you have modular Laravel application or you've changed your models default director, you'll need to do an [extra step](#model-directories). + +You can manage the cached models as follows: + +```php +get()); ``` -## Usage +You can also control the auto-cached collection by overriding the `getDataForCaching` method as it's default behaviour is to return the `all` method result: ```php -$variable = new VendorName\Skeleton(); -echo $variable->echoPhrase('Hello, VendorName!'); +whereNotNull('published_at'); + } + + public static function isAutoCacheSyncEnabled(): bool + { + return true; + } + + protected static function getDataForCaching() + { + return static::published() + ->latest() + ->get(); + } +} +``` + +And now only the published posts will cached by auto-caching feature and in order of the latest. + +You can also control the `TTL` of the cached data by overriding the `getCacheTTL` method: + +```php +whereNotNull('published_at'); + } + + public static function isAutoCacheSyncEnabled(): bool + { + return true; + } + + protected static function getDataForCaching() + { + return static::published() + ->latest() + ->get(); + } + + protected static function getCacheTTL(): null | int | DateInterval | DateTimeInterface + { + return 86400; + } +} +``` + +#### 3. Cached Route Model Binding + +You can also utilize the cached records to be used in route model binding by simply be using the `CachedRouteBinding` trait: + +```php +whereNotNull('published_at'); + } + + public static function isAutoCacheSyncEnabled(): bool + { + return true; + } + + protected static function getDataForCaching() + { + return static::published() + ->latest() + ->get(); + } +} +``` + +> **Note**: Modifying the auto-cached data by overriding the `getDataForCaching` method may result in `404 Not Found` for non-cached model records. Solutions for this scenario will be discussed further. + +You can change the cached data that is used for route model binding by overriding 2 methods: +- You should override the `shouldUseDifferentDataForBinding` method to return `true` *(which by default it returns `false`)*. +- And you ou should also override the `getBindingData` method which represents the data collection to be cached *(by default it returns `null`)*. + +```php +whereNotNull('published_at'); + } + + public static function isAutoCacheSyncEnabled(): bool + { + return true; + } + + protected static function getDataForCaching() + { + return static::published() + ->latest() + ->get(); + } + + protected static function shouldUseDifferentDataForBinding(): bool + { + return true; + } + + protected static function getBindingData() + { + return static::all(); + } +} +``` + +And now the data used for route model binding will be cached with a different key and have different values and will be synconized automatically. + +**When** the usage of a different data for binding is enabled, you can control the `TTL` of the route binding cache by overriding the `getBindingCacheTTL` method: +```php +whereNotNull('published_at'); + } + + public static function isAutoCacheSyncEnabled(): bool + { + return true; + } + + protected static function getDataForCaching() + { + return static::published() + ->latest() + ->get(); + } + + protected static function shouldUseDifferentDataForBinding(): bool + { + return true; + } + + protected static function getBindingData() + { + return static::all(); + } + + protected static function getBindingCacheTTL(): null | int| DateInterval | DateTimeInterface + { + return 3600; + } +} +``` + +You can also allow the model to use an alternative route binding method by overriding the `shouldUseAlternativeRouteBinding` method to return `true`: + +```php +whereNotNull('published_at'); + } + + public static function isAutoCacheSyncEnabled(): bool + { + return true; + } + + protected static function getDataForCaching() + { + return static::published() + ->latest() + ->get(); + } + + protected static function shouldUseDifferentDataForBinding(): bool + { + return true; + } + + protected static function shouldUseDifferentDataForBinding(): bool + { + return true; + } + + protected static function getBindingData() + { + return static::all(); + } + + protected static function shouldUseAlternativeRouteBinding(): bool + { + return true; + } +} +``` + +The default alternative route binding resolver is the `resolveRouteBinding` method, which can be customized by overriding the `alternativeRouteBinding` method: + +```php +whereNotNull('published_at'); + } + + public static function isAutoCacheSyncEnabled(): bool + { + return true; + } + + protected static function getDataForCaching() + { + return static::published() + ->latest() + ->get(); + } + + protected static function shouldUseDifferentDataForBinding(): bool + { + return true; + } + + protected static function getBindingData() + { + return static::all(); + } + + protected static function shouldUseAlternativeRouteBinding(): bool + { + return true; + } + + protected static function alternativeRouteBinding($value, $field = null) + { + return parent::customRouteBinding($value, $field); + } +} +``` + +### 2. Cacheable Interface & Trait + +You can easily make any thing cacheable by making it implements the `Cacheable` interface and use the `CacahableTrait` trait: + +```php +customAttribute = $value; + } +} +``` + +And you can use the cache function to store the object as follows: + +```php +cache('cache_key_2'); + +# Caching object for 200 seconds +$object->cache('cache_key', 200); + +``` + +### 3. Cacheable Facade + +You can also utilize the `Cacheable` facade for some caching features, which has some drived features from the `Cache` facade: + +```php + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/composer.json b/composer.json index 65e9908..628821b 100644 --- a/composer.json +++ b/composer.json @@ -1,24 +1,30 @@ { - "name": ":vendor_slug/:package_slug", - "description": ":package_description", + "name": "mahmoud217tr/cacheable", + "description": "Laravel package for easing and enhance caching for models and cacheable classes", "keywords": [ - ":vendor_name", - "laravel", - ":package_slug" + "Mahmoud217TR", + "cacheable", + "Laravel", + "caching", + "cache", + "model-caching" ], - "homepage": "https://github.com/:vendor_slug/:package_slug", + "homepage": "https://github.com/mahmoud217tr/cacheable", "license": "MIT", "authors": [ { - "name": ":author_name", - "email": "author@domain.com", + "name": "Mahmoud Mahmoud", + "email": "mahmoud17tr@gmail.com", + "homepage": "https://github.com/Mahmoud217TR", "role": "Developer" } ], "require": { - "php": "^8.2", + "php": "^8.0", "spatie/laravel-package-tools": "^1.16", - "illuminate/contracts": "^10.0||^11.0" + "illuminate/contracts": "^9.0|^10.0|^11.0", + "illuminate/support": "^9.0|^10.0|^11.0", + "illuminate/cache": "^9.0|^10.0|^11.0" }, "require-dev": { "laravel/pint": "^1.14", @@ -30,24 +36,25 @@ "pestphp/pest-plugin-laravel": "^2.3", "phpstan/extension-installer": "^1.3", "phpstan/phpstan-deprecation-rules": "^1.1", - "phpstan/phpstan-phpunit": "^1.3", - "spatie/laravel-ray": "^1.35" + "phpstan/phpstan-phpunit": "^1.3" }, "autoload": { "psr-4": { - "VendorName\\Skeleton\\": "src/", - "VendorName\\Skeleton\\Database\\Factories\\": "database/factories/" - } + "Mahmoud217TR\\Cacheable\\": "src/" + }, + "files": [ + "src/helpers.php" + ] }, "autoload-dev": { "psr-4": { - "VendorName\\Skeleton\\Tests\\": "tests/", + "Mahmoud217TR\\Cacheable\\Tests\\": "tests/", "Workbench\\App\\": "workbench/app/" } }, "scripts": { "post-autoload-dump": "@composer run prepare", - "clear": "@php vendor/bin/testbench package:purge-skeleton --ansi", + "clear": "@php vendor/bin/testbench package:purge-cacheable --ansi", "prepare": "@php vendor/bin/testbench package:discover --ansi", "build": [ "@composer run prepare", @@ -73,10 +80,10 @@ "extra": { "laravel": { "providers": [ - "VendorName\\Skeleton\\SkeletonServiceProvider" + "Mahmoud217TR\\Cacheable\\CacheableServiceProvider" ], "aliases": { - "Skeleton": "VendorName\\Skeleton\\Facades\\Skeleton" + "Cacheable": "Mahmoud217TR\\Cacheable\\Facades\\Cacheable" } } }, diff --git a/config/cacheable.php b/config/cacheable.php new file mode 100644 index 0000000..fab7c43 --- /dev/null +++ b/config/cacheable.php @@ -0,0 +1,13 @@ + false, +]; diff --git a/config/skeleton.php b/config/skeleton.php deleted file mode 100644 index 7e74186..0000000 --- a/config/skeleton.php +++ /dev/null @@ -1,6 +0,0 @@ - $version) { - if (in_array($name, $names, true)) { - unset($data['require-dev'][$name]); - } - } - - file_put_contents(__DIR__.'/composer.json', json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)); -} - -function remove_composer_script($scriptName) -{ - $data = json_decode(file_get_contents(__DIR__.'/composer.json'), true); - - foreach ($data['scripts'] as $name => $script) { - if ($scriptName === $name) { - unset($data['scripts'][$name]); - break; - } - } - - file_put_contents(__DIR__.'/composer.json', json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)); -} - -function remove_readme_paragraphs(string $file): void -{ - $contents = file_get_contents($file); - - file_put_contents( - $file, - preg_replace('/.*/s', '', $contents) ?: $contents - ); -} - -function safeUnlink(string $filename) -{ - if (file_exists($filename) && is_file($filename)) { - unlink($filename); - } -} - -function determineSeparator(string $path): string -{ - return str_replace('/', DIRECTORY_SEPARATOR, $path); -} - -function replaceForWindows(): array -{ - return preg_split('/\\r\\n|\\r|\\n/', run('dir /S /B * | findstr /v /i .git\ | findstr /v /i vendor | findstr /v /i '.basename(__FILE__).' | findstr /r /i /M /F:/ ":author :vendor :package VendorName skeleton migration_table_name vendor_name vendor_slug author@domain.com"')); -} - -function replaceForAllOtherOSes(): array -{ - return explode(PHP_EOL, run('grep -E -r -l -i ":author|:vendor|:package|VendorName|skeleton|migration_table_name|vendor_name|vendor_slug|author@domain.com" --exclude-dir=vendor ./* ./.github/* | grep -v '.basename(__FILE__))); -} - -function getGitHubApiEndpoint(string $endpoint): ?stdClass -{ - try { - $curl = curl_init("https://api.github.com/{$endpoint}"); - curl_setopt_array($curl, [ - CURLOPT_RETURNTRANSFER => true, - CURLOPT_FOLLOWLOCATION => true, - CURLOPT_HTTPGET => true, - CURLOPT_HTTPHEADER => [ - 'User-Agent: spatie-configure-script/1.0', - ], - ]); - - $response = curl_exec($curl); - $statusCode = curl_getinfo($curl, CURLINFO_HTTP_CODE); - - curl_close($curl); - - if ($statusCode === 200) { - return json_decode($response); - } - } catch (Exception $e) { - // ignore - } - - return null; -} - -function searchCommitsForGitHubUsername(): string -{ - $authorName = strtolower(trim(shell_exec('git config user.name'))); - - $committersRaw = shell_exec("git log --author='@users.noreply.github.com' --pretty='%an:%ae' --reverse"); - $committersLines = explode("\n", $committersRaw ?? ''); - $committers = array_filter(array_map(function ($line) use ($authorName) { - $line = trim($line); - [$name, $email] = explode(':', $line) + [null, null]; - - return [ - 'name' => $name, - 'email' => $email, - 'isMatch' => strtolower($name) === $authorName && ! str_contains($name, '[bot]'), - ]; - }, $committersLines), fn ($item) => $item['isMatch']); - - if (empty($committers)) { - return ''; - } - - $firstCommitter = reset($committers); - - return explode('@', $firstCommitter['email'])[0] ?? ''; -} - -function guessGitHubUsernameUsingCli() -{ - try { - if (preg_match('/ogged in to github\.com as ([a-zA-Z-_]+).+/', shell_exec('gh auth status -h github.com 2>&1'), $matches)) { - return $matches[1]; - } - } catch (Exception $e) { - // ignore - } - - return ''; -} - -function guessGitHubUsername(): string -{ - $username = searchCommitsForGitHubUsername(); - if (! empty($username)) { - return $username; - } - - $username = guessGitHubUsernameUsingCli(); - if (! empty($username)) { - return $username; - } - - // fall back to using the username from the git remote - $remoteUrl = shell_exec('git config remote.origin.url'); - $remoteUrlParts = explode('/', str_replace(':', '/', trim($remoteUrl))); - - return $remoteUrlParts[1] ?? ''; -} - -function guessGitHubVendorInfo($authorName, $username): array -{ - $remoteUrl = shell_exec('git config remote.origin.url'); - $remoteUrlParts = explode('/', str_replace(':', '/', trim($remoteUrl))); - - $response = getGitHubApiEndpoint("orgs/{$remoteUrlParts[1]}"); - - if ($response === null) { - return [$authorName, $username]; - } - - return [$response->name ?? $authorName, $response->login ?? $username]; -} - -$gitName = run('git config user.name'); -$authorName = ask('Author name', $gitName); - -$gitEmail = run('git config user.email'); -$authorEmail = ask('Author email', $gitEmail); -$authorUsername = ask('Author username', guessGitHubUsername()); - -$guessGitHubVendorInfo = guessGitHubVendorInfo($authorName, $authorUsername); - -$vendorName = ask('Vendor name', $guessGitHubVendorInfo[0]); -$vendorUsername = ask('Vendor username', $guessGitHubVendorInfo[1] ?? slugify($vendorName)); -$vendorSlug = slugify($vendorUsername); - -$vendorNamespace = str_replace('-', '', ucwords($vendorName)); -$vendorNamespace = ask('Vendor namespace', $vendorNamespace); - -$currentDirectory = getcwd(); -$folderName = basename($currentDirectory); - -$packageName = ask('Package name', $folderName); -$packageSlug = slugify($packageName); -$packageSlugWithoutPrefix = remove_prefix('laravel-', $packageSlug); - -$className = title_case($packageName); -$className = ask('Class name', $className); -$variableName = lcfirst($className); -$description = ask('Package description', "This is my package {$packageSlug}"); - -$usePhpStan = confirm('Enable PhpStan?', true); -$useLaravelPint = confirm('Enable Laravel Pint?', true); -$useDependabot = confirm('Enable Dependabot?', true); -$useLaravelRay = confirm('Use Ray for debugging?', true); -$useUpdateChangelogWorkflow = confirm('Use automatic changelog updater workflow?', true); - -writeln('------'); -writeln("Author : {$authorName} ({$authorUsername}, {$authorEmail})"); -writeln("Vendor : {$vendorName} ({$vendorSlug})"); -writeln("Package : {$packageSlug} <{$description}>"); -writeln("Namespace : {$vendorNamespace}\\{$className}"); -writeln("Class name : {$className}"); -writeln('---'); -writeln('Packages & Utilities'); -writeln('Use Laravel/Pint : '.($useLaravelPint ? 'yes' : 'no')); -writeln('Use Larastan/PhpStan : '.($usePhpStan ? 'yes' : 'no')); -writeln('Use Dependabot : '.($useDependabot ? 'yes' : 'no')); -writeln('Use Ray App : '.($useLaravelRay ? 'yes' : 'no')); -writeln('Use Auto-Changelog : '.($useUpdateChangelogWorkflow ? 'yes' : 'no')); -writeln('------'); - -writeln('This script will replace the above values in all relevant files in the project directory.'); - -if (! confirm('Modify files?', true)) { - exit(1); -} - -$files = (str_starts_with(strtoupper(PHP_OS), 'WIN') ? replaceForWindows() : replaceForAllOtherOSes()); - -foreach ($files as $file) { - replace_in_file($file, [ - ':author_name' => $authorName, - ':author_username' => $authorUsername, - 'author@domain.com' => $authorEmail, - ':vendor_name' => $vendorName, - ':vendor_slug' => $vendorSlug, - 'VendorName' => $vendorNamespace, - ':package_name' => $packageName, - ':package_slug' => $packageSlug, - ':package_slug_without_prefix' => $packageSlugWithoutPrefix, - 'Skeleton' => $className, - 'skeleton' => $packageSlug, - 'migration_table_name' => title_snake($packageSlug), - 'variable' => $variableName, - ':package_description' => $description, - ]); - - match (true) { - str_contains($file, determineSeparator('src/Skeleton.php')) => rename($file, determineSeparator('./src/'.$className.'.php')), - str_contains($file, determineSeparator('src/SkeletonServiceProvider.php')) => rename($file, determineSeparator('./src/'.$className.'ServiceProvider.php')), - str_contains($file, determineSeparator('src/Facades/Skeleton.php')) => rename($file, determineSeparator('./src/Facades/'.$className.'.php')), - str_contains($file, determineSeparator('src/Commands/SkeletonCommand.php')) => rename($file, determineSeparator('./src/Commands/'.$className.'Command.php')), - str_contains($file, determineSeparator('database/migrations/create_skeleton_table.php.stub')) => rename($file, determineSeparator('./database/migrations/create_'.title_snake($packageSlugWithoutPrefix).'_table.php.stub')), - str_contains($file, determineSeparator('config/skeleton.php')) => rename($file, determineSeparator('./config/'.$packageSlugWithoutPrefix.'.php')), - str_contains($file, 'README.md') => remove_readme_paragraphs($file), - default => [], - }; -} - -if (! $useLaravelPint) { - safeUnlink(__DIR__.'/.github/workflows/fix-php-code-style-issues.yml'); - safeUnlink(__DIR__.'/pint.json'); -} - -if (! $usePhpStan) { - safeUnlink(__DIR__.'/phpstan.neon.dist'); - safeUnlink(__DIR__.'/phpstan-baseline.neon'); - safeUnlink(__DIR__.'/.github/workflows/phpstan.yml'); - - remove_composer_deps([ - 'phpstan/extension-installer', - 'phpstan/phpstan-deprecation-rules', - 'phpstan/phpstan-phpunit', - 'larastan/larastan', - ]); - - remove_composer_script('phpstan'); -} - -if (! $useDependabot) { - safeUnlink(__DIR__.'/.github/dependabot.yml'); - safeUnlink(__DIR__.'/.github/workflows/dependabot-auto-merge.yml'); -} - -if (! $useLaravelRay) { - remove_composer_deps(['spatie/laravel-ray']); -} - -if (! $useUpdateChangelogWorkflow) { - safeUnlink(__DIR__.'/.github/workflows/update-changelog.yml'); -} - -confirm('Execute `composer install` and run tests?') && run('composer install && composer test'); - -confirm('Let this script delete itself?', true) && unlink(__FILE__); diff --git a/database/factories/ModelFactory.php b/database/factories/ModelFactory.php deleted file mode 100644 index c51604f..0000000 --- a/database/factories/ModelFactory.php +++ /dev/null @@ -1,19 +0,0 @@ -id(); - - // add fields - - $table->timestamps(); - }); - } -}; diff --git a/phpstan.neon.dist b/phpstan.neon.dist index ab1b4c3..4a0559d 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -6,7 +6,6 @@ parameters: paths: - src - config - - database tmpDir: build/phpstan checkOctaneCompatibility: true checkModelProperties: true diff --git a/phpunit.xml.dist b/phpunit.xml.dist index bfe434d..2172af3 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -16,7 +16,7 @@ backupStaticProperties="false" > - + tests diff --git a/resources/views/.gitkeep b/resources/views/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/Cacheable.php b/src/Cacheable.php new file mode 100644 index 0000000..14853a9 --- /dev/null +++ b/src/Cacheable.php @@ -0,0 +1,138 @@ +name('skeleton') - ->hasConfigFile() - ->hasViews() - ->hasMigration('create_skeleton_table') - ->hasCommand(SkeletonCommand::class); + ->name('cacheable') + ->hasConfigFile(); } } diff --git a/src/Collections/Collection.php b/src/Collections/Collection.php new file mode 100644 index 0000000..9582eef --- /dev/null +++ b/src/Collections/Collection.php @@ -0,0 +1,12 @@ +comment('All done'); - - return self::SUCCESS; - } -} diff --git a/src/Contracts/Cacheable.php b/src/Contracts/Cacheable.php new file mode 100644 index 0000000..518aff6 --- /dev/null +++ b/src/Contracts/Cacheable.php @@ -0,0 +1,11 @@ +syncCache(); + }); + + static::updated(function (CacheableModel $model) { + $model->syncCache(); + }); + + static::deleted(function (CacheableModel $model) { + $model->syncCache(); + }); + } + } + + public static function getCacheKey(): string + { + return static::class; + } + + public static function syncCache(): void + { + static::flushCache(); + static::setCache(static::getDataForCaching(), static::getCacheTTL()); + if (static::usesCachedRouteBinding()) { + static::syncBindingCache(); + } + } + + public static function flushCache(): void + { + Cacheable::forget(static::getCacheKey()); + } + + public static function setCache($data, int|DateInterval|DateTimeInterface|null $ttl = null): void + { + Cacheable::set( + static::getCacheKey(), + $data, + $ttl + ); + } + + public static function getCached() + { + return Cacheable::cached( + static::getCacheKey(), + fn () => static::getDataForCaching(), + static::getCacheTTL() + ); + } + + public static function isAutoCacheSyncEnabled(): bool + { + return Cacheable::config('auto_model_caching'); + } + + /** + * Create a new Eloquent Collection instance. + * + * @return \Illuminate\Database\Eloquent\Collection + */ + public function newCollection(array $models = []) + { + return new CacheableCollection($models); + } + + protected static function getDataForCaching() + { + return static::all(); + } + + protected static function getCacheTTL(): null|int|DateInterval|DateTimeInterface + { + return null; + } + + protected static function usesCachedRouteBinding(): bool + { + return in_array( + CachedRouteBinding::class, + array_keys(class_uses(static::class)) + ); + } +} diff --git a/src/Models/Traits/CachedRouteBinding.php b/src/Models/Traits/CachedRouteBinding.php new file mode 100644 index 0000000..bea8bc2 --- /dev/null +++ b/src/Models/Traits/CachedRouteBinding.php @@ -0,0 +1,86 @@ +where($field ?? $this->getRouteKeyName(), $value) + ->first(); + } + + if (is_null($model) && static::shouldUseAlternativeRouteBinding()) { + $model = static::alternativeRouteBinding($value, $field); + } + + return $model; + } + + public static function syncBindingCache(): void + { + if (static::shouldUseDifferentDataForBinding()) { + Cacheable::forget(static::getBindingCacheKey()); + static::getCachedBindingData(); + } + } + + public static function getBindingCacheKey(): string + { + return static::getCacheKey().'.ForBinding'; + } + + protected static function getCachedBindingData() + { + if (static::shouldUseDifferentDataForBinding()) { + return Cacheable::cached( + static::getBindingCacheKey(), + static::getBindingData(), + static::getBindingCacheTTL() + ); + } + + return static::getCached(); + } + + protected static function shouldUseDifferentDataForBinding(): bool + { + return false; + } + + protected static function getBindingData() + { + return null; + } + + protected static function getBindingCacheTTL(): null|int|DateInterval|DateTimeInterface + { + return null; + } + + protected static function shouldUseAlternativeRouteBinding(): bool + { + return false; + } + + protected static function alternativeRouteBinding($value, $field = null) + { + return parent::resolveRouteBinding($value, $field); + } +} diff --git a/src/Skeleton.php b/src/Skeleton.php deleted file mode 100755 index 34c7194..0000000 --- a/src/Skeleton.php +++ /dev/null @@ -1,5 +0,0 @@ -tobeNull(); + + $cache = Cacheable::set($cacheKey, $data); + $cache = Cacheable::get($cacheKey); + expect($cache)->toEqual($data); +}); + +it('uses cached method correctly', function () { + $cacheKey = 'data'; + $data = 'welcome'; + + Cacheable::cached($cacheKey, $data); + $cache = Cacheable::get($cacheKey); + expect($cache)->toEqual($data); +}); diff --git a/tests/Feature/Collection/CacheableCollectionTest.php b/tests/Feature/Collection/CacheableCollectionTest.php new file mode 100644 index 0000000..0f223a0 --- /dev/null +++ b/tests/Feature/Collection/CacheableCollectionTest.php @@ -0,0 +1,17 @@ +toBeNull(); + + $collection = Collection::make([1, 2, 3]); + $collection->cache($cacheKey); + + $cache = Cacheable::get($cacheKey); + expect($cache)->toEqual($collection); +}); diff --git a/tests/Pest.php b/tests/Pest.php index 7fe1500..35e0f94 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -1,5 +1,5 @@ in(__DIR__); diff --git a/tests/TestCase.php b/tests/TestCase.php index d04fb0c..426ad63 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -1,36 +1,26 @@ 'VendorName\\Skeleton\\Database\\Factories\\'.class_basename($modelName).'Factory' - ); } protected function getPackageProviders($app) { return [ - SkeletonServiceProvider::class, + CacheableServiceProvider::class, ]; } public function getEnvironmentSetUp($app) { - config()->set('database.default', 'testing'); - - /* - $migration = include __DIR__.'/../database/migrations/create_skeleton_table.php.stub'; - $migration->up(); - */ + config()->set('cache.default', 'array'); } }