generated from tomdavies/craft-plugin-template
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
Browse the repository at this point in the history
* added: stash functionality [#23] * fixed: docs typo
- Loading branch information
Showing
11 changed files
with
561 additions
and
22 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -15,3 +15,5 @@ node_modules | |
|
||
docs/.vitepress/dist/ | ||
docs/.vitepress/cache/ | ||
|
||
tests/storage/logs/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,146 @@ | ||
# The Stash | ||
|
||
Stash provides a simple and efficient way to store and retrieve data in memory over the course of a request. It's useful for storing data that needs to be accessed multiple times in a single request, without having to re-fetch it each time from the DB, or drill it down through props / params passed to includes or other component patterns. | ||
|
||
## Using the Stash | ||
|
||
The Stash is accessible either via the global `stash` object in Twig, or via the `craft.toolbelt.stash()` template variable. Both are references to the same service, and provide the same API. | ||
|
||
### Quick API reference | ||
|
||
```twig | ||
{{ stash.set('key.optionaly.in.dot.notation', value) }} | ||
{{ stash.get('key.optionaly.in.dot.notation', optionalDefaultValue) }} | ||
{{ stash.push('key.optionaly.in.dot.notation', value2) }} | ||
{{ stash.pop('key.optionaly.in.dot.notation') }} | ||
{{ stash.has('key.optionaly.in.dot.notation') }} | ||
{{ stash.drop('key.optionaly.in.dot.notation') }} | ||
{{ stash.clear() }} | ||
{{ stash.getAll() }} | ||
{{ stash.getKeys() }} | ||
{{ stash.getValues() }} | ||
{{ stash.getCount() }} | ||
``` | ||
|
||
See below for more detailed information on each method. | ||
|
||
|
||
## Stashing and retrieving values | ||
|
||
Values can be added to the stash using the `set()` method and retrieved using `get()`. The value can be any valid PHP value, though it's safest to stick to primitive values and instances / collections of models. Stashing something like a singleton service instance is probably a bad idea and may lead to unexpected effects. It also really shouldn't ever be necessary. | ||
|
||
```twig | ||
{{ stash.set('areApplesNice', true) }} | ||
{# then later, in some other template... #} | ||
{{ dump(stash.get('areApplesNice')) }} | ||
{# `true` #} | ||
``` | ||
|
||
### Default values | ||
|
||
The `get()` method also accepts an optional second argument, which will be returned if the key does not exist in the stash. This can be useful for providing a default value for a key that may or may not be set. | ||
|
||
```twig | ||
{{ dump(stash.get('keyThatDoesntExist', 'default value')) }} | ||
{# `default value` #} | ||
``` | ||
|
||
|
||
## Pushing and popping values | ||
|
||
The stash also supports a stack-like interface, allowing you to `push()` and `pop()` values onto and off of a named stack. This can be useful for storing and retrieving values in collection that you will want to manipulate or iterate over later. | ||
|
||
### Push / pop example: | ||
|
||
```twig | ||
{{ stash.push('myStack', 'first') }} | ||
{{ stash.push('myStack', 'second') }} | ||
{{ stash.push('myStack', 'third') }} | ||
{{ dump(stash.pop('myStack')) }} | ||
{# `third` #} | ||
{{ dump(stash.pop('myStack')) }} | ||
{# `second` #} | ||
{{ dump(stash.pop('myStack')) }} | ||
{# `first` #} | ||
``` | ||
Stacks are created on demand, so you don't need to worry about creating them before you use them. If you `push()` to a stack that doesn't exist, it will be created for you using a Laravel Collection. If you initialise a stack with an empty array using `set()` first, that array will be used as the initial/return value. Attempting to `push()` or `pop()` to a previously initialized non-array or non-Collection value will throw an exception. | ||
|
||
### Push-then-iterate example | ||
|
||
```twig | ||
{{ stash.push('myStack', { name: 'Phil', hasGoodHair: false }) }} | ||
{{ stash.push('myStack', { name: 'Patrick', goodHair: true }) }} | ||
{{ stash.push('myStack', { name: 'Tom', hasGoodHair: false }) }} | ||
{# stash.get('myStack') is automatically created as a collection, so we can use collection methods on it, as well as iterate over it #} | ||
{% for person in stash.get('myStack').filter((person) => person.hasGoodHair == false) %} | ||
{{ person.name }} | ||
{% endfor %} | ||
{# `Phil` `Tom` #} | ||
``` | ||
|
||
|
||
|
||
## Deleting values and clearing the stash | ||
|
||
Values can be removed from the stash using the `drop()` method, which will remove the value at the specified key. The `clear()` method will remove all values from the stash. | ||
|
||
```twig | ||
{{ stash.drop('key') }} | ||
{{ stash.clear() }} | ||
``` | ||
|
||
|
||
## Checking for the existence of a value | ||
|
||
You can check if a value exists in the stash using the `has()` method. This will return `true` if the value exists, and `false` if it does not. | ||
|
||
```twig | ||
{{ stash.has('key') }} | ||
``` | ||
|
||
## Getting everything, all keys, values, and the count | ||
|
||
You can get a shallow array copy of the current stash state, of all keys, or of all values, or the count of values in the stash using the `getAll()`, `getKeys()`, `getValues()`, and `getCount()` methods respectively: | ||
|
||
```twig | ||
{{ stash.getAll() }} / {{ stash.all }} - returns a shallow copy of the current stash state | ||
{{ stash.getKeys() }} / {{ stash.keys }} - returns an array of all keys in the stash | ||
{{ stash.getValues() }} / {{ stash.values }} - returns an array of all values in the stash | ||
{{ stash.getCount() }} / {{ stash.count }} - returns the number of values in the stash | ||
``` | ||
|
||
|
||
## Dot notation | ||
|
||
The following methods all support dot notation for keys: | ||
|
||
- `set()` | ||
- `get()` | ||
- `push()` | ||
- `drop()` | ||
- `has()` | ||
|
||
This allows you to store and retrieve nested values in the stash. For example: | ||
|
||
```twig | ||
{{ stash.set('key.optionaly.in.dot.notation', value) }} | ||
{{ stash.get('key.optionaly.in.dot.notation') }} | ||
``` | ||
|
||
Setting a deeply nested value will create the necessary intermediate arrays or collections as needed. Attempting to `get()` a deeply nested value that doesn't exist will return `null`. Setting a deeply nested value using an intermediate key that is not an array or collection will throw an exception. | ||
|
||
## Caveats, limitations and internals | ||
|
||
The stash is deliberately simple by design. It is in-memory only, so it's not suitable for storing data that needs to persist between requests. It's also not shared between requests, so you can't use it to store data that needs to be shared between different requests. | ||
|
||
The stash works because Craft/Yii modules and their component services are singletons, and thus are shared for a single request. It's not a replacement for proper query caching or other more robust caching strategies, but a compliment to them to be used sparingly. It may electrocute your dog if you try to use it for something it's not designed for. | ||
|
||
Internally the Stash uses Laravel Collections by default, along with Laravel's robust `Illuminate\Support\Arr` helper functions to support features like dot notation. | ||
|
||
## Tests | ||
|
||
Are written using [Pest](https://pestphp.com/) and are located in `tests/Unit/StashTest.php`. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,161 @@ | ||
<?php | ||
|
||
namespace zaengle\Toolbelt\Services; | ||
|
||
use craft\base\Component; | ||
use Illuminate\Support\Arr; | ||
use Illuminate\Support\Collection; | ||
use TypeError; | ||
|
||
/** | ||
* Stash service | ||
* | ||
* @property-read int $count | ||
* @property-read array $keys | ||
* @property-read array $values | ||
* @property-read array $all | ||
*/ | ||
class StashService extends Component | ||
{ | ||
/** | ||
* @var Collection<string, mixed> | ||
*/ | ||
protected Collection $data; | ||
|
||
public function init(): void | ||
{ | ||
$this->data = new Collection([]); | ||
parent::init(); | ||
} | ||
public function set(string $key, mixed $value): void | ||
{ | ||
// non dot notation | ||
if (!self::isDotNotationKey($key)) { | ||
$this->data->put($key, $value); | ||
return; | ||
} | ||
// unset any existing value | ||
if ($this->has($key)) { | ||
$this->drop($key); | ||
} | ||
// set the new value | ||
$update = collect([$key => $value])->undot(); | ||
$this->data = $this->data->mergeRecursive($update); | ||
} | ||
|
||
/** | ||
* Supports dot notation | ||
*/ | ||
public function get(string $key, mixed $default = null): mixed | ||
{ | ||
return Arr::get($this->data, $key) ?? $default; | ||
} | ||
|
||
public function push(string $key, mixed $value): void | ||
{ | ||
$existing = $this->get($key) ?? collect(); | ||
|
||
if ($existing instanceof Collection) { | ||
$existing->push($value); | ||
} elseif (is_array($existing)) { | ||
$existing[] = $value; | ||
} else { | ||
throw new TypeError("Cannot push value, as existing stash for $key is not a Collection or array"); | ||
} | ||
|
||
$this->set($key, $existing); | ||
} | ||
public function pop(string $key): mixed | ||
{ | ||
$existing = $this->get($key); | ||
|
||
if ($existing instanceof Collection) { | ||
$value = $existing->pop(); | ||
|
||
$this->set($key, $existing); | ||
|
||
return $value; | ||
} | ||
|
||
if (is_array($existing)) { | ||
$value = array_pop($existing); | ||
|
||
$this->set($key, $existing); | ||
|
||
return $value; | ||
} | ||
|
||
throw new TypeError("Cannot pop value, as existing stash for $key is not a Collection or array"); | ||
} | ||
|
||
/** | ||
* Supports dot notation | ||
*/ | ||
public function has(string $key): bool | ||
{ | ||
return Arr::has($this->data, $key); | ||
} | ||
/** | ||
* Supports dot notation | ||
*/ | ||
public function drop(string $key): void | ||
{ | ||
if (!$this->has($key)) { | ||
return; | ||
} | ||
// non dot notation | ||
if (!self::isDotNotationKey($key)) { | ||
$this->data->forget($key); | ||
return; | ||
} | ||
// update the parent stash | ||
$segments = collect(explode('.', $key)); | ||
$dropKey = $segments->last(); | ||
$parentPath = $segments->slice(0, -1)->implode('.'); | ||
|
||
$parent = $this->get($parentPath); | ||
|
||
if ($parent instanceof Collection) { | ||
$parent->forget($dropKey); | ||
} elseif (is_array($parent)) { | ||
unset($parent[$dropKey]); | ||
} else { | ||
throw new TypeError("Cannot drop value, as parent stash for $key is not a Collection or array"); | ||
} | ||
$this->set($parentPath, $parent); | ||
} | ||
public function clear(): void | ||
{ | ||
$this->data = collect(); | ||
} | ||
/** | ||
* @return array<string, mixed> | ||
*/ | ||
public function getAll(): array | ||
{ | ||
return $this->data->toArray(); | ||
} | ||
/** | ||
* @return array<string> | ||
*/ | ||
public function getKeys(): array | ||
{ | ||
return $this->data->keys()->toArray(); | ||
} | ||
/** | ||
* @return array | ||
*/ | ||
public function getValues(): array | ||
{ | ||
return $this->data->values()->toArray(); | ||
} | ||
public function getCount(): int | ||
{ | ||
return $this->data->count(); | ||
} | ||
|
||
public static function isDotNotationKey(string $key): bool | ||
{ | ||
return str_contains($key, '.'); | ||
} | ||
} |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.