diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml
index eda55d0..b2d1f4a 100644
--- a/.github/workflows/phpunit.yml
+++ b/.github/workflows/phpunit.yml
@@ -1,47 +1,90 @@
-name : PHPUnit Test
+name: CI
-on :
- push :
- branches: [ "main" ]
+on:
+ push:
+ branches: ["main"]
pull_request:
- branches: [ "main" ]
+ branches: ["main"]
permissions:
contents: read
-jobs :
- test:
+jobs:
+ lint:
+ name: Code Style
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+ - name: Set up PHP
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: "8.4"
+
+ - name: Install dependencies
+ run: composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist
+
+ - name: Run Laravel Pint
+ run: composer lint:test
+
+ analyse:
+ name: Static Analysis
runs-on: ubuntu-latest
- steps :
- -
- name: Set up PHP
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Set up PHP
uses: shivammathur/setup-php@v2
with:
- php-version: '8.4'
+ php-version: "8.4"
- -
+ - name: Install dependencies
+ run: composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist
+
+ - name: Run PHPStan
+ run: composer analyse
+
+ test:
+ name: PHPUnit Tests
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Set up PHP
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: "8.4"
+ coverage: pcov
+
+ - name: Checkout code
uses: actions/checkout@v4
- -
- name: Validate composer.json and composer.lock
- run : composer validate --strict
+ - name: Validate composer.json and composer.lock
+ run: composer validate --strict
- -
- name: Cache Composer packages
- id : composer-cache
+ - name: Cache Composer packages
+ id: composer-cache
uses: actions/cache@v4
with:
- path : vendor
- key : ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }}
+ path: vendor
+ key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }}
restore-keys: |
${{ runner.os }}-php-
- -
- name: Install dependencies
- run : composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist
+ - name: Install dependencies
+ run: composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist
- -
- name: Run test suite
- run : composer run-script test
+ - name: Run test suite with coverage
+ run: vendor/bin/phpunit tests/ProgressableTest.php --testdox --coverage-text --coverage-clover=coverage.xml
+
+ - name: Upload coverage to Codecov
+ uses: codecov/codecov-action@v4
+ with:
+ files: coverage.xml
+ fail_ci_if_error: false
+ verbose: true
+ env:
+ CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
diff --git a/.gitignore b/.gitignore
index de514fb..36c622e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,6 +4,7 @@ vendor/
.vscode/
node_modules/
.phpunit.result.cache
+.phpunit.cache/
*.log
.DS_Store
ai.txt
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..b658e05
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,71 @@
+# Changelog
+
+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).
+
+## [2.0.0] - 2025-01-XX
+
+### Added
+- **Metadata Support**: Store additional data with progress
+ - `setMetadata(array $metadata)` - Set metadata array
+ - `getMetadata()` - Get all metadata
+ - `addMetadata(string $key, mixed $value)` - Add single metadata value
+ - `getMetadataValue(string $key, mixed $default)` - Get single metadata value
+- **Status Messages**: Attach status messages to progress
+ - `setStatusMessage(?string $message)` - Set status message
+ - `getStatusMessage()` - Get status message
+- **Event Callbacks**: React to progress changes
+ - `onProgressChange(callable $callback)` - Called on progress change with `(float $new, float $old, static $instance)`
+ - `onComplete(callable $callback)` - Called when progress reaches 100% with `(static $instance)`
+- **Progress Helpers**:
+ - `incrementLocalProgress(float $amount = 1)` - Increment/decrement progress
+ - `isComplete()` - Check if local progress is 100%
+ - `isOverallComplete()` - Check if overall progress is 100%
+ - `removeLocalFromOverall()` - Remove instance from overall calculation
+- **Precision Configuration**:
+ - `setPrecision(int $precision)` - Set decimal precision
+ - `getPrecision()` - Get current precision
+ - Configurable default precision via `config/progressable.php`
+- **CI/CD Improvements**:
+ - PHPStan static analysis (level 5) with Larastan
+ - Laravel Pint code style enforcement
+ - Code coverage with pcov and Codecov integration
+ - Parallel CI jobs for lint, analyse, and test
+- **Documentation**:
+ - CHANGELOG.md with full history
+ - CONTRIBUTING.md with guidelines
+ - Comprehensive README with API tables
+ - Codecov badge in README
+
+### Changed
+- `getLocalProgress()` now accepts `?int` (null uses configured default precision)
+- `getOverallProgress()` now accepts `?int` (null uses configured default precision)
+- Progress data now stores metadata and messages alongside progress value
+- Improved test suite with 35 tests and 70+ assertions
+
+### Fixed
+- README.md incorrect method names (`updateLocalProgress` -> `setLocalProgress`)
+- README.md incorrect imports (`use Verseles\Progressable` -> `use Verseles\Progressable\Progressable`)
+- Config comment referencing wrong class name (`FullProgress` -> `Progressable`)
+- Added missing return type to `getOverallUniqueName()`
+- Optimized `setLocalKey()` to avoid duplicate storage calls
+
+## [1.0.0] - Previous Release
+
+### Added
+- Initial release with core progress tracking functionality
+- `setOverallUniqueName()` - Set progress group identifier
+- `setLocalProgress()` - Update instance progress
+- `getLocalProgress()` - Get instance progress
+- `getOverallProgress()` - Get average progress of all instances
+- `resetLocalProgress()` - Reset instance to 0
+- `resetOverallProgress()` - Clear all progress data
+- `setCustomSaveData()` / `setCustomGetData()` - Custom storage callbacks
+- `setTTL()` / `getTTL()` - Cache TTL configuration
+- `setPrefixStorageKey()` - Custom cache key prefix
+- `setLocalKey()` / `getLocalKey()` - Custom instance identifiers
+- Laravel service provider with auto-discovery
+- Support for Laravel 11 and 12
+- PHP 8.4 requirement
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..6570b60
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,86 @@
+# Contributing to Progressable
+
+First off, thank you for considering contributing to Progressable!
+
+## Development Setup
+
+1. Clone the repository:
+```bash
+git clone https://github.com/verseles/progressable.git
+cd progressable
+```
+
+2. Install dependencies:
+```bash
+composer install
+```
+
+3. Run tests to make sure everything works:
+```bash
+composer test
+```
+
+## Code Quality Tools
+
+This project uses several tools to maintain code quality:
+
+### PHPUnit Tests
+```bash
+composer test
+```
+
+### Laravel Pint (Code Style)
+```bash
+# Check for style issues
+composer lint:test
+
+# Fix style issues automatically
+composer lint
+```
+
+### PHPStan (Static Analysis)
+```bash
+composer analyse
+```
+
+## Pull Request Process
+
+1. Fork the repository and create your branch from `main`
+2. Make your changes
+3. Ensure all tests pass: `composer test`
+4. Ensure code style is correct: `composer lint:test`
+5. Ensure static analysis passes: `composer analyse`
+6. Update documentation if needed (README.md, CHANGELOG.md)
+7. Submit your pull request
+
+## Coding Standards
+
+- Follow PSR-12 coding standards (enforced by Laravel Pint)
+- Write tests for new features
+- Keep methods focused and small
+- Use descriptive variable and method names
+- Add PHPDoc blocks for public methods
+
+## Adding New Features
+
+When adding new features:
+
+1. Add the feature implementation in `src/Progressable.php`
+2. Add tests in `tests/ProgressableTest.php`
+3. Update `README.md` with usage examples
+4. Add entry to `CHANGELOG.md` under `[Unreleased]`
+
+## Reporting Bugs
+
+When reporting bugs, please include:
+
+- PHP version
+- Laravel version (if applicable)
+- Progressable version
+- Steps to reproduce
+- Expected behavior
+- Actual behavior
+
+## Questions?
+
+Feel free to open an issue for any questions about contributing.
diff --git a/README.md b/README.md
index 503c933..03f57d7 100644
--- a/README.md
+++ b/README.md
@@ -1,8 +1,10 @@
-# Progressable 🚀
+# Progressable
A Laravel [(not only)](#without-laravel) package to track and manage progress for different tasks or processes.
-
+
+
+
## Installation
@@ -24,9 +26,9 @@ The published config file provides the following options:
```php
return [
- 'ttl' => env('PROGRESSABLE_TTL', 1140), // Default cache time-to-live (in minutes)
-
- 'prefix' => env('PROGRESSABLE_PREFIX', 'progressable'), // Cache key prefix
+ 'ttl' => env('PROGRESSABLE_TTL', 1140), // Cache TTL in minutes (default: 19 hours)
+ 'prefix' => env('PROGRESSABLE_PREFIX', 'progressable'), // Cache key prefix
+ 'precision' => env('PROGRESSABLE_PRECISION', 2), // Default decimal precision
];
```
@@ -36,13 +38,14 @@ This package provides a main trait: `Progressable`.
### With Laravel
-The `Progressable` trait can be used in any class that needs to track progress. It provides two main methods: `updateLocalProgress` and `getLocalProgress`.
+The `Progressable` trait can be used in any class that needs to track progress.
+
+"Local" refers to the progress of your class/model/etc, while "Overall" represents the average of all Progressable instances using the same unique name.
-"Local" refers to the progress of your class/model/etc, while "Overall" represents the sum of all Progressable classes using the same key name.
+### Basic Example
-### Example
```php
-use Verseles\Progressable;
+use Verseles\Progressable\Progressable;
class MyFirstTask
{
@@ -57,89 +60,153 @@ class MyFirstTask
{
foreach (range(1, 100) as $value) {
$this->setLocalProgress($value);
- usleep(100000); // Sleep for 100 milliseconds
echo "Overall Progress: " . $this->getOverallProgress() . "%" . PHP_EOL;
}
}
}
```
+### Available Methods
+
+#### Progress Management
+
+| Method | Description |
+|--------|-------------|
+| `setOverallUniqueName(string $name)` | Set the progress group identifier |
+| `setLocalProgress(float $progress)` | Set progress for this instance (0-100) |
+| `getLocalProgress(?int $precision)` | Get current progress (default precision from config) |
+| `incrementLocalProgress(float $amount = 1)` | Increment progress by amount (can be negative) |
+| `resetLocalProgress()` | Reset instance progress to 0 |
+| `getOverallProgress(?int $precision)` | Get average progress of all instances |
+| `resetOverallProgress()` | Clear all progress data for the group |
+| `removeLocalFromOverall()` | Remove this instance from overall calculation |
+
+#### Status Checks
+
+| Method | Description |
+|--------|-------------|
+| `isComplete()` | Check if local progress is 100% |
+| `isOverallComplete()` | Check if overall progress is 100% |
+
+#### Metadata & Status Messages
+
+| Method | Description |
+|--------|-------------|
+| `setStatusMessage(?string $message)` | Set a status message for this instance |
+| `getStatusMessage()` | Get the current status message |
+| `setMetadata(array $metadata)` | Set metadata array for this instance |
+| `getMetadata()` | Get all metadata |
+| `addMetadata(string $key, mixed $value)` | Add/update a single metadata value |
+| `getMetadataValue(string $key, mixed $default)` | Get a single metadata value |
+
+#### Event Callbacks
+
+| Method | Description |
+|--------|-------------|
+| `onProgressChange(callable $callback)` | Called when progress changes: `fn($new, $old, $instance)` |
+| `onComplete(callable $callback)` | Called when progress reaches 100%: `fn($instance)` |
+
+#### Configuration
+
+| Method | Description |
+|--------|-------------|
+| `setTTL(int $minutes)` | Set cache time-to-live |
+| `getTTL()` | Get current TTL |
+| `setPrecision(int $decimals)` | Set decimal precision for progress values |
+| `getPrecision()` | Get current precision |
+| `setPrefixStorageKey(string $prefix)` | Set custom cache key prefix (before setting unique name) |
+| `setLocalKey(string $key)` | Set custom identifier for this instance |
+| `getLocalKey()` | Get current instance identifier |
+
+#### Custom Storage (for non-Laravel usage)
+
+| Method | Description |
+|--------|-------------|
+| `setCustomSaveData(callable $callback)` | Set custom save callback |
+| `setCustomGetData(callable $callback)` | Set custom get callback |
+
+### Example with Callbacks and Metadata
+
```php
-use Verseles\Progressable;
+use Verseles\Progressable\Progressable;
-class MySecondTask
+class FileProcessor
{
use Progressable;
- public function __construct()
+ public function process(array $files)
{
- $this->setOverallUniqueName('my-job');
- }
+ $this->setOverallUniqueName('file-processing')
+ ->resetOverallProgress()
+ ->onProgressChange(fn($new, $old) => logger("Progress: {$old}% -> {$new}%"))
+ ->onComplete(fn() => logger("All files processed!"));
- public function run()
- {
- foreach (range(1, 100) as $value) {
- $this->setLocalProgress($value);
- usleep(100000); // Sleep for 100 milliseconds
- echo "Overall Progress: " . $this->getOverallProgress() . "%" . PHP_EOL;
+ $increment = 100 / count($files);
+
+ foreach ($files as $index => $file) {
+ $this->setStatusMessage("Processing: {$file}")
+ ->addMetadata('current_file', $file)
+ ->addMetadata('files_processed', $index + 1);
+
+ $this->processFile($file);
+ $this->incrementLocalProgress($increment);
}
}
}
```
-- Use `setOverallUniqueName` to associate the progress with a specific overall progress instance.
-- `setLocalProgress` updates the progress for the current instance.
-- `getLocalProgress` retrieves the current progress.
-- `getOverallProgress` retrieves the overall progress data.
-- `resetOverallProgress` resets the overall progress (recommended after setting the unique name for the first time).
-
-The progress value ranges from 0 to 100.
-
### Without Laravel
-You can use the `Progressable` trait without Laravel by providing custom save and get data methods.
-
-### Example
+You can use the `Progressable` trait without Laravel by providing custom save and get data callbacks:
```php
-$overallUniqueName = 'test-without-laravel';
+use Verseles\Progressable\Progressable;
-$my_super_storage = [];
+$storage = [];
-$saveCallback = function ($key, $data, $ttl) use (&$my_super_storage) {
- $my_super_storage[$key] = $data;
+$saveCallback = function ($key, $data, $ttl) use (&$storage) {
+ $storage[$key] = $data;
};
-$getCallback = function ($key) use (&$my_super_storage) {
- return $my_super_storage[$key] ?? [];
+$getCallback = function ($key) use (&$storage) {
+ return $storage[$key] ?? [];
};
+$obj = new class { use Progressable; };
+$obj
+ ->setCustomSaveData($saveCallback)
+ ->setCustomGetData($getCallback)
+ ->setOverallUniqueName('my-task')
+ ->resetOverallProgress()
+ ->setLocalProgress(25);
-$obj1 = new class { use Progressable; };
-$obj1
- ->setCustomSaveData($saveCallback)
- ->setCustomGetData($getCallback)
- ->setOverallUniqueName($overallUniqueName)
- ->resetOverallProgress()
- ->updateLocalProgress(25);
-
-$obj2 = new class { use Progressable; };
-$obj2
- ->setCustomSaveData($saveCallback)
- ->setCustomGetData($getCallback)
- ->setOverallUniqueName($overallUniqueName)
- ->updateLocalProgress(75);
-
+echo $obj->getLocalProgress(); // 25
```
## Testing
-To run the tests, execute the following command:
-
```bash
-make
+# Run tests
+composer test
+
+# Check code style
+composer lint:test
+
+# Fix code style
+composer lint
+
+# Run static analysis
+composer analyse
```
+## Contributing
+
+Please see [CONTRIBUTING.md](CONTRIBUTING.md) for details.
+
+## Changelog
+
+Please see [CHANGELOG.md](CHANGELOG.md) for recent changes.
+
## License
The Progressable package is open-sourced software licensed under the [MIT license](./LICENSE.md).
diff --git a/composer.json b/composer.json
index ee011eb..c68e173 100644
--- a/composer.json
+++ b/composer.json
@@ -33,7 +33,10 @@
}
},
"scripts": {
- "test": "vendor/bin/phpunit tests/ProgressableTest.php --testdox"
+ "test": "vendor/bin/phpunit tests/ProgressableTest.php --testdox",
+ "lint": "vendor/bin/pint",
+ "lint:test": "vendor/bin/pint --test",
+ "analyse": "vendor/bin/phpstan analyse"
},
"config": {
"sort-packages": true
@@ -41,7 +44,10 @@
"minimum-stability": "dev",
"prefer-stable": true,
"require-dev": {
+ "larastan/larastan": "^3.8",
+ "laravel/pint": "^1.25",
"orchestra/testbench": "^9.0",
+ "phpstan/phpstan": "^2.1",
"phpunit/phpunit": "^12"
}
}
diff --git a/composer.lock b/composer.lock
index bf16940..dafd128 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "d1b660271758a8c2f3df35c8c557ef2c",
+ "content-hash": "3db87b53cfbba85a71a5233d3918555b",
"packages": [
{
"name": "brick/math",
@@ -5844,6 +5844,137 @@
},
"time": "2025-04-30T06:54:44+00:00"
},
+ {
+ "name": "iamcal/sql-parser",
+ "version": "v0.6",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/iamcal/SQLParser.git",
+ "reference": "947083e2dca211a6f12fb1beb67a01e387de9b62"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/iamcal/SQLParser/zipball/947083e2dca211a6f12fb1beb67a01e387de9b62",
+ "reference": "947083e2dca211a6f12fb1beb67a01e387de9b62",
+ "shasum": ""
+ },
+ "require-dev": {
+ "php-coveralls/php-coveralls": "^1.0",
+ "phpunit/phpunit": "^5|^6|^7|^8|^9"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "iamcal\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Cal Henderson",
+ "email": "cal@iamcal.com"
+ }
+ ],
+ "description": "MySQL schema parser",
+ "support": {
+ "issues": "https://github.com/iamcal/SQLParser/issues",
+ "source": "https://github.com/iamcal/SQLParser/tree/v0.6"
+ },
+ "time": "2025-03-17T16:59:46+00:00"
+ },
+ {
+ "name": "larastan/larastan",
+ "version": "v3.8.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/larastan/larastan.git",
+ "reference": "d13ef96d652d1b2a8f34f1760ba6bf5b9c98112e"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/larastan/larastan/zipball/d13ef96d652d1b2a8f34f1760ba6bf5b9c98112e",
+ "reference": "d13ef96d652d1b2a8f34f1760ba6bf5b9c98112e",
+ "shasum": ""
+ },
+ "require": {
+ "ext-json": "*",
+ "iamcal/sql-parser": "^0.6.0",
+ "illuminate/console": "^11.44.2 || ^12.4.1",
+ "illuminate/container": "^11.44.2 || ^12.4.1",
+ "illuminate/contracts": "^11.44.2 || ^12.4.1",
+ "illuminate/database": "^11.44.2 || ^12.4.1",
+ "illuminate/http": "^11.44.2 || ^12.4.1",
+ "illuminate/pipeline": "^11.44.2 || ^12.4.1",
+ "illuminate/support": "^11.44.2 || ^12.4.1",
+ "php": "^8.2",
+ "phpstan/phpstan": "^2.1.29"
+ },
+ "require-dev": {
+ "doctrine/coding-standard": "^13",
+ "laravel/framework": "^11.44.2 || ^12.7.2",
+ "mockery/mockery": "^1.6.12",
+ "nikic/php-parser": "^5.4",
+ "orchestra/canvas": "^v9.2.2 || ^10.0.1",
+ "orchestra/testbench-core": "^9.12.0 || ^10.1",
+ "phpstan/phpstan-deprecation-rules": "^2.0.1",
+ "phpunit/phpunit": "^10.5.35 || ^11.5.15"
+ },
+ "suggest": {
+ "orchestra/testbench": "Using Larastan for analysing a package needs Testbench",
+ "phpmyadmin/sql-parser": "Install to enable Larastan's optional phpMyAdmin-based SQL parser automatically"
+ },
+ "type": "phpstan-extension",
+ "extra": {
+ "phpstan": {
+ "includes": [
+ "extension.neon"
+ ]
+ },
+ "branch-alias": {
+ "dev-master": "3.0-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Larastan\\Larastan\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Can Vural",
+ "email": "can9119@gmail.com"
+ }
+ ],
+ "description": "Larastan - Discover bugs in your code without running it. A phpstan/phpstan extension for Laravel",
+ "keywords": [
+ "PHPStan",
+ "code analyse",
+ "code analysis",
+ "larastan",
+ "laravel",
+ "package",
+ "php",
+ "static analysis"
+ ],
+ "support": {
+ "issues": "https://github.com/larastan/larastan/issues",
+ "source": "https://github.com/larastan/larastan/tree/v3.8.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/canvural",
+ "type": "github"
+ }
+ ],
+ "time": "2025-10-27T23:09:14+00:00"
+ },
{
"name": "laravel/pail",
"version": "v1.2.2",
@@ -5922,6 +6053,72 @@
},
"time": "2025-01-28T15:15:15+00:00"
},
+ {
+ "name": "laravel/pint",
+ "version": "v1.25.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/laravel/pint.git",
+ "reference": "5016e263f95d97670d71b9a987bd8996ade6d8d9"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/laravel/pint/zipball/5016e263f95d97670d71b9a987bd8996ade6d8d9",
+ "reference": "5016e263f95d97670d71b9a987bd8996ade6d8d9",
+ "shasum": ""
+ },
+ "require": {
+ "ext-json": "*",
+ "ext-mbstring": "*",
+ "ext-tokenizer": "*",
+ "ext-xml": "*",
+ "php": "^8.2.0"
+ },
+ "require-dev": {
+ "friendsofphp/php-cs-fixer": "^3.87.2",
+ "illuminate/view": "^11.46.0",
+ "larastan/larastan": "^3.7.1",
+ "laravel-zero/framework": "^11.45.0",
+ "mockery/mockery": "^1.6.12",
+ "nunomaduro/termwind": "^2.3.1",
+ "pestphp/pest": "^2.36.0"
+ },
+ "bin": [
+ "builds/pint"
+ ],
+ "type": "project",
+ "autoload": {
+ "psr-4": {
+ "App\\": "app/",
+ "Database\\Seeders\\": "database/seeders/",
+ "Database\\Factories\\": "database/factories/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nuno Maduro",
+ "email": "enunomaduro@gmail.com"
+ }
+ ],
+ "description": "An opinionated code formatter for PHP.",
+ "homepage": "https://laravel.com",
+ "keywords": [
+ "format",
+ "formatter",
+ "lint",
+ "linter",
+ "php"
+ ],
+ "support": {
+ "issues": "https://github.com/laravel/pint/issues",
+ "source": "https://github.com/laravel/pint"
+ },
+ "time": "2025-09-19T02:57:12+00:00"
+ },
{
"name": "laravel/tinker",
"version": "v2.10.1",
@@ -6820,6 +7017,59 @@
},
"time": "2022-02-21T01:04:05+00:00"
},
+ {
+ "name": "phpstan/phpstan",
+ "version": "2.1.32",
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phpstan/phpstan/zipball/e126cad1e30a99b137b8ed75a85a676450ebb227",
+ "reference": "e126cad1e30a99b137b8ed75a85a676450ebb227",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.4|^8.0"
+ },
+ "conflict": {
+ "phpstan/phpstan-shim": "*"
+ },
+ "bin": [
+ "phpstan",
+ "phpstan.phar"
+ ],
+ "type": "library",
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "PHPStan - PHP Static Analysis Tool",
+ "keywords": [
+ "dev",
+ "static analysis"
+ ],
+ "support": {
+ "docs": "https://phpstan.org/user-guide/getting-started",
+ "forum": "https://github.com/phpstan/phpstan/discussions",
+ "issues": "https://github.com/phpstan/phpstan/issues",
+ "security": "https://github.com/phpstan/phpstan/security/policy",
+ "source": "https://github.com/phpstan/phpstan-src"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/ondrejmirtes",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/phpstan",
+ "type": "github"
+ }
+ ],
+ "time": "2025-11-11T15:18:17+00:00"
+ },
{
"name": "phpunit/php-code-coverage",
"version": "12.3.0",
diff --git a/config/progressable.php b/config/progressable.php
index 9302814..9da9de5 100644
--- a/config/progressable.php
+++ b/config/progressable.php
@@ -1,27 +1,40 @@
env('PROGRESSABLE_TTL', 1140),
+ 'ttl' => env('PROGRESSABLE_TTL', 1140),
- /*
- |--------------------------------------------------------------------------
- | Cache Prefix
- |--------------------------------------------------------------------------
- |
- | This option specifies the default prefix for the progress data.
- |
- */
+ /*
+ |--------------------------------------------------------------------------
+ | Cache Prefix
+ |--------------------------------------------------------------------------
+ |
+ | This option specifies the default prefix for the progress data.
+ |
+ */
- 'prefix' => env('PROGRESSABLE_PREFIX', 'progressable'),
+ 'prefix' => env('PROGRESSABLE_PREFIX', 'progressable'),
+
+ /*
+ |--------------------------------------------------------------------------
+ | Default Precision
+ |--------------------------------------------------------------------------
+ |
+ | This option specifies the default precision (decimal places) for
+ | progress values. You can override this per-call by passing a precision
+ | parameter to getLocalProgress() or getOverallProgress().
+ |
+ */
+
+ 'precision' => env('PROGRESSABLE_PRECISION', 2),
];
diff --git a/phpstan-bootstrap.php b/phpstan-bootstrap.php
new file mode 100644
index 0000000..6efb046
--- /dev/null
+++ b/phpstan-bootstrap.php
@@ -0,0 +1,14 @@
+
+
+
+
+ tests
+
+
+
+
+
+ src
+
+
+
diff --git a/pint.json b/pint.json
new file mode 100644
index 0000000..6524fb3
--- /dev/null
+++ b/pint.json
@@ -0,0 +1,13 @@
+{
+ "preset": "laravel",
+ "rules": {
+ "simplified_null_return": true,
+ "braces_position": {
+ "functions_opening_brace": "same_line",
+ "classes_opening_brace": "same_line"
+ },
+ "align_multiline_comment": {
+ "comment_type": "phpdocs_only"
+ }
+ }
+}
diff --git a/src/Exceptions/UniqueNameAlreadySetException.php b/src/Exceptions/UniqueNameAlreadySetException.php
index a722256..81d617a 100644
--- a/src/Exceptions/UniqueNameAlreadySetException.php
+++ b/src/Exceptions/UniqueNameAlreadySetException.php
@@ -4,15 +4,13 @@
use Exception;
-class UniqueNameAlreadySetException extends Exception
-{
- /**
- * Exception constructor.
- *
- * @return void
- */
- public function __construct()
- {
- parent::__construct('The unique name has already been set.');
- }
+class UniqueNameAlreadySetException extends Exception {
+ /**
+ * Exception constructor.
+ *
+ * @return void
+ */
+ public function __construct() {
+ parent::__construct('The unique name has already been set.');
+ }
}
diff --git a/src/Exceptions/UniqueNameNotSetException.php b/src/Exceptions/UniqueNameNotSetException.php
index 6d5f729..5820d03 100644
--- a/src/Exceptions/UniqueNameNotSetException.php
+++ b/src/Exceptions/UniqueNameNotSetException.php
@@ -4,15 +4,13 @@
use Exception;
-class UniqueNameNotSetException extends Exception
-{
- /**
- * Exception constructor.
- *
- * @return void
- */
- public function __construct()
- {
- parent::__construct('You must set a unique name before updating progress');
- }
+class UniqueNameNotSetException extends Exception {
+ /**
+ * Exception constructor.
+ *
+ * @return void
+ */
+ public function __construct() {
+ parent::__construct('You must set a unique name before updating progress');
+ }
}
diff --git a/src/Progressable.php b/src/Progressable.php
index 8cb9cec..a12076c 100644
--- a/src/Progressable.php
+++ b/src/Progressable.php
@@ -6,328 +6,500 @@
use Verseles\Progressable\Exceptions\UniqueNameAlreadySetException;
use Verseles\Progressable\Exceptions\UniqueNameNotSetException;
-trait Progressable
-{
- /**
- * The unique name of the overall progress.
- *
- * @var string
- */
- protected string $overallUniqueName;
-
- /**
- * The progress value for this instance.
- *
- * @var float
- */
- protected float $progress = 0;
-
- /**
- * The callback function for saving cache data.
- *
- * @var callable|null
- */
- protected $customSaveData = null;
-
- /**
- * The callback function for retrieving cache data.
- *
- * @var callable|null
- */
- protected $customGetData = null;
-
- /**
- * The default cache time-to-live in minutes.
- *
- * @var int
- */
- protected int $defaultTTL = 1140;
-
- /**
- * If you want to override the default cache time-to-live in minutes,
- *
- * @var null|int
- */
- protected $customTTL = null;
-
- protected string|null $localKey = null;
-
- protected string $defaultPrefixStorageKey = "progressable";
-
- protected $customPrefixStorageKey = null;
-
- /**
- * Set the callback function for saving cache data.
- *
- * @param callable $callback
- * @return $this
- */
- public function setCustomSaveData(callable $callback): static
- {
- $this->customSaveData = $callback;
- return $this;
- }
-
- /**
- * Set the callback function for retrieving cache data.
- *
- * @param callable $callback
- * @return $this
- */
- public function setCustomGetData(callable $callback): static
- {
- $this->customGetData = $callback;
- return $this;
- }
-
- /**
- * Get the overall progress for the unique name.
- *
- * @param int $precision The precision of the overall progress
- * @return float
- */
- public function getOverallProgress(int $precision = 2): float
- {
- $progressData = $this->getOverallProgressData();
-
- $totalProgress = array_sum(array_column($progressData, "progress"));
- $totalCount = count($progressData);
-
- if ($totalCount === 0) {
- return 0;
- }
-
- return round($totalProgress / $totalCount, $precision, PHP_ROUND_HALF_ODD);
- }
-
- /**
- * Get the overall progress data from the storage.
- *
- * @return array
- */
- public function getOverallProgressData(): array
- {
- if ($this->customGetData !== null) {
- return call_user_func($this->customGetData, $this->getStorageKeyName());
- }
-
- return Cache::get($this->getStorageKeyName(), []);
- }
-
- /**
- * Get the cache key for the unique name.
- *
- * @return string
- */
- protected function getStorageKeyName(): string
- {
- return $this->getPrefixStorageKey() . '_' . $this->getOverallUniqueName();
- }
-
- /**
- * Retrieve the prefix storage key for the PHP function.
- *
- * @return string
- */
- protected function getPrefixStorageKey(): string
- {
- return $this->customPrefixStorageKey ?? config("progressable.prefix", $this->defaultPrefixStorageKey);
- }
-
- /**
- * Get the overall unique name.
- *
- * @return string the overall unique name
- * @throws UniqueNameNotSetException If the overall unique name is not set
- */
- public function getOverallUniqueName()
- {
- if (!isset($this->overallUniqueName) || empty($this->overallUniqueName)) {
- throw new UniqueNameNotSetException();
- }
-
- return $this->overallUniqueName;
- }
-
- /**
- * Set the unique name of the overall progress.
- *
- * @param string $overallUniqueName
- * @return $this
- */
- public function setOverallUniqueName(string $overallUniqueName): static
- {
- $this->overallUniqueName = $overallUniqueName;
-
- $this->makeSureLocalIsPartOfTheCalc();
-
- return $this;
- }
-
- /**
- * @return void
- * @throws UniqueNameNotSetException
- */
- public function makeSureLocalIsPartOfTheCalc(): void
- {
- if ($this->getLocalProgress(0) == 0) {
- // This make sure that the class who called this method will be part of the overall progress calculation
- $this->resetLocalProgress();
- }
- }
-
- /**
- * Get the progress value for this instance
- *
- * @param int $precision The precision of the local progress
- * @return float
- */
- public function getLocalProgress(int $precision = 2): float
- {
- return round($this->progress, $precision, PHP_ROUND_HALF_ODD);
- }
-
- /**
- * @return static
- * @throws UniqueNameNotSetException
- */
- public function resetLocalProgress(): static
- {
- return $this->setLocalProgress(0);
- }
-
- /**
- * Update the progress value for this instance.
- *
- * @param float $progress
- * @return $this
- * @throws UniqueNameNotSetException
- */
- public function setLocalProgress(float $progress): static
- {
- $this->progress = max(0, min(100, $progress));
-
- return $this->updateLocalProgressData($this->progress);
- }
-
- /**
- * Update the progress data in storage.
- *
- * @return static
- */
- protected function updateLocalProgressData(float $progress): static
- {
- $progressData = $this->getOverallProgressData();
-
- $progressData[$this->getLocalKey()] = [
- "progress" => $progress,
- ];
-
- return $this->saveOverallProgressData($progressData);
- }
-
- /**
- * Get the cache identifier for this instance.
- *
- * @return string
- */
- public function getLocalKey(): string
- {
- return $this->localKey ?? get_class($this) . "@" . spl_object_hash($this);
- }
-
- /**
- * Set the local key
- *
- * @param string $name
- * @return $this
- */
- public function setLocalKey(string $name): static
- {
- if (isset($this->getOverallProgressData()[$this->getLocalKey()])) {
- // Rename the local key preserving the data
- $overallProgressData = $this->getOverallProgressData();
- $overallProgressData[$name] = $overallProgressData[$this->getLocalKey()];
- unset($overallProgressData[$this->getLocalKey()]);
- $this->saveOverallProgressData($overallProgressData);
- }
-
- $this->localKey = $name;
-
- $this->makeSureLocalIsPartOfTheCalc();
-
- return $this;
- }
-
- /**
- * Save the overall progress data to the storage.
- *
- * @param array $progressData
- * @return static
- */
- protected function saveOverallProgressData(array $progressData): static
- {
- if ($this->customSaveData !== null) {
- call_user_func(
- $this->customSaveData,
- $this->getStorageKeyName(),
- $progressData,
- $this->getTTL()
- );
- } else {
- Cache::put($this->getStorageKeyName(), $progressData, $this->getTTL());
- }
-
- return $this;
- }
-
- /**
- * Get the storage time-to-live in minutes.
- *
- * @return int
- */
- public function getTTL(): int
- {
- return $this->customTTL ?? config("progressable.ttl", $this->defaultTTL);
- }
-
- /**
- * Reset the overall progress.
- *
- * @return static
- */
- public function resetOverallProgress(): static
- {
- return $this->saveOverallProgressData([]);
- }
-
- /**
- * Set the prefix storage key.
- *
- * @param string $prefixStorageKey The prefix storage key to set.
- * @throw UniqueNameAlreadySetException If the unique name has already been set
- * @return static
- */
- public function setPrefixStorageKey(string $prefixStorageKey): static
- {
- if (isset($this->overallUniqueName)) {
- throw new UniqueNameAlreadySetException();
- }
-
- $this->customPrefixStorageKey = $prefixStorageKey;
-
- return $this;
- }
-
- /**
- * Set the time-to-live for the object.
- *
- * @param int $defaultTTL The time-to-live value to set
- * @return static
- */
- public function setTTL(int $TTL): static
- {
- $this->customTTL = $TTL;
- return $this;
- }
+trait Progressable {
+ /**
+ * The unique name of the overall progress.
+ */
+ protected string $overallUniqueName;
+
+ /**
+ * The progress value for this instance.
+ */
+ protected float $progress = 0;
+
+ /**
+ * The callback function for saving cache data.
+ *
+ * @var callable|null
+ */
+ protected mixed $customSaveData = null;
+
+ /**
+ * The callback function for retrieving cache data.
+ *
+ * @var callable|null
+ */
+ protected mixed $customGetData = null;
+
+ /**
+ * The default cache time-to-live in minutes.
+ */
+ protected int $defaultTTL = 1140;
+
+ /**
+ * If you want to override the default cache time-to-live in minutes,
+ */
+ protected ?int $customTTL = null;
+
+ /**
+ * The local key identifier for this instance.
+ */
+ protected ?string $localKey = null;
+
+ /**
+ * The default prefix for storage keys.
+ */
+ protected string $defaultPrefixStorageKey = 'progressable';
+
+ /**
+ * Custom prefix for storage keys.
+ */
+ protected ?string $customPrefixStorageKey = null;
+
+ /**
+ * The default precision for progress values.
+ */
+ protected int $defaultPrecision = 2;
+
+ /**
+ * Custom precision for progress values.
+ */
+ protected ?int $customPrecision = null;
+
+ /**
+ * Metadata associated with this progress instance.
+ */
+ protected array $metadata = [];
+
+ /**
+ * Status message for this progress instance.
+ */
+ protected ?string $statusMessage = null;
+
+ /**
+ * Callback to be called when progress changes.
+ *
+ * @var callable|null
+ */
+ protected mixed $onProgressChange = null;
+
+ /**
+ * Callback to be called when progress completes.
+ *
+ * @var callable|null
+ */
+ protected mixed $onComplete = null;
+
+ /**
+ * Set the callback function for saving cache data.
+ *
+ * @return $this
+ */
+ public function setCustomSaveData(callable $callback): static {
+ $this->customSaveData = $callback;
+
+ return $this;
+ }
+
+ /**
+ * Set the callback function for retrieving cache data.
+ *
+ * @return $this
+ */
+ public function setCustomGetData(callable $callback): static {
+ $this->customGetData = $callback;
+
+ return $this;
+ }
+
+ /**
+ * Get the overall progress for the unique name.
+ *
+ * @param int|null $precision The precision of the overall progress (null uses configured default)
+ */
+ public function getOverallProgress(?int $precision = null): float {
+ $precision = $precision ?? $this->getPrecision();
+ $progressData = $this->getOverallProgressData();
+
+ $totalProgress = array_sum(array_column($progressData, 'progress'));
+ $totalCount = count($progressData);
+
+ if ($totalCount === 0) {
+ return 0;
+ }
+
+ return round($totalProgress / $totalCount, $precision, PHP_ROUND_HALF_ODD);
+ }
+
+ /**
+ * Get the overall progress data from the storage.
+ */
+ public function getOverallProgressData(): array {
+ if ($this->customGetData !== null) {
+ return call_user_func($this->customGetData, $this->getStorageKeyName());
+ }
+
+ return Cache::get($this->getStorageKeyName(), []);
+ }
+
+ /**
+ * Get the cache key for the unique name.
+ */
+ protected function getStorageKeyName(): string {
+ return $this->getPrefixStorageKey().'_'.$this->getOverallUniqueName();
+ }
+
+ /**
+ * Retrieve the prefix storage key for the PHP function.
+ */
+ protected function getPrefixStorageKey(): string {
+ return $this->customPrefixStorageKey ?? config('progressable.prefix', $this->defaultPrefixStorageKey);
+ }
+
+ /**
+ * Get the overall unique name.
+ *
+ * @return string The overall unique name
+ *
+ * @throws UniqueNameNotSetException If the overall unique name is not set
+ */
+ public function getOverallUniqueName(): string {
+ if (! isset($this->overallUniqueName) || empty($this->overallUniqueName)) {
+ throw new UniqueNameNotSetException;
+ }
+
+ return $this->overallUniqueName;
+ }
+
+ /**
+ * Set the unique name of the overall progress.
+ *
+ * @return $this
+ */
+ public function setOverallUniqueName(string $overallUniqueName): static {
+ $this->overallUniqueName = $overallUniqueName;
+
+ $this->makeSureLocalIsPartOfTheCalc();
+
+ return $this;
+ }
+
+ /**
+ * @throws UniqueNameNotSetException
+ */
+ public function makeSureLocalIsPartOfTheCalc(): void {
+ if ($this->getLocalProgress(0) == 0) {
+ // This make sure that the class who called this method will be part of the overall progress calculation
+ $this->resetLocalProgress();
+ }
+ }
+
+ /**
+ * Get the progress value for this instance
+ *
+ * @param int|null $precision The precision of the local progress (null uses configured default)
+ */
+ public function getLocalProgress(?int $precision = null): float {
+ $precision = $precision ?? $this->getPrecision();
+
+ return round($this->progress, $precision, PHP_ROUND_HALF_ODD);
+ }
+
+ /**
+ * @throws UniqueNameNotSetException
+ */
+ public function resetLocalProgress(): static {
+ return $this->setLocalProgress(0);
+ }
+
+ /**
+ * Increment the local progress by a given amount.
+ *
+ * @param float $amount The amount to increment (can be negative to decrement)
+ *
+ * @throws UniqueNameNotSetException
+ */
+ public function incrementLocalProgress(float $amount = 1): static {
+ return $this->setLocalProgress($this->progress + $amount);
+ }
+
+ /**
+ * Check if the local progress is complete (100%).
+ */
+ public function isComplete(): bool {
+ return $this->progress >= 100;
+ }
+
+ /**
+ * Check if the overall progress is complete (100%).
+ */
+ public function isOverallComplete(): bool {
+ return $this->getOverallProgress(0) >= 100;
+ }
+
+ /**
+ * Remove this instance from the overall progress calculation.
+ *
+ *
+ * @throws UniqueNameNotSetException
+ */
+ public function removeLocalFromOverall(): static {
+ $progressData = $this->getOverallProgressData();
+ $localKey = $this->getLocalKey();
+
+ if (isset($progressData[$localKey])) {
+ unset($progressData[$localKey]);
+ $this->saveOverallProgressData($progressData);
+ }
+
+ $this->progress = 0;
+
+ return $this;
+ }
+
+ /**
+ * Update the progress value for this instance.
+ *
+ * @return $this
+ *
+ * @throws UniqueNameNotSetException
+ */
+ public function setLocalProgress(float $progress): static {
+ $oldProgress = $this->progress;
+ $this->progress = max(0, min(100, $progress));
+
+ $this->updateLocalProgressData($this->progress);
+
+ // Fire progress change callback
+ if ($this->onProgressChange !== null && $oldProgress !== $this->progress) {
+ call_user_func($this->onProgressChange, $this->progress, $oldProgress, $this);
+ }
+
+ // Fire complete callback
+ if ($this->onComplete !== null && $this->progress >= 100 && $oldProgress < 100) {
+ call_user_func($this->onComplete, $this);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Update the progress data in storage.
+ */
+ protected function updateLocalProgressData(float $progress): static {
+ $progressData = $this->getOverallProgressData();
+
+ $localData = [
+ 'progress' => $progress,
+ ];
+
+ if ($this->statusMessage !== null) {
+ $localData['message'] = $this->statusMessage;
+ }
+
+ if (! empty($this->metadata)) {
+ $localData['metadata'] = $this->metadata;
+ }
+
+ $progressData[$this->getLocalKey()] = $localData;
+
+ return $this->saveOverallProgressData($progressData);
+ }
+
+ /**
+ * Get the cache identifier for this instance.
+ */
+ public function getLocalKey(): string {
+ return $this->localKey ?? get_class($this).'@'.spl_object_hash($this);
+ }
+
+ /**
+ * Set the local key
+ *
+ * @param string $name The new local key name
+ */
+ public function setLocalKey(string $name): static {
+ $currentKey = $this->getLocalKey();
+ $overallProgressData = $this->getOverallProgressData();
+
+ if (isset($overallProgressData[$currentKey])) {
+ // Rename the local key preserving the data
+ $overallProgressData[$name] = $overallProgressData[$currentKey];
+ unset($overallProgressData[$currentKey]);
+ $this->saveOverallProgressData($overallProgressData);
+ }
+
+ $this->localKey = $name;
+
+ $this->makeSureLocalIsPartOfTheCalc();
+
+ return $this;
+ }
+
+ /**
+ * Save the overall progress data to the storage.
+ */
+ protected function saveOverallProgressData(array $progressData): static {
+ if ($this->customSaveData !== null) {
+ call_user_func(
+ $this->customSaveData,
+ $this->getStorageKeyName(),
+ $progressData,
+ $this->getTTL()
+ );
+ } else {
+ Cache::put($this->getStorageKeyName(), $progressData, $this->getTTL());
+ }
+
+ return $this;
+ }
+
+ /**
+ * Get the storage time-to-live in minutes.
+ */
+ public function getTTL(): int {
+ return $this->customTTL ?? config('progressable.ttl', $this->defaultTTL);
+ }
+
+ /**
+ * Reset the overall progress.
+ */
+ public function resetOverallProgress(): static {
+ return $this->saveOverallProgressData([]);
+ }
+
+ /**
+ * Set the prefix storage key.
+ *
+ * @param string $prefixStorageKey The prefix storage key to set.
+ *
+ * @throw UniqueNameAlreadySetException If the unique name has already been set
+ */
+ public function setPrefixStorageKey(string $prefixStorageKey): static {
+ if (isset($this->overallUniqueName)) {
+ throw new UniqueNameAlreadySetException;
+ }
+
+ $this->customPrefixStorageKey = $prefixStorageKey;
+
+ return $this;
+ }
+
+ /**
+ * Set the time-to-live for the object.
+ *
+ * @param int $TTL The time-to-live value to set in minutes
+ */
+ public function setTTL(int $TTL): static {
+ $this->customTTL = $TTL;
+
+ return $this;
+ }
+
+ /**
+ * Get the precision for progress values.
+ */
+ public function getPrecision(): int {
+ return $this->customPrecision ?? config('progressable.precision', $this->defaultPrecision);
+ }
+
+ /**
+ * Set the precision for progress values.
+ *
+ * @param int $precision The number of decimal places
+ */
+ public function setPrecision(int $precision): static {
+ $this->customPrecision = max(0, $precision);
+
+ return $this;
+ }
+
+ /**
+ * Set the status message for this progress instance.
+ */
+ public function setStatusMessage(?string $message): static {
+ $this->statusMessage = $message;
+
+ // Update storage with new message if we have a unique name
+ if (isset($this->overallUniqueName)) {
+ $this->updateLocalProgressData($this->progress);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Get the status message for this progress instance.
+ */
+ public function getStatusMessage(): ?string {
+ return $this->statusMessage;
+ }
+
+ /**
+ * Set metadata for this progress instance.
+ *
+ * @param array $metadata
+ */
+ public function setMetadata(array $metadata): static {
+ $this->metadata = $metadata;
+
+ // Update storage with new metadata if we have a unique name
+ if (isset($this->overallUniqueName)) {
+ $this->updateLocalProgressData($this->progress);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Get metadata for this progress instance.
+ *
+ * @return array
+ */
+ public function getMetadata(): array {
+ return $this->metadata;
+ }
+
+ /**
+ * Add or update a single metadata value.
+ */
+ public function addMetadata(string $key, mixed $value): static {
+ $this->metadata[$key] = $value;
+
+ // Update storage with new metadata if we have a unique name
+ if (isset($this->overallUniqueName)) {
+ $this->updateLocalProgressData($this->progress);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Get a single metadata value.
+ */
+ public function getMetadataValue(string $key, mixed $default = null): mixed {
+ return $this->metadata[$key] ?? $default;
+ }
+
+ /**
+ * Set a callback to be called when progress changes.
+ *
+ * The callback receives: (float $newProgress, float $oldProgress, static $instance)
+ */
+ public function onProgressChange(callable $callback): static {
+ $this->onProgressChange = $callback;
+
+ return $this;
+ }
+
+ /**
+ * Set a callback to be called when progress reaches 100%.
+ *
+ * The callback receives: (static $instance)
+ */
+ public function onComplete(callable $callback): static {
+ $this->onComplete = $callback;
+
+ return $this;
+ }
}
diff --git a/src/ProgressableServiceProvider.php b/src/ProgressableServiceProvider.php
index a8781fe..953b93f 100644
--- a/src/ProgressableServiceProvider.php
+++ b/src/ProgressableServiceProvider.php
@@ -4,37 +4,29 @@
use Illuminate\Support\ServiceProvider;
-class ProgressableServiceProvider extends ServiceProvider
-{
- /**
- * Register services.
- *
- * @return void
- */
- public function register(): void
- {
- $this->mergeConfigFrom(__DIR__.'/../config/progressable.php', 'progressable');
- }
+class ProgressableServiceProvider extends ServiceProvider {
+ /**
+ * Register services.
+ */
+ public function register(): void {
+ $this->mergeConfigFrom(__DIR__.'/../config/progressable.php', 'progressable');
+ }
- /**
- * Bootstrap services.
- *
- * @return void
- */
- public function boot(): void
- {
- if ($this->app->runningInConsole()) {
- $this->publishesConfig();
+ /**
+ * Bootstrap services.
+ */
+ public function boot(): void {
+ if ($this->app->runningInConsole()) {
+ $this->publishesConfig();
+ }
}
- }
- /**
- * Publish config file
- */
- protected function publishesConfig(): void
- {
- $this->publishes([
- __DIR__.'/../config/progressable.php' => config_path('progressable.php'),
- ], 'config');
- }
+ /**
+ * Publish config file
+ */
+ protected function publishesConfig(): void {
+ $this->publishes([
+ __DIR__.'/../config/progressable.php' => config_path('progressable.php'),
+ ], 'config');
+ }
}
diff --git a/tests/ProgressableTest.php b/tests/ProgressableTest.php
index c45dbd2..2a21740 100644
--- a/tests/ProgressableTest.php
+++ b/tests/ProgressableTest.php
@@ -3,137 +3,470 @@
namespace Verseles\Progressable\Tests;
use Orchestra\Testbench\TestCase;
+use Verseles\Progressable\Exceptions\UniqueNameAlreadySetException;
use Verseles\Progressable\Exceptions\UniqueNameNotSetException;
use Verseles\Progressable\Progressable;
-class ProgressableTest extends TestCase
-{
- use Progressable;
-
- public function testSetOverallUniqueName()
- {
- $this->setOverallUniqueName('test');
- $this->assertEquals('test', $this->getOverallUniqueName());
- }
-
- public function testUpdateLocalProgress()
- {
- $this->setOverallUniqueName('test');
- $this->setLocalProgress(50);
- $this->assertEquals(50, $this->getLocalProgress());
- }
-
- public function testUpdateLocalProgressBounds()
- {
- $this->setOverallUniqueName('test');
- $this->setLocalProgress(-10);
- $this->assertEquals(0, $this->getLocalProgress());
- $this->setLocalProgress(120);
- $this->assertEquals(100, $this->getLocalProgress());
- }
-
- public function testGetOverallProgress()
- {
- $uniqueName = 'test';
- $this->setOverallUniqueName($uniqueName);
- $this->setLocalProgress(25);
-
- $obj2 = new class {
- use Progressable;
- };
- $obj2->setOverallUniqueName($uniqueName);
- $obj2->setLocalProgress(75);
-
- $this->assertEquals(50, $this->getOverallProgress());
- }
-
- public function testGetOverallProgressData()
- {
- $this->setOverallUniqueName('test');
- $this->setLocalProgress(50);
-
- $progressData = $this->getOverallProgressData();
- $this->assertArrayHasKey($this->getLocalKey(), $progressData);
- $this->assertEquals(50, $progressData[$this->getLocalKey()]['progress']);
- }
-
- public function testUpdateLocalProgressWithoutUniqueName()
- {
- $this->overallUniqueName = '';
- $this->expectException(UniqueNameNotSetException::class);
- $this->setLocalProgress(50);
- }
-
- public function testResetLocalProgress()
- {
- $this->setOverallUniqueName('test');
- $this->setLocalProgress(50);
- $this->resetLocalProgress();
- $this->assertEquals(0, $this->getLocalProgress());
- }
-
- public function testResetOverallProgress()
- {
- $this->setOverallUniqueName('test');
- $this->setLocalProgress(50);
-
- $obj2 = new class {
- use Progressable;
- };
- $obj2->setOverallUniqueName('test');
- $obj2->setLocalProgress(75);
-
- $this->assertNotEquals(0, $this->getOverallProgress());
- $this->resetOverallProgress();
- $this->assertEquals(0, $this->getOverallProgress());
- }
-
- public function testSetLocalKey()
- {
- $this->setOverallUniqueName('test');
- $this->setLocalKey('my_custom_key');
- $progressData = $this->getOverallProgressData();
- $this->assertArrayHasKey('my_custom_key', $progressData);
- }
-
- public function testSetPrefixStorageKey()
- {
- $this->setPrefixStorageKey('custom_prefix');
- $this->setOverallUniqueName('test');
- $this->assertEquals('custom_prefix_test', $this->getStorageKeyName());
-
- }
-
- public function testSetTTL()
- {
- $this->setOverallUniqueName('test');
- $this->setLocalProgress(50);
- $this->setTTL(60); // 1 heure
-
- $ttl = $this->getTTL();
- $this->assertEquals(60, $ttl);
- }
-
- public function testCustomSaveAndGetData()
- {
- $storage = [];
-
- $saveCallback = function ($key, $data, $ttl) use (&$storage) {
- $storage[$key] = $data;
- };
-
- $getCallback = function ($key) use (&$storage) {
- return $storage[$key] ?? [];
- };
-
- $this->setCustomSaveData($saveCallback);
- $this->setCustomGetData($getCallback);
-
- $this->setOverallUniqueName('custom_test');
- $this->setLocalProgress(50);
-
- $progressData = $this->getOverallProgressData();
- $this->assertArrayHasKey($this->getLocalKey(), $progressData);
- $this->assertEquals(50, $progressData[$this->getLocalKey()]['progress']);
- }
+class ProgressableTest extends TestCase {
+ use Progressable;
+
+ /**
+ * Unique test identifier to isolate tests.
+ */
+ private string $testId;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ // Generate unique ID for each test to ensure isolation
+ $this->testId = uniqid('test_', true);
+
+ // Reset trait properties to default state
+ $this->progress = 0;
+ $this->customSaveData = null;
+ $this->customGetData = null;
+ $this->customTTL = null;
+ $this->localKey = null;
+ $this->customPrefixStorageKey = null;
+ $this->customPrecision = null;
+ $this->metadata = [];
+ $this->statusMessage = null;
+ $this->onProgressChange = null;
+ $this->onComplete = null;
+
+ // Unset overallUniqueName to simulate fresh state
+ unset($this->overallUniqueName);
+ }
+
+ public function test_set_overall_unique_name(): void {
+ $uniqueName = 'test_unique_'.$this->testId;
+ $this->setOverallUniqueName($uniqueName);
+ $this->assertEquals($uniqueName, $this->getOverallUniqueName());
+ }
+
+ public function test_update_local_progress(): void {
+ $this->setOverallUniqueName('test_progress_'.$this->testId);
+ $this->setLocalProgress(50);
+ $this->assertEquals(50, $this->getLocalProgress());
+ }
+
+ public function test_update_local_progress_bounds(): void {
+ $this->setOverallUniqueName('test_bounds_'.$this->testId);
+ $this->setLocalProgress(-10);
+ $this->assertEquals(0, $this->getLocalProgress());
+ $this->setLocalProgress(120);
+ $this->assertEquals(100, $this->getLocalProgress());
+ }
+
+ public function test_get_overall_progress(): void {
+ $uniqueName = 'test_overall_'.$this->testId;
+ $this->setOverallUniqueName($uniqueName);
+ $this->setLocalProgress(25);
+
+ $obj2 = new class {
+ use Progressable;
+ };
+ $obj2->setOverallUniqueName($uniqueName);
+ $obj2->setLocalProgress(75);
+
+ $this->assertEquals(50, $this->getOverallProgress());
+ }
+
+ public function test_get_overall_progress_data(): void {
+ $this->setOverallUniqueName('test_data_'.$this->testId);
+ $this->setLocalProgress(50);
+
+ $progressData = $this->getOverallProgressData();
+ $this->assertArrayHasKey($this->getLocalKey(), $progressData);
+ $this->assertEquals(50, $progressData[$this->getLocalKey()]['progress']);
+ }
+
+ public function test_update_local_progress_without_unique_name(): void {
+ $this->expectException(UniqueNameNotSetException::class);
+ $this->setLocalProgress(50);
+ }
+
+ public function test_reset_local_progress(): void {
+ $this->setOverallUniqueName('test_reset_local_'.$this->testId);
+ $this->setLocalProgress(50);
+ $this->resetLocalProgress();
+ $this->assertEquals(0, $this->getLocalProgress());
+ }
+
+ public function test_reset_overall_progress(): void {
+ $uniqueName = 'test_reset_overall_'.$this->testId;
+ $this->setOverallUniqueName($uniqueName);
+ $this->setLocalProgress(50);
+
+ $obj2 = new class {
+ use Progressable;
+ };
+ $obj2->setOverallUniqueName($uniqueName);
+ $obj2->setLocalProgress(75);
+
+ $this->assertNotEquals(0, $this->getOverallProgress());
+ $this->resetOverallProgress();
+ $this->assertEquals(0, $this->getOverallProgress());
+ }
+
+ public function test_set_local_key(): void {
+ $this->setOverallUniqueName('test_local_key_'.$this->testId);
+ $this->setLocalKey('my_custom_key');
+ $progressData = $this->getOverallProgressData();
+ $this->assertArrayHasKey('my_custom_key', $progressData);
+ }
+
+ public function test_set_local_key_preserves_progress_data(): void {
+ $this->setOverallUniqueName('test_local_key_rename_'.$this->testId);
+ $this->setLocalKey('original_key');
+ $this->setLocalProgress(75);
+
+ // Verify original key has the progress
+ $progressData = $this->getOverallProgressData();
+ $this->assertArrayHasKey('original_key', $progressData);
+ $this->assertEquals(75, $progressData['original_key']['progress']);
+
+ // Rename the key
+ $this->setLocalKey('renamed_key');
+
+ // Verify data was preserved under new key and old key is removed
+ $progressData = $this->getOverallProgressData();
+ $this->assertArrayHasKey('renamed_key', $progressData);
+ $this->assertArrayNotHasKey('original_key', $progressData);
+ $this->assertEquals(75, $progressData['renamed_key']['progress']);
+ }
+
+ public function test_set_prefix_storage_key(): void {
+ $this->setPrefixStorageKey('custom_prefix');
+ $this->setOverallUniqueName('test_prefix_'.$this->testId);
+ $this->assertEquals('custom_prefix_test_prefix_'.$this->testId, $this->getStorageKeyName());
+ }
+
+ public function test_set_prefix_storage_key_after_unique_name_throws_exception(): void {
+ $this->setOverallUniqueName('test_prefix_exception_'.$this->testId);
+ $this->expectException(UniqueNameAlreadySetException::class);
+ $this->setPrefixStorageKey('custom_prefix');
+ }
+
+ public function test_set_ttl(): void {
+ $this->setOverallUniqueName('test_ttl_'.$this->testId);
+ $this->setLocalProgress(50);
+ $this->setTTL(60); // 1 hour
+
+ $ttl = $this->getTTL();
+ $this->assertEquals(60, $ttl);
+ }
+
+ public function test_custom_save_and_get_data(): void {
+ $storage = [];
+
+ $saveCallback = function ($key, $data, $ttl) use (&$storage) {
+ $storage[$key] = $data;
+ };
+
+ $getCallback = function ($key) use (&$storage) {
+ return $storage[$key] ?? [];
+ };
+
+ $this->setCustomSaveData($saveCallback);
+ $this->setCustomGetData($getCallback);
+
+ $this->setOverallUniqueName('custom_test_'.$this->testId);
+ $this->setLocalProgress(50);
+
+ $progressData = $this->getOverallProgressData();
+ $this->assertArrayHasKey($this->getLocalKey(), $progressData);
+ $this->assertEquals(50, $progressData[$this->getLocalKey()]['progress']);
+ }
+
+ public function test_make_sure_local_is_part_of_the_calc(): void {
+ $uniqueName = 'test_auto_register_'.$this->testId;
+
+ // When setting unique name, instance should be automatically registered
+ $this->setOverallUniqueName($uniqueName);
+
+ $progressData = $this->getOverallProgressData();
+ $this->assertArrayHasKey($this->getLocalKey(), $progressData);
+ $this->assertEquals(0, $progressData[$this->getLocalKey()]['progress']);
+ }
+
+ public function test_get_overall_progress_with_empty_data(): void {
+ $this->setOverallUniqueName('test_empty_'.$this->testId);
+ $this->resetOverallProgress();
+
+ // Should return 0 when no progress data exists
+ $this->assertEquals(0, $this->getOverallProgress());
+ }
+
+ public function test_progress_precision(): void {
+ $this->setOverallUniqueName('test_precision_'.$this->testId);
+ $this->setLocalProgress(33.3333);
+
+ $this->assertEquals(33.33, $this->getLocalProgress(2));
+ $this->assertEquals(33.333, $this->getLocalProgress(3));
+ $this->assertEquals(33, $this->getLocalProgress(0));
+ }
+
+ public function test_increment_local_progress(): void {
+ $this->setOverallUniqueName('test_increment_'.$this->testId);
+ $this->setLocalProgress(10);
+
+ $this->incrementLocalProgress(5);
+ $this->assertEquals(15, $this->getLocalProgress());
+
+ $this->incrementLocalProgress(10);
+ $this->assertEquals(25, $this->getLocalProgress());
+ }
+
+ public function test_increment_local_progress_with_negative(): void {
+ $this->setOverallUniqueName('test_decrement_'.$this->testId);
+ $this->setLocalProgress(50);
+
+ $this->incrementLocalProgress(-10);
+ $this->assertEquals(40, $this->getLocalProgress());
+ }
+
+ public function test_increment_local_progress_respects_bounds(): void {
+ $this->setOverallUniqueName('test_increment_bounds_'.$this->testId);
+ $this->setLocalProgress(95);
+
+ $this->incrementLocalProgress(10);
+ $this->assertEquals(100, $this->getLocalProgress());
+
+ $this->setLocalProgress(5);
+ $this->incrementLocalProgress(-10);
+ $this->assertEquals(0, $this->getLocalProgress());
+ }
+
+ public function test_is_complete(): void {
+ $this->setOverallUniqueName('test_is_complete_'.$this->testId);
+
+ $this->setLocalProgress(50);
+ $this->assertFalse($this->isComplete());
+
+ $this->setLocalProgress(99.99);
+ $this->assertFalse($this->isComplete());
+
+ $this->setLocalProgress(100);
+ $this->assertTrue($this->isComplete());
+ }
+
+ public function test_is_overall_complete(): void {
+ $uniqueName = 'test_is_overall_complete_'.$this->testId;
+ $this->setOverallUniqueName($uniqueName);
+ $this->setLocalProgress(100);
+
+ $obj2 = new class {
+ use Progressable;
+ };
+ $obj2->setOverallUniqueName($uniqueName);
+ $obj2->setLocalProgress(50);
+
+ $this->assertFalse($this->isOverallComplete());
+
+ $obj2->setLocalProgress(100);
+ $this->assertTrue($this->isOverallComplete());
+ }
+
+ public function test_remove_local_from_overall(): void {
+ $uniqueName = 'test_remove_local_'.$this->testId;
+ $this->setOverallUniqueName($uniqueName);
+ $this->setLocalKey('instance_1');
+ $this->setLocalProgress(50);
+
+ $obj2 = new class {
+ use Progressable;
+ };
+ $obj2->setOverallUniqueName($uniqueName);
+ $obj2->setLocalKey('instance_2');
+ $obj2->setLocalProgress(100);
+
+ // Both instances contribute to overall progress
+ $this->assertEquals(75, $this->getOverallProgress());
+
+ // Remove first instance
+ $this->removeLocalFromOverall();
+
+ // Only second instance remains
+ $progressData = $this->getOverallProgressData();
+ $this->assertArrayNotHasKey('instance_1', $progressData);
+ $this->assertArrayHasKey('instance_2', $progressData);
+ $this->assertEquals(100, $this->getOverallProgress());
+
+ // Local progress should be reset to 0
+ $this->assertEquals(0, $this->progress);
+ }
+
+ public function test_set_precision(): void {
+ $this->setOverallUniqueName('test_set_precision_'.$this->testId);
+ $this->setLocalProgress(33.33333);
+
+ // Default precision
+ $this->assertEquals(33.33, $this->getLocalProgress());
+
+ // Custom precision
+ $this->setPrecision(4);
+ $this->assertEquals(4, $this->getPrecision());
+ $this->assertEquals(33.3333, $this->getLocalProgress());
+
+ // Precision 0
+ $this->setPrecision(0);
+ $this->assertEquals(33, $this->getLocalProgress());
+ }
+
+ public function test_get_local_progress_uses_default_precision(): void {
+ $this->setOverallUniqueName('test_default_precision_'.$this->testId);
+ $this->setLocalProgress(33.33333);
+
+ // Without parameter, should use default precision (2)
+ $this->assertEquals(33.33, $this->getLocalProgress());
+
+ // With explicit null, should also use default
+ $this->assertEquals(33.33, $this->getLocalProgress(null));
+ }
+
+ public function test_get_overall_progress_uses_default_precision(): void {
+ $this->setOverallUniqueName('test_overall_default_precision_'.$this->testId);
+ $this->setLocalProgress(33.33333);
+
+ // Without parameter, should use default precision (2)
+ $this->assertEquals(33.33, $this->getOverallProgress());
+
+ // With explicit null, should also use default
+ $this->assertEquals(33.33, $this->getOverallProgress(null));
+ }
+
+ public function test_set_status_message(): void {
+ $this->setOverallUniqueName('test_status_message_'.$this->testId);
+ $this->setLocalProgress(50);
+
+ $this->setStatusMessage('Processing files...');
+ $this->assertEquals('Processing files...', $this->getStatusMessage());
+
+ // Check it's stored in progress data
+ $progressData = $this->getOverallProgressData();
+ $this->assertEquals('Processing files...', $progressData[$this->getLocalKey()]['message']);
+ }
+
+ public function test_set_metadata(): void {
+ $this->setOverallUniqueName('test_metadata_'.$this->testId);
+ $this->setLocalProgress(50);
+
+ $metadata = ['file' => 'test.txt', 'size' => 1024];
+ $this->setMetadata($metadata);
+
+ $this->assertEquals($metadata, $this->getMetadata());
+ $this->assertEquals('test.txt', $this->getMetadataValue('file'));
+ $this->assertEquals(1024, $this->getMetadataValue('size'));
+ $this->assertNull($this->getMetadataValue('nonexistent'));
+ $this->assertEquals('default', $this->getMetadataValue('nonexistent', 'default'));
+
+ // Check it's stored in progress data
+ $progressData = $this->getOverallProgressData();
+ $this->assertEquals($metadata, $progressData[$this->getLocalKey()]['metadata']);
+ }
+
+ public function test_add_metadata(): void {
+ $this->setOverallUniqueName('test_add_metadata_'.$this->testId);
+ $this->setLocalProgress(50);
+
+ $this->addMetadata('step', 1);
+ $this->addMetadata('total', 10);
+
+ $this->assertEquals(1, $this->getMetadataValue('step'));
+ $this->assertEquals(10, $this->getMetadataValue('total'));
+
+ // Update existing key
+ $this->addMetadata('step', 2);
+ $this->assertEquals(2, $this->getMetadataValue('step'));
+ }
+
+ public function test_on_progress_change_callback(): void {
+ $this->setOverallUniqueName('test_callback_change_'.$this->testId);
+
+ $callbackCalled = false;
+ $capturedNew = null;
+ $capturedOld = null;
+
+ $this->onProgressChange(function ($new, $old, $instance) use (&$callbackCalled, &$capturedNew, &$capturedOld) {
+ $callbackCalled = true;
+ $capturedNew = $new;
+ $capturedOld = $old;
+ });
+
+ $this->setLocalProgress(50);
+
+ $this->assertTrue($callbackCalled);
+ $this->assertEquals(50, $capturedNew);
+ $this->assertEquals(0, $capturedOld);
+ }
+
+ public function test_on_progress_change_not_called_when_same_value(): void {
+ $this->setOverallUniqueName('test_callback_same_'.$this->testId);
+ $this->setLocalProgress(50);
+
+ $callCount = 0;
+ $this->onProgressChange(function () use (&$callCount) {
+ $callCount++;
+ });
+
+ // Setting same value should not trigger callback
+ $this->setLocalProgress(50);
+ $this->assertEquals(0, $callCount);
+
+ // Setting different value should trigger callback
+ $this->setLocalProgress(51);
+ $this->assertEquals(1, $callCount);
+ }
+
+ public function test_on_complete_callback(): void {
+ $this->setOverallUniqueName('test_callback_complete_'.$this->testId);
+
+ $completeCalled = false;
+ $this->onComplete(function ($instance) use (&$completeCalled) {
+ $completeCalled = true;
+ });
+
+ $this->setLocalProgress(50);
+ $this->assertFalse($completeCalled);
+
+ $this->setLocalProgress(100);
+ $this->assertTrue($completeCalled);
+ }
+
+ public function test_on_complete_callback_only_fires_once(): void {
+ $this->setOverallUniqueName('test_callback_complete_once_'.$this->testId);
+
+ $callCount = 0;
+ $this->onComplete(function () use (&$callCount) {
+ $callCount++;
+ });
+
+ $this->setLocalProgress(100);
+ $this->assertEquals(1, $callCount);
+
+ // Setting to 100 again should not trigger (already at 100)
+ $this->setLocalProgress(100);
+ $this->assertEquals(1, $callCount);
+
+ // Going down and back up should trigger again
+ $this->setLocalProgress(50);
+ $this->setLocalProgress(100);
+ $this->assertEquals(2, $callCount);
+ }
+
+ public function test_metadata_and_message_stored_together(): void {
+ $this->setOverallUniqueName('test_metadata_message_'.$this->testId);
+
+ $this->setStatusMessage('Processing...');
+ $this->setMetadata(['step' => 1]);
+ $this->setLocalProgress(50);
+
+ $progressData = $this->getOverallProgressData();
+ $localData = $progressData[$this->getLocalKey()];
+
+ $this->assertEquals(50, $localData['progress']);
+ $this->assertEquals('Processing...', $localData['message']);
+ $this->assertEquals(['step' => 1], $localData['metadata']);
+ }
}