diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml new file mode 100644 index 0000000..16f47c8 --- /dev/null +++ b/.github/workflows/ci-cd.yml @@ -0,0 +1,63 @@ +name: CI/CD + +on: + push: + branches: [ main, dev ] + pull_request: + branches: [ main, dev ] + types: [ opened, synchronize, reopened, closed ] + +permissions: + contents: write + pull-requests: write + +jobs: + tests: + name: PHPUnit tests + runs-on: ubuntu-latest + if: github.event.action != 'closed' && !contains(github.event.head_commit.message, '--no-change') + strategy: + matrix: + php: [8.3, 8.4, 8.5] + steps: + - uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + extensions: mbstring, bcmath, pdo, sqlite + coverage: xdebug + + - name: Install Dependencies + run: composer install --prefer-dist --no-progress --no-interaction + + - name: Run Tests + env: + XDEBUG_MODE: coverage + run: vendor/bin/phpunit --colors=never --coverage-text + + release-please: + name: Release Please + needs: tests + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + steps: + - uses: google-github-actions/release-please-action@v4 + with: + release-type: php + token: ${{ secrets.GITHUB_TOKEN }} + + cleanup-release-please: + name: Cleanup release-please branch + if: > + github.event_name == 'pull_request' && + github.event.pull_request.merged == true && + startsWith(github.event.pull_request.head.ref, 'release-please') + runs-on: ubuntu-latest + steps: + - name: Delete release-please branch + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh api -X DELETE "/repos/${{ github.repository }}/git/refs/heads/${{ github.event.pull_request.head.ref }}" diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml deleted file mode 100644 index 495d780..0000000 --- a/.github/workflows/phpunit.yml +++ /dev/null @@ -1,32 +0,0 @@ -name: PHP Tests - -on: - push: - branches: [ main, dev ] - pull_request: - branches: [ main, dev ] - -jobs: - tests: - runs-on: ubuntu-latest - strategy: - matrix: - php: [8.2, 8.3] - - steps: - - uses: actions/checkout@v4 - - - name: Setup PHP - uses: shivammathur/setup-php@v2 - with: - php-version: ${{ matrix.php }} - extensions: mbstring, bcmath, pdo, sqlite - coverage: xdebug - - - name: Install Dependencies - run: composer install --prefer-dist --no-progress --no-interaction - - - name: Run Tests - env: - XDEBUG_MODE: coverage - run: vendor/bin/phpunit --colors=never --coverage-text diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..7c04ab9 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,20 @@ +default_install_hook_types: + - pre-commit + - commit-msg + +repos: + - repo: https://github.com/compilerla/conventional-pre-commit + rev: v4.4.0 + hooks: + - id: conventional-pre-commit + stages: [commit-msg] + args: [] + + - repo: local + hooks: + - id: phpunit + name: phpunit + entry: vendor/bin/phpunit + language: system + pass_filenames: false + always_run: true diff --git a/composer.json b/composer.json index 5afbeb9..c3c6b35 100644 --- a/composer.json +++ b/composer.json @@ -14,7 +14,7 @@ } }, "require": { - "php": ">=8.2" + "php": ">=8.3" }, "require-dev": { "phpunit/phpunit": "^10.0", diff --git a/readme.md b/readme.md index 0f86024..6e79355 100644 --- a/readme.md +++ b/readme.md @@ -39,6 +39,11 @@ $duration = DurationImmutable::hoursAndMinutes(1, 30); // From Carbon $duration = DurationImmutable::fromCarbon(\Carbon\CarbonInterval::hours(2)); + +// From a string (similar to Carbon::parse) +$duration = DurationImmutable::parse('1h 30m'); +$duration = DurationImmutable::parse('2 days'); +$duration = DurationImmutable::parse('PT1H30M'); ``` ### Accessing Units @@ -112,6 +117,18 @@ $duration->toShortHuman(); // "1d 2h 3m" // String conversion (string) $duration; // "02:03" (hh:mm) + +// JSON Serialization +$json = json_encode($duration); +/* +{ + "seconds": 5400, + "human": "1 hour 30 minutes", + "short_human": "1h 30m", + "formatted": "01:30", + "iso8601": "PT1H30M" +} +*/ ``` ### TimeDelta (Negative Durations) @@ -150,6 +167,15 @@ $task = Task::find(1); $task->duration_in_seconds; // Returns DurationImmutable instance ``` +## Development + +This package uses [pre-commit](https://pre-commit.com/) to maintain code quality. To set up your local development environment: + +1. Install pre-commit: `pip install pre-commit` (or your preferred method) +2. Install the git hooks: `pre-commit install --hook-type pre-commit --hook-type commit-msg` + +The hooks will automatically run PHPUnit and validate your commit messages against [Conventional Commits](https://www.conventionalcommits.org/). + ## Testing ```bash diff --git a/src/Casts/Days.php b/src/Casts/Days.php index f313b9b..7e5500a 100644 --- a/src/Casts/Days.php +++ b/src/Casts/Days.php @@ -1,5 +1,7 @@ totalSeconds / $seconds) * $seconds); $this->totalSeconds = (new self($seconds))->totalSeconds; diff --git a/src/DurationImmutable.php b/src/DurationImmutable.php index 53dc215..72db269 100644 --- a/src/DurationImmutable.php +++ b/src/DurationImmutable.php @@ -4,7 +4,7 @@ namespace AyupCreative\Duration; -final class DurationImmutable implements \JsonSerializable, DurationInterface +final class DurationImmutable implements \JsonSerializable, DurationInterface, Wireable { use Features\Arithmetic; use Features\Builders; @@ -13,6 +13,7 @@ final class DurationImmutable implements \JsonSerializable, DurationInterface use Features\Formatting; use Features\MagicProperties; use Features\TemporalUnits; + use Features\Wireable; protected int $totalSeconds; diff --git a/src/DurationInterface.php b/src/DurationInterface.php index 4a5fdb2..b7f10bf 100644 --- a/src/DurationInterface.php +++ b/src/DurationInterface.php @@ -1,5 +1,7 @@ totalSeconds); + } + /** * Create a duration from a CarbonInterval. * diff --git a/src/Features/Constants.php b/src/Features/Constants.php index 8d92198..ea451df 100644 --- a/src/Features/Constants.php +++ b/src/Features/Constants.php @@ -1,5 +1,7 @@ */ - public function jsonSerialize(): int + public function jsonSerialize(): array { - return $this->totalSeconds; + return [ + 'seconds' => $this->toSeconds(), + 'human' => $this->toHuman(), + 'short_human' => $this->toShortHuman(), + 'iso8601' => $this->toCarbonInterval()->spec(), + ]; } } diff --git a/src/Features/Formatting.php b/src/Features/Formatting.php index 8e584b3..847e699 100644 --- a/src/Features/Formatting.php +++ b/src/Features/Formatting.php @@ -1,5 +1,7 @@ $this->totalSeconds]; + } + + /** + * Create an instance from the value stored by Livewire. + * + * @param int|string $value + * @return static + */ + public static function fromLivewire($value): static + { + return static::seconds(...$value); + } +} diff --git a/src/TimeDelta.php b/src/TimeDelta.php index b49dc5b..94838ea 100644 --- a/src/TimeDelta.php +++ b/src/TimeDelta.php @@ -4,7 +4,7 @@ namespace AyupCreative\Duration; -final class TimeDelta implements \JsonSerializable, DurationInterface +final class TimeDelta implements \JsonSerializable, DurationInterface, Wireable { use Features\Arithmetic; use Features\Builders; @@ -13,6 +13,7 @@ final class TimeDelta implements \JsonSerializable, DurationInterface use Features\Formatting; use Features\MagicProperties; use Features\TemporalUnits; + use Features\Wireable; protected int $totalSeconds; diff --git a/src/Wireable.php b/src/Wireable.php new file mode 100644 index 0000000..23aa8af --- /dev/null +++ b/src/Wireable.php @@ -0,0 +1,20 @@ +assertEquals(86400, DurationImmutable::days(1)->totalSeconds()); $this->assertEquals(604800, DurationImmutable::weeks(1)->totalSeconds()); $this->assertEquals(2629800, DurationImmutable::months(1)->totalSeconds()); - $this->assertEquals(31557600, DurationImmutable::years(1)->totalSeconds()); + $this->assertEquals(31536000, DurationImmutable::years(1)->totalSeconds()); $this->assertEquals(3660, DurationImmutable::hoursAndMinutes(1, 1)->totalSeconds()); $this->assertEquals(90061, DurationImmutable::make(1, 1, 1, 1)->totalSeconds()); @@ -162,7 +164,7 @@ public function it_can_convert_to_various_units() // This test might fail due to bug in Conversion trait $this->assertEquals(3600 / 2629800, $duration->toMonths(), '', 0.00001); - $this->assertEquals(3600 / 31557600, $duration->toYears(), '', 0.00001); + $this->assertEquals(3600 / 31536000, $duration->toYears(), '', 0.00001); } /** @test */ @@ -367,7 +369,22 @@ public function it_can_be_converted_to_mutable() /** @test */ public function it_is_json_serializable() { - $duration = DurationImmutable::seconds(100); - $this->assertEquals(100, json_decode(json_encode($duration))); + $duration = DurationImmutable::seconds(3700); // 1h 1m 40s + $json = json_encode($duration); + $data = json_decode($json, true); + + $this->assertEquals(3700, $data['seconds']); + $this->assertEquals('1 hour 1 minute', $data['human']); + $this->assertEquals('1h 1m 40s', $data['short_human']); + $this->assertEquals('PT3700S', $data['iso8601']); + } + + /** @test */ + public function it_can_be_parsed_from_a_string() + { + $this->assertEquals(3600, DurationImmutable::parse('1h')->totalSeconds()); + $this->assertEquals(5400, DurationImmutable::parse('1h 30m')->totalSeconds()); + $this->assertEquals(172800, DurationImmutable::parse('2 days')->totalSeconds()); + $this->assertEquals(5400, DurationImmutable::parse('PT1H30M')->totalSeconds()); } } diff --git a/tests/DurationTest.php b/tests/DurationTest.php index 760c802..b71fa72 100644 --- a/tests/DurationTest.php +++ b/tests/DurationTest.php @@ -1,5 +1,7 @@ add(Duration::hours(5)); // 1d 5h $duration->ceilToDays(1); $this->assertEquals(172800, $duration->totalSeconds()); // 2d + + $duration = Duration::seconds(65); + $duration->ceilTo(0); + $this->assertEquals(65, $duration->totalSeconds()); } /** @test */ @@ -87,7 +93,7 @@ public function it_supports_additional_temporal_units() $this->assertEquals(26, $duration->totalHours()); $this->assertEquals(1563, $duration->totalMinutes()); $this->assertEquals(93784, $duration->totalSeconds()); - + $this->assertEquals(0, $duration->totalWeeks()); $this->assertEquals(0, $duration->totalMonths()); $this->assertEquals(0, $duration->totalYears()); @@ -109,4 +115,26 @@ public function it_supports_magic_properties() $this->assertEquals(0, $duration->totalMonths); $this->assertEquals(0, $duration->totalYears); } + + /** @test */ + public function it_is_json_serializable() + { + $duration = Duration::seconds(3700); // 1h 1m 40s + $json = json_encode($duration); + $data = json_decode($json, true); + + $this->assertEquals(3700, $data['seconds']); + $this->assertEquals('1 hour 1 minute', $data['human']); + $this->assertEquals('1h 1m 40s', $data['short_human']); + $this->assertEquals('PT3700S', $data['iso8601']); + } + + /** @test */ + public function it_can_be_parsed_from_a_string() + { + $this->assertEquals(3600, Duration::parse('1h')->totalSeconds()); + $this->assertEquals(5400, Duration::parse('1h 30m')->totalSeconds()); + $this->assertEquals(172800, Duration::parse('2 days')->totalSeconds()); + $this->assertEquals(5400, Duration::parse('PT1H30M')->totalSeconds()); + } } diff --git a/tests/TimeDeltaTest.php b/tests/TimeDeltaTest.php index d321ddb..9d17418 100644 --- a/tests/TimeDeltaTest.php +++ b/tests/TimeDeltaTest.php @@ -1,5 +1,7 @@