Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 19 additions & 19 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
name: Build

on:
pull_request:
types: [closed]
branches: [main]
pull_request:
types: [closed]
branches: [main]

jobs:
build:
if: github.event.pull_request.merged == true
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build install archive
run: |
chmod +x create_install_archive.sh
ARCHIVE=$(./create_install_archive.sh)
test -f "$ARCHIVE"
echo "ARCHIVE=$ARCHIVE" >> "$GITHUB_ENV"
- uses: actions/upload-artifact@v4
with:
name: install
path: ${{ env.ARCHIVE }}
if-no-files-found: error
build:
if: github.event.pull_request.merged == true
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build install archive
run: |
chmod +x create_install_archive.sh
ARCHIVE=$(./create_install_archive.sh)
test -f "$ARCHIVE"
echo "ARCHIVE=$ARCHIVE" >> "$GITHUB_ENV"
- uses: actions/upload-artifact@v4
with:
name: install
path: ${{ env.ARCHIVE }}
if-no-files-found: error
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -531,6 +531,7 @@ upload/devcraft/.composer/*
install_*.zip

.phpunit.result.cache/
.phpunit.result.cache
.phpunit.cache/
.phpunit/
*.code-workspace
Expand Down
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## 1.1.0

### Added

- PSR-6 file cache: `Devcraft\Cache\FileCachePool`, `CacheItem`, `InvalidArgument`
- `FileCachePool::clearNamespace()` extension for prefix clears (`Translation/…`)
- Dependency on `psr/cache` `^3.0`

## 1.0.1

### Added
Expand Down
21 changes: 18 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Reusable PHP 8.3 attributes and runtime helpers.

See [the documentation](https://readme.devcraft.club/dev/dev-tools/1.0.1/getting_started) for the full guide.
See [the documentation](https://readme.devcraft.club/dev/dev-tools/1.1.0/getting_started) for the full guide.

## Local installation

Expand All @@ -18,7 +18,7 @@ Add a Composer path repository in the consuming project and require the package:
}
],
"require": {
"devcraftclub/dev-tools": "@dev"
"devcraftclub/dev-tools": "^1.1"
}
}
```
Expand All @@ -27,7 +27,22 @@ Path repositories must be configured by the consuming root project; Composer
does not inherit them transitively. Publish this package later, or replace the
path repository with a normal repository and version.

The package depends on [`marcin-orlowski/lombok-php`](https://github.com/MarcinOrlowski/lombok-php) `^1.2`.
The package depends on [`marcin-orlowski/lombok-php`](https://github.com/MarcinOrlowski/lombok-php) `^1.2` and [`psr/cache`](https://www.php-fig.org/psr/psr-6/) `^3`.

## PSR-6 file cache

```php
use Devcraft\Cache\FileCachePool;

$pool = new FileCachePool('/path/to/cache', defaultTtlSeconds: 3600);
$item = $pool->getItem('Translation/dict');
$item->set(['hello' => 'world']);
$pool->save($item);

$pool->clearNamespace('Translation'); // DevTools extension (not in PSR-6)
```

Keys may contain `/` as a namespace separator (stored as subdirectories). Other PSR-6 reserved characters remain forbidden. See [docs/cache.md](docs/cache.md).

## Fluent properties

Expand Down
7 changes: 5 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
{
"name": "devcraftclub/dev-tools",
"description": "Reusable PHP attributes and validation utilities",
"version": "1.0.1",
"type": "library",
"version": "1.1.0",
"homepage": "https://github.com/DevCraftClub/DevTools",
"support": {
"email": "dev@devcraft.club",
"issues": "https://github.com/DevCraftClub/DevTools/issues",
"forum": "https://devcraft.club",
"docs": "https://readme.devcraft.club/dev/dev-tools/1.0.1/en/getting_started"
"docs": "https://readme.devcraft.club/dev/dev-tools/1.1.0/en/getting_started"
},
"require": {
"php": ">=8.3",
"psr/cache": "^3.0",
"psr/log": "^3.0",
"analog/analog": "^1.0",
"marcin-orlowski/lombok-php": "^1.2"
Expand All @@ -23,6 +25,7 @@
"psr-4": {
"Devcraft\\Abstracts\\": "src/Abstracts/",
"Devcraft\\Attributes\\": "src/Attributes/",
"Devcraft\\Cache\\": "src/Cache/",
"Devcraft\\Exceptions\\": "src/Exceptions/",
"Devcraft\\Interfaces\\": "src/Interfaces/",
"Devcraft\\Mapper\\": "src/Mapper/",
Expand Down
35 changes: 35 additions & 0 deletions docs/cache.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# PSR-6 file cache

`Devcraft\Cache\FileCachePool` implements `Psr\Cache\CacheItemPoolInterface`.

## Basics

| Class | Role |
|-------|------|
| `FileCachePool` | Filesystem pool |
| `CacheItem` | Cache item |
| `InvalidArgument` | Invalid key / argument |

Constructor: `new FileCachePool(string $baseDir, ?int $defaultTtlSeconds = null)`.

- `null` or `0` default TTL → items without explicit expiry do not expire.
- Positive TTL → applied when the item still uses the default expiry (`expiresAfter` / `expiresAt` not called).

## Keys and storage

- Allowed: letters, digits, and `/` as a **namespace** separator (extension beyond strict PSR-6 reserved list).
- Forbidden: `{}()\@:` and `..`.
- File path: `{baseDir}/{key}.cache` (`/` → subdirectory).
- Envelope JSON: `{ "e": expiryUnix|null, "f": "j"|"s", "v": value }`.

## Extension: `clearNamespace`

```php
$pool->clearNamespace('Translation'); // deletes Translation/* keys
```

Not part of PSR-6; provided for DevCraft-style typed caches.

## DevCraft Admin

`DevCraft\Core\Cache\CacheControl` is a thin facade: `setCache($type, $name, $data)` maps to key `{type}/{name}` and delegates to `FileCachePool`. Prefer `CacheControl::pool()` for new code.
7 changes: 7 additions & 0 deletions renovate.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["config:recommended"],
"enabled": true,
"automerge": true,
"enabledManagers": ["composer"]
}
133 changes: 133 additions & 0 deletions src/Cache/CacheItem.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
<?php

declare(strict_types=1);

namespace Devcraft\Cache;

use DateInterval;
use DateTimeImmutable;
use DateTimeInterface;
use Psr\Cache\CacheItemInterface;

/**
* PSR-6 cache item for {@see FileCachePool}.
*/
final class CacheItem implements CacheItemInterface {

private mixed $value = NULL;

private bool $hit = false;

private ?int $expiry = NULL;

private bool $isDefaultExpiry = true;

public function __construct(
private readonly string $key,
) {}

public function getKey(): string {
return $this->key;
}

public function get(): mixed {
return $this->isHit()? $this->value : NULL;
}

public function isHit(): bool {
return $this->hit;
}

public function set(mixed $value): static {
$this->value = $value;

return $this;
}

public function expiresAt(?DateTimeInterface $expiration): static {
$this->isDefaultExpiry = false;

if($expiration === NULL) {
$this->expiry = NULL;

return $this;
}

$this->expiry = $expiration->getTimestamp();

return $this;
}

public function expiresAfter(int|DateInterval|null $time): static {
$this->isDefaultExpiry = false;

if($time === NULL) {
$this->expiry = NULL;

return $this;
}

if($time instanceof DateInterval) {
$this->expiry = (new DateTimeImmutable())->add($time)->getTimestamp();

return $this;
}

if($time <= 0) {
$this->expiry = NULL;

return $this;
}

$this->expiry = time() + $time;

return $this;
}

/**
* @internal
*/
public function markHit(mixed $value, ?int $expiry): void {
$this->hit = true;
$this->value = $value;
$this->expiry = $expiry;
$this->isDefaultExpiry = false;
}

/**
* @internal
*/
public function markMiss(): void {
$this->hit = false;
$this->value = NULL;
}

/**
* @internal
*/
public function rawValue(): mixed {
return $this->value;
}

/**
* @internal
*/
public function expiryUnix(): ?int {
return $this->expiry;
}

/**
* @internal
*/
public function usesDefaultExpiry(): bool {
return $this->isDefaultExpiry;
}

/**
* @internal
*/
public function isExpired(): bool {
return $this->expiry !== NULL && $this->expiry <= time();
}

}
Loading
Loading