Full Code of tomloprod/time-warden for AI

main e9051e2c66ad cached
32 files
74.6 KB
20.2k tokens
75 symbols
1 requests
Download .txt
Repository: tomloprod/time-warden
Branch: main
Commit: e9051e2c66ad
Files: 32
Total size: 74.6 KB

Directory structure:
gitextract_0ftg2r_n/

├── .editorconfig
├── .gitattributes
├── .github/
│   ├── FUNDING.yml
│   └── workflows/
│       ├── formats.yml
│       └── tests.yml
├── .gitignore
├── CHANGELOG.md
├── CONTRIBUTING.md
├── LICENSE.md
├── README.md
├── composer.json
├── phpstan.neon.dist
├── phpunit.xml.dist
├── pint.json
├── rector.php
├── src/
│   ├── Concerns/
│   │   └── HasTasks.php
│   ├── Contracts/
│   │   └── Taskable.php
│   ├── Group.php
│   ├── Services/
│   │   └── TimeWardenManager.php
│   ├── Support/
│   │   ├── Console/
│   │   │   └── Table.php
│   │   ├── Facades/
│   │   │   └── TimeWarden.php
│   │   └── TimeWardenAlias.php
│   ├── Task.php
│   └── TimeWardenSummary.php
└── tests/
    ├── ArchTest.php
    ├── Contracts/
    │   └── TaskableTest.php
    ├── GroupTest.php
    ├── Services/
    │   └── TimeWardenManagerTest.php
    ├── Support/
    │   ├── Facades/
    │   │   └── TimeWardenTest.php
    │   └── TimeWardenAliasTest.php
    ├── TaskTest.php
    └── TimeWardenSummaryTest.php

================================================
FILE CONTENTS
================================================

================================================
FILE: .editorconfig
================================================
; This file is for unifying the coding style for different editors and IDEs.
; More information at http://editorconfig.org

root = true

[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 4
trim_trailing_whitespace = true

[*.md]
trim_trailing_whitespace = false

[*.yml]
indent_size = 2

================================================
FILE: .gitattributes
================================================
/docs              export-ignore
/tests             export-ignore
/scripts           export-ignore
/.github           export-ignore
/.php_cs           export-ignore
.editorconfig      export-ignore
.gitattributes     export-ignore
.gitignore         export-ignore
phpstan.neon.dist  export-ignore
phpunit.xml.dist   export-ignore
rector.php         export-ignore
CHANGELOG.md       export-ignore
CONTRIBUTING.md    export-ignore
README.md          export-ignore


================================================
FILE: .github/FUNDING.yml
================================================
custom: https://www.paypal.com/paypalme/tomloprod


================================================
FILE: .github/workflows/formats.yml
================================================
name: Formats

on: ['push', 'pull_request']

jobs:
  ci:
    runs-on: ${{ matrix.os }}

    strategy:
      fail-fast: true
      matrix:
        os: [ubuntu-latest]
        php: [8.2]
        dependency-version: [prefer-lowest, prefer-stable]

    name: Formats P${{ matrix.php }} - ${{ matrix.os }} - ${{ matrix.dependency-version }}

    steps:

    - name: Checkout
      uses: actions/checkout@v3

    - name: Setup PHP
      uses: shivammathur/setup-php@v2
      with:
        php-version: ${{ matrix.php }}
        extensions: dom, mbstring, zip
        coverage: pcov

    - name: Get Composer cache directory
      id: composer-cache
      shell: bash
      run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT

    - name: Cache dependencies
      uses: actions/cache@v3
      with:
        path: ${{ steps.composer-cache.outputs.dir }}
        key: dependencies-php-${{ matrix.php }}-os-${{ matrix.os }}-version-${{ matrix.dependency-version }}-composer-${{ hashFiles('composer.json') }}
        restore-keys: dependencies-php-${{ matrix.php }}-os-${{ matrix.os }}-version-${{ matrix.dependency-version }}-composer-

    - name: Install Composer dependencies
      run: composer update --${{ matrix.dependency-version }} --no-interaction --prefer-dist

    - name: Coding Style Checks
      run: composer test:lint

    - name: Type Checks
      run: composer test:types


================================================
FILE: .github/workflows/tests.yml
================================================
name: Tests

on: ['push', 'pull_request']

jobs:
  ci:
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: true
      matrix:
        os: [ubuntu-latest, macos-latest, windows-latest]
        php: [8.2, 8.3]
        dependency-version: [prefer-lowest, prefer-stable]

    name: Tests P${{ matrix.php }} - ${{ matrix.os }} - ${{ matrix.dependency-version }}

    steps:

    - name: Checkout
      uses: actions/checkout@v3

    - name: Setup PHP
      uses: shivammathur/setup-php@v2
      with:
        php-version: ${{ matrix.php }}
        extensions: dom, mbstring, zip
        coverage: none

    - name: Get Composer cache directory
      id: composer-cache
      shell: bash
      run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT

    - name: Cache dependencies
      uses: actions/cache@v3
      with:
        path: ${{ steps.composer-cache.outputs.dir }}
        key: dependencies-php-${{ matrix.php }}-os-${{ matrix.os }}-version-${{ matrix.dependency-version }}-composer-${{ hashFiles('composer.json') }}
        restore-keys: dependencies-php-${{ matrix.php }}-os-${{ matrix.os }}-version-${{ matrix.dependency-version }}-composer-

    - name: Install Composer dependencies
      run: composer update --${{ matrix.dependency-version }} --no-interaction --prefer-dist

    - name: Integration Tests
      run: php ./vendor/bin/pest


================================================
FILE: .gitignore
================================================
/.phpunit.result.cache
/.phpunit.cache
/.php-cs-fixer.cache
/.php-cs-fixer.php
/composer.lock
/phpunit.xml
/vendor/
*.swp
*.swo

================================================
FILE: CHANGELOG.md
================================================
## Version 1.0.1
> 20 May, 2024

- test: created task belongs to group (`getTaskable()`) by @tomloprod in https://github.com/tomloprod/time-warden/pull/1
- ref: replace task substitution system by @tomloprod in https://github.com/tomloprod/time-warden/pull/2

## Version 1.0.0
> 20 May, 2024

- First TimeWarden version


================================================
FILE: CONTRIBUTING.md
================================================
# 🧑‍🤝‍🧑 Contributing

Contributions are welcome, and are accepted via pull requests.
Please review these guidelines before submitting any pull requests.

## Process

1. Fork the project
1. Create a new branch
1. Code, test, commit and push
1. Open a pull request detailing your changes.

## Guidelines

Time warden uses a few tools to ensure the code quality and consistency. [Pest](https://pestphp.com) is the testing framework of choice, and we also use [PHPStan](https://phpstan.org) for static analysis. Pest's type coverage is at 100%, and the test suite is also at 100% coverage.

In terms of code style, we use [Laravel Pint](https://laravel.com/docs/11.x/pint) to ensure the code is consistent and follows the Laravel conventions. We also use [Rector](https://getrector.org) to ensure the code is up to date with the latest PHP version.

You run these tools individually using the following commands:

```bash
# Lint the code using Pint
composer lint
composer test:lint

# Refactor the code using Rector
composer refactor
composer test:refactor

# Run PHPStan
composer test:types

# Run the test suite
composer test:unit

# Run all the tools
composer test
```


================================================
FILE: LICENSE.md
================================================
The MIT License (MIT)

Copyright (c) Tomás López <tomloprod@gmail.com>

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.


================================================
FILE: README.md
================================================
<p align="center">
    <p align="center">
        <a href="https://github.com/tomloprod/time-warden/actions"><img alt="GitHub Workflow Status (master)" src="https://github.com/tomloprod/time-warden/actions/workflows/tests.yml/badge.svg"></a>
        <a href="https://packagist.org/packages/tomloprod/time-warden"><img alt="Total Downloads" src="https://img.shields.io/packagist/dt/tomloprod/time-warden"></a>
        <a href="https://packagist.org/packages/tomloprod/time-warden"><img alt="Latest Version" src="https://img.shields.io/packagist/v/tomloprod/time-warden"></a>
        <a href="https://packagist.org/packages/tomloprod/time-warden"><img alt="License" src="https://img.shields.io/packagist/l/tomloprod/time-warden"></a>
    </p>
</p>

------
## ⏱️ **About TimeWarden**

TimeWarden is a lightweight PHP library that allows you to **monitor the processing time of tasks** (*useful during the development stage and debugging*) and also lets you set estimated execution times for tasks, **enabling reactive actions** when tasks exceed their estimated duration.

TimeWarden uses **high-resolution timing** (`hrtime`) for **nanosecond precision**, ensuring accurate measurements even for very fast operations.

TimeWarden is framework-agnostic, meaning it's not exclusive to any particular framework. It can seamlessly integrate into any PHP application, whether they utilize frameworks like Laravel (🧡), Symfony, or operate without any framework at all.

## **✨ Getting Started**

### Reactive Actions
You can specify an estimated execution time for each task and set an action to be performed when the time is exceeded (*example: send an email, add an entry to the error log, etc.*).

#### Example
```php
timeWarden()->task('Checking articles')->start();

foreach ($articles as $article) {
    // Perform long process... 🕒 
}

// Using traditional anonymous function
timeWarden()->stop(static function (Task $task): void {
    $task->onExceedsMilliseconds(500, static function (Task $task): void {
        // Do what you need, for example, send an email 🙂
        Mail::to('foo@bar.com')->queue(
            new SlowArticleProcess($task)
        );
    });
});

// Or using an arrow function
timeWarden()->stop(static function (Task $task): void {
    $task->onExceedsMilliseconds(500, fn (Task $task) => Log::error($task->name.' has taken too long'));
});
```

#### Available methods

If you're not convinced about using `onExceedsMilliseconds`, you have other options:
```php
$task->onExceedsSeconds(10, function () { ... });
$task->onExceedsMinutes(5, function () { ... });
$task->onExceedsHours(2, function () { ... });
```

### Quick Measurement
TimeWarden provides a convenient `measure()` method that automatically handles task creation, starting, and stopping for you. This method executes a callable and returns the execution time in milliseconds.

#### Example
```php
// Simple measurement
$duration = timeWarden()->measure(function() {
    // Your code here
    sleep(1);
    processData();
});

echo "Execution took: {$duration} ms";

// With custom task name
$duration = timeWarden()->measure(function() {
    // Your code here
    processArticles();
}, 'Processing Articles');

// The task will appear in your TimeWarden output with the specified name
echo timeWarden()->output();
```

The `measure()` method:
- Automatically creates a task with the provided name (or 'callable' by default)
- Starts timing before execution
- Executes your callable
- Stops timing after execution (even if an exception occurs)
- Returns the duration in milliseconds
- Integrates with groups and the TimeWarden workflow

### Execution Time Debugging
It allows you to measure the execution time of tasks in your application, as well as the possibility of adding those tasks to a group.

#### Simple tasks

```php
timeWarden()->task('Articles task');

foreach ($articles as $article) {
    // Perform long process...
}

// Previous task is automatically stopped when a new task is created
timeWarden()->task('Customers task');

foreach ($customers as $customer) {
    // Perform long process...
}

/**
 * You can print the results directly or obtain a 
 * summary with the `getSummary()` method
 */
echo timeWarden()->output();
```
**Result:**
```log
╔═════════════════════ TIMEWARDEN ═════╤═══════════════╗
║ GROUP               │ TASK           │ DURATION (MS) ║
╠═════════════════════╪════════════════╪═══════════════╣
║ default (320.37 ms) │ Articles task  │ 70.23         ║
║                     │ Customers task │ 250.14        ║
╚══════════════════ Total: 320.37 ms ══╧═══════════════╝
```

#### Grouped tasks

```php
timeWarden()->group('Articles')->task('Loop of articles')->start();

foreach ($articles as $article) {
    // Perform first operations
}

timeWarden()->task('Other articles process')->start();
Foo::bar();

// Previous task is automatically stopped when a new task is created
timeWarden()->group('Customers')->task('Customers task')->start();

foreach ($customers as $customer) {
    // Perform long process...
}

timeWarden()->task('Other customer process')->start();
Bar::foo();

/**
 * You can print the results directly or obtain a 
 * summary with the `getSummary()` method
 */
echo timeWarden()->output();
```
**Result:**
```log
╔═══════════════════════╤══ TIMEWARDEN ══════════╤═══════════════╗
║ GROUP                 │ TASK                   │ DURATION (MS) ║
╠═══════════════════════╪════════════════════════╪═══════════════╣
║ Articles (85.46 ms)   │ Loop of articles       │ 70.24         ║
║                       │ Other articles process │ 15.22         ║
╟───────────────────────┼────────────────────────┼───────────────╢
║ Customers (280.46 ms) │ Customers task         │ 250.22        ║
║                       │ Other customer process │ 30.24         ║
╚═══════════════════════ Total: 365.92 ms ═══════╧═══════════════╝
```

#### 🧙 Tip

If your application has any logging system, it would be a perfect place to send the output. 
```php 
if (app()->environment('local')) {
    Log::debug(timeWarden()->output());
}
```

### Ways of using TimeWarden
You can use TimeWarden either with the aliases `timeWarden()` (or `timewarden()`):
```php
timeWarden()->task('Task 1')->start();
```

or by directly invoking the static methods of the `TimeWarden` facade:

```php
TimeWarden::task('Task 1')->start();
```
You decide how to use it 🙂

## **🧱 Architecture**
TimeWarden is composed of several types of elements. Below are some features of each of these elements.

### `TimeWarden`

`Tomloprod\TimeWarden\Support\Facades\TimeWarden` is a facade that acts as a simplified interface for using the rest of the TimeWarden elements.

#### Methods
Most methods in this class return their own instance, allowing fluent syntax through method chaining.

```php
// Destroys the TimeWarden instance and returns a new one.
TimeWarden::reset(): TimeWarden

// Creates a new group.
TimeWarden::group(string $groupName): TimeWarden

/**
 * Creates a new task inside the last created group
 * or within the TimeWarden instance itself.
 */
TimeWarden::task(string $taskName): TimeWarden

// Starts the last created task
TimeWarden::start(): TimeWarden

// Stops the last created task
TimeWarden::stop(): TimeWarden

// Measures the execution time of a callable and returns duration in milliseconds
TimeWarden::measure(callable $fn, ?string $taskName = null): float

// Obtains all the created groups
TimeWarden::getGroups(): array

/**
 * It allows you to obtain a TimeWardenSummary instance, 
 * which is useful for getting a summary of all groups 
 * and tasks generated by TimeWarden. 
 * 
 * Through that instance, you can retrieve the summary 
 * in array or string (JSON) format.
 */
TimeWarden::getSummary(): TimeWardenSummary

/**
 * Returns a table with execution time debugging info 
 * (ideal for displaying in the console).
 */
TimeWarden::output(): string
```
Additionally, it has all the methods of the [Taskable](#taskable) interface.

### `Task`
All tasks you create are instances of `Tomloprod\TimeWarden\Task`.
The most useful methods and properties of a task are the following:

#### Properties
- `name`

#### Methods
```php
$task = new Task('Task 1');

$task->start(): void
$task->stop(?callable $fn = null): void

// Returns the duration of the task in a human-readable format. Example: *1day 10h 20min 30sec 150ms*
$task->getFriendlyDuration(): string
// Returns the duration of the task in milliseconds
$task->getDuration(): float

// Returns the taskable element to which the task belongs.
$task->getTaskable(): ?Taskable

$task->hasStarted(): bool
$task->hasEnded(): bool

$task->getStartDateTime(): ?DateTimeImmutable
$task->getEndDateTime(): ?DateTimeImmutable

// Returns the start and end timestamps in nanoseconds (high precision)
$task->getStartTimestamp(): int
$task->getEndTimestamp(): int

/** @return array<string, mixed> */
$task->toArray(): array

// Reactive execution time methods
$task->onExceedsMilliseconds(float $milliseconds, callable $fn): ?Task
$task->onExceedsSeconds(float $seconds, callable $fn): ?Task
$task->onExceedsMinutes(float $minutes, callable $fn): ?Task
$task->onExceedsHours(float $hours, callable $fn): ?Task
```

### `Group`
All groups you create are instances of the `Tomloprod\TimeWarden\Group` object.
The most useful methods and properties of a group are the following:

#### Properties
- `name`

#### Methods
```php

// Starts the last created task inside this group
$group->start(): void
```
Additionally, it has all the methods of the [Taskable](#taskable) interface.

### `Taskable`
`Tomloprod\TimeWarden\Contracts\Taskable` is the interface used by the **TimeWarden** instance as well as by each task **group**

#### Methods
```php
// Create a new task within the taskable.
$taskable->createTask(string $taskName): Task

$taskable->getTasks(): array

$taskable->getLastTask(): ?Task

// Return the total time in milliseconds of all tasks within the taskable.
$taskable->getDuration(): float

$taskable->toArray(): array

$taskable->toJson(): string
```

### `TimeWardenSummary`
`Tomloprod\TimeWarden\TimeWardenSummary` is a class that allows obtaining a general summary of groups and their tasks generated with TimeWarden.

It is useful for obtaining a summary in array or string (JSON) format.

You can obtain an instance of `TimeWardenSummary` as follows:
```php
/** @var Tomloprod\TimeWarden\TimeWardenSummary $timeWardenSummary */
$timeWardenSummary = timeWarden()->getSummary();
```

#### Methods
```php

$timeWardenSummary->toArray(): array
$timeWardenSummary->toJson(): string
```

Here is an example of the data returned in array format:

```php
$summaryArray = [
    [
        'name' => 'default',
        'duration' => 42.0,
        'tasks' => [
            [
                'name' => 'TaskName1',
                'duration' => 19.0,
                'friendly_duration' => '19ms',
                'start_timestamp' => 1496664000000000000, // nanoseconds
                'end_timestamp' => 1496664000019000000,   // nanoseconds
                'start_datetime' => '2017-06-05T12:00:00+00:00',
                'end_datetime' => '2017-06-05T12:00:00+00:00',
            ],
            [
                'name' => 'TaskName2',
                'duration' => 23.0,
                'friendly_duration' => '23ms',
                'start_timestamp' => 1496664000000000000, // nanoseconds
                'end_timestamp' => 1496664000023000000,   // nanoseconds
                'start_datetime' => '2017-06-05T12:00:00+00:00',
                'end_datetime' => '2017-06-05T12:00:00+00:00',
            ],
        ],
    ],
    [ // Others groups... ],
];
```

## **🚀 Installation & Requirements**

> **Requires [PHP 8.2+](https://php.net/releases/)**

You may use [Composer](https://getcomposer.org) to install TimeWarden into your PHP project:

```bash
composer require tomloprod/time-warden
```

## **🧑‍🤝‍🧑 Contributing**

Contributions are welcome, and are accepted via pull requests.
Please [review these guidelines](./CONTRIBUTING.md) before submitting any pull requests.

------

**TimeWarden** was created by **[Tomás López](https://twitter.com/tomloprod)** and open-sourced under the **[MIT license](https://opensource.org/licenses/MIT)**.


================================================
FILE: composer.json
================================================
{
    "name": "tomloprod/time-warden",
    "description": "TimeWarden is a lightweight PHP library that enables you to monitor the processing time of tasks and task groups (useful during the development stage). Additionally, it allows you to set maximum execution times to tasks, empowering proactive actions when tasks exceed their planned duration.",
    "type": "library",
    "keywords": [
        "tomloprod",
        "time-warden",
        "execution time",
        "debugging",
        "monitoring",
        "performance"
    ],
    "license": "MIT",
    "authors": [
        {
            "name": "Tomás López",
            "email": "tomloprod@gmail.com"
        }
    ],
    "require": {
        "php": "^8.2.0"
    },
    "require-dev": {
        "laravel/pint": "^1.22.1",
        "pestphp/pest": "^3.8.2",
        "pestphp/pest-plugin-type-coverage": "^3.5.0",
        "rector/rector": "^1.1.0"
    },
    "autoload": {
        "psr-4": {
            "Tomloprod\\TimeWarden\\": "src/"
        },
        "files": [
            "src/Support/TimeWardenAlias.php"
        ]
    },
    "autoload-dev": {
        "psr-4": {
            "Tests\\": "tests/"
        }
    },
    "minimum-stability": "dev",
    "prefer-stable": true,
    "config": {
        "sort-packages": true,
        "preferred-install": "dist",
        "allow-plugins": {
            "pestphp/pest-plugin": true
        }
    },
    "scripts": {
        "lint": "pint",
        "refactor": "rector",
        "test:lint": "pint --test",
        "test:refactor": "rector --dry-run",
        "test:types": "phpstan analyse",
        "test:type-coverage": "pest --type-coverage --min=100",
        "test:unit": "pest --coverage --min=100",
        "test": [
            "@test:lint",
            "@test:refactor",
            "@test:types",
            "@test:type-coverage",
            "@test:unit"
        ]
    }
}


================================================
FILE: phpstan.neon.dist
================================================
parameters:
    level: max
    paths:
        - src

    reportUnmatchedIgnoredErrors: true


================================================
FILE: phpunit.xml.dist
================================================
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.0/phpunit.xsd"
    colors="true"
    cacheDirectory=".phpunit.cache">
    <source>
        <include>
            <directory suffix=".php">./src</directory>
        </include>
    </source>
    <testsuites>
        <testsuite name="default">
            <directory suffix=".php">./tests</directory>
        </testsuite>
    </testsuites>
</phpunit>


================================================
FILE: pint.json
================================================
{
    "preset": "laravel",
    "rules": {
        "array_push": true,
        "backtick_to_shell_exec": true,
        "date_time_immutable": true,
        "declare_strict_types": true,
        "lowercase_keywords": true,
        "lowercase_static_reference": true,
        "final_class": true,
        "final_internal_class": true,
        "final_public_method_for_abstract_class": true,
        "fully_qualified_strict_types": true,
        "global_namespace_import": {
            "import_classes": true,
            "import_constants": true,
            "import_functions": true
        },
        "mb_str_functions": true,
        "modernize_types_casting": true,
        "new_with_parentheses": false,
        "no_superfluous_elseif": true,
        "no_useless_else": true,
        "no_multiple_statements_per_line": true,
        "ordered_class_elements": {
            "order": [
                "use_trait",
                "case",
                "constant",
                "constant_public",
                "constant_protected",
                "constant_private",
                "property_public",
                "property_protected",
                "property_private",
                "construct",
                "destruct",
                "magic",
                "phpunit",
                "method_abstract",
                "method_public_static",
                "method_public",
                "method_protected_static",
                "method_protected",
                "method_private_static",
                "method_private"
            ],
            "sort_algorithm": "none"
        },
        "ordered_interfaces": true,
        "ordered_traits": true,
        "protected_to_private": true,
        "self_accessor": true,
        "self_static_accessor": true,
        "strict_comparison": true,
        "visibility_required": true
    }
}

================================================
FILE: rector.php
================================================
<?php

declare(strict_types=1);

use Rector\Config\RectorConfig;

return RectorConfig::configure()
    ->withPaths([
        __DIR__.'/src',
        __DIR__.'/tests',
    ])
    ->withSkip([])
    ->withPreparedSets(
        deadCode: true,
        codeQuality: true,
        typeDeclarations: true,
        privatization: true,
        earlyReturn: true,
        strictBooleans: true,
    )
    ->withPhpSets();


================================================
FILE: src/Concerns/HasTasks.php
================================================
<?php

declare(strict_types=1);

namespace Tomloprod\TimeWarden\Concerns;

use Tomloprod\TimeWarden\Task;

trait HasTasks
{
    /**
     * @var array<Task>
     */
    private array $tasks = [];

    public function createTask(string $taskName): Task
    {
        $task = new Task($taskName, $this);

        $this->tasks[] = $task;

        return $task;
    }

    /**
     * @return array<Task>
     */
    public function getTasks(): array
    {
        return $this->tasks;
    }

    /**
     * @return float The duration time in milliseconds
     */
    public function getDuration(): float
    {
        $duration = 0.0;

        /** @var Task $task */
        foreach ($this->getTasks() as $task) {
            $duration += $task->getDuration();
        }

        return $duration;
    }

    public function getLastTask(): ?Task
    {
        /** @var Task|bool $lastTask */
        $lastTask = end($this->tasks);

        return ($lastTask instanceof Task) ? $lastTask : null;
    }

    public function toArray(): array
    {
        /** @var array<string, mixed> $tasksInfo */
        $tasksInfo = [];

        /** @var Task $task */
        foreach ($this->getTasks() as $task) {
            $tasksInfo[] = $task->toArray();
        }

        return [
            'name' => $this->name,
            'duration' => $this->getDuration(),
            'tasks' => $tasksInfo,
        ];
    }

    public function toJson(): string
    {
        $json = json_encode($this->toArray());

        return ($json === false) ? '[]' : $json;
    }
}


================================================
FILE: src/Contracts/Taskable.php
================================================
<?php

declare(strict_types=1);

namespace Tomloprod\TimeWarden\Contracts;

use Tomloprod\TimeWarden\Task;

interface Taskable
{
    public function createTask(string $taskName): Task;

    /**
     * @return array<Task>
     */
    public function getTasks(): array;

    public function getLastTask(): ?Task;

    public function getDuration(): float;

    /** @return array<string, mixed> */
    public function toArray(): array;

    public function toJson(): string;
}


================================================
FILE: src/Group.php
================================================
<?php

declare(strict_types=1);

namespace Tomloprod\TimeWarden;

use Tomloprod\TimeWarden\Concerns\HasTasks;
use Tomloprod\TimeWarden\Contracts\Taskable;

final class Group implements Taskable
{
    use HasTasks;

    public function __construct(public string $name) {}

    public function start(): void
    {
        /** @var Task|null $lastTask */
        $lastTask = $this->getLastTask();

        if ($lastTask instanceof Task) {
            $lastTask->start();
        }
    }
}


================================================
FILE: src/Services/TimeWardenManager.php
================================================
<?php

declare(strict_types=1);

namespace Tomloprod\TimeWarden\Services;

use Exception;
use Tomloprod\TimeWarden\Concerns\HasTasks;
use Tomloprod\TimeWarden\Contracts\Taskable;
use Tomloprod\TimeWarden\Group;
use Tomloprod\TimeWarden\Support\Console\Table;
use Tomloprod\TimeWarden\Task;
use Tomloprod\TimeWarden\TimeWardenSummary;

final class TimeWardenManager implements Taskable
{
    use HasTasks;

    public string $name = 'default';

    private static TimeWardenManager $instance;

    /**
     * @var array<Group>
     */
    private array $groups = [];

    private function __construct() {}

    public function __clone()
    {
        throw new Exception('Cannot clone singleton');
    }

    public function __wakeup()
    {
        throw new Exception('Cannot unserialize singleton');
    }

    /**
     * Get the singleton instance of TimeWarden.
     */
    public static function instance(): self
    {
        if (! isset(self::$instance)) {
            self::$instance = new self();
        }

        return self::$instance;
    }

    public function reset(): self
    {
        self::$instance = new self();

        return self::$instance;
    }

    public function group(string $groupName): self
    {
        $this->stop();

        $group = $this->getLastGroup();
        if ($group && ! $group->getLastTask() instanceof Task) {
            $group->name = $groupName;
        } else {
            $this->groups[] = new Group($groupName);
        }

        return self::$instance;
    }

    public function task(string $taskName): self
    {
        /** @var Taskable $taskable */
        $taskable = $this->getActiveTaskable();

        /** @var Task|null $lastTask */
        $lastTask = $taskable->getLastTask();

        // If the last task was never started, we replace its name with `$taskName`
        if ($lastTask instanceof Task && ! $lastTask->hasStarted()) {
            $lastTask->name = $taskName;
        } else {
            // If there is a task, but it has already started, we stop it
            if ($lastTask instanceof Task && $lastTask->hasStarted()) {
                $lastTask->stop();
            }

            // And add the task to the taskable.
            $taskable->createTask($taskName);
        }

        return self::$instance;
    }

    public function start(): self
    {
        /** @var Task|null $lastTask */
        $lastTask = $this->getActiveTaskable()->getLastTask();

        if ($lastTask instanceof Task) {
            $lastTask->start();
        }

        return self::$instance;
    }

    public function stop(?callable $fn = null): self
    {
        /** @var Task|null $lastTask */
        $lastTask = $this->getActiveTaskable()->getLastTask();

        if ($lastTask instanceof Task) {
            $lastTask->stop($fn);
        }

        return self::$instance;
    }

    /**
     * Measure the execution time of a callable
     *
     * @param  callable  $fn  The callable to measure
     * @param  string  $taskName  The task name. If not provided, will use 'callable' as default.
     * @return float The duration time in milliseconds
     */
    public function measure(callable $fn, string $taskName = 'callable'): float
    {
        // Create task and start
        $this->task($taskName)->start();

        try {
            $fn();
        } finally {
            $this->stop();
        }

        // Get the duration from the last task
        $lastTask = $this->getActiveTaskable()->getLastTask();

        return $lastTask instanceof Task ? $lastTask->getDuration() : 0.0;
    }

    /**
     * @return array<Group>
     */
    public function getGroups(): array
    {
        return $this->groups;
    }

    public function getSummary(): TimeWardenSummary
    {
        $this->stop();

        return new TimeWardenSummary();
    }

    public function output(): string
    {
        $this->stop();

        /** @var string $output */
        $output = '';

        /** @var array<string> $columns */
        $columns = [
            'GROUP',
            'TASK',
            'DURATION (MS)',
        ];

        /** @var array<string|float> $rows */
        $rows = [];

        $totalGroups = 0;
        $totalTasks = 0;
        $totalDuration = $this->getDuration();

        /** @var Task $task */
        foreach ($this->getTasks() as $iTask => $task) {
            $rows[] = [
                ($iTask === 0) ? 'default ('.$this->getDuration().' ms)' : '',
                $task->name,
                $task->getDuration(),
            ];

            $totalTasks++;
        }

        if ($totalTasks > 0) {
            $rows[] = Table::separator();
        }

        /** @var Group|null $lastIterateGroup */
        $lastIterateGroup = null;

        /** @var Group $group */
        foreach ($this->groups as $iGroup => $group) {

            /** @var Task $task */
            foreach ($group->getTasks() as $task) {
                $rows[] = [
                    ($lastIterateGroup !== $group) ? $group->name.' ('.$group->getDuration().' ms)' : '',
                    $task->name,
                    $task->getDuration(),
                ];

                $lastIterateGroup = $group;
                $totalTasks++;
            }

            if ($iGroup !== count($this->groups) - 1) {
                $rows[] = Table::separator();
            }

            $totalDuration += $group->getDuration();
            $totalGroups++;
        }

        $output = (new Table())
            ->setHeaders($columns)
            ->setRows($rows)
            ->setStyle('box-double')
            ->setFooterTitle('Total: '.$totalDuration.' ms')
            ->setHeaderTitle('TIMEWARDEN')
            ->render();

        return PHP_EOL.$output;
    }

    private function getActiveTaskable(): Taskable
    {
        return $this->getLastGroup() ?? $this;
    }

    private function getLastGroup(): ?Group
    {
        /** @var Group|bool $lastGroup */
        $lastGroup = end($this->groups);

        return ($lastGroup instanceof Group) ? $lastGroup : null;
    }
}


================================================
FILE: src/Support/Console/Table.php
================================================
<?php

declare(strict_types=1);

namespace Tomloprod\TimeWarden\Support\Console;

/**
 * @codeCoverageIgnore
 */
final class Table
{
    private const TABLE_SEPARATOR = '---SEPARATOR---';

    private const STYLES = [
        'default' => [
            'top_left' => '+',
            'top_mid' => '+',
            'top_right' => '+',
            'mid_left' => '+',
            'mid_mid' => '+',
            'mid_right' => '+',
            'bottom_left' => '+',
            'bottom_mid' => '+',
            'bottom_right' => '+',
            'horizontal' => '-',
            'vertical' => '|',
        ],
        'box-double' => [
            'top_left' => '╔',
            'top_mid' => '╦',
            'top_right' => '╗',
            'mid_left' => '╠',
            'mid_mid' => '╬',
            'mid_right' => '╣',
            'bottom_left' => '╚',
            'bottom_mid' => '╩',
            'bottom_right' => '╝',
            'horizontal' => '═',
            'vertical' => '║',
            'column_separator' => '║',
            'separator_left' => '╠',
            'separator_mid' => '╬',
            'separator_right' => '╣',
            'separator_horizontal' => '═',
        ],
    ];

    /**
     * @var array<string>
     */
    private array $headers = [];

    /**
     * @var array<mixed>
     */
    private array $rows = [];

    private string $style = 'default';

    private string $headerTitle = '';

    private string $footerTitle = '';

    public static function separator(): string
    {
        return self::TABLE_SEPARATOR;
    }

    /**
     * @param  array<string>  $headers
     */
    public function setHeaders(array $headers): self
    {
        $this->headers = $headers;

        return $this;
    }

    /**
     * @param  array<mixed>  $rows
     */
    public function setRows(array $rows): self
    {
        $this->rows = $rows;

        return $this;
    }

    public function setStyle(string $style): self
    {
        $this->style = $style;

        return $this;
    }

    public function setHeaderTitle(string $title): self
    {
        $this->headerTitle = $title;

        return $this;
    }

    public function setFooterTitle(string $title): self
    {
        $this->footerTitle = $title;

        return $this;
    }

    public function render(): string
    {
        if ($this->headers === [] && $this->rows === []) {
            return '';
        }

        $output = [];

        $styleChars = self::STYLES[$this->style] ?? self::STYLES['default'];

        // Calculate column widths
        $columnWidths = $this->calculateColumnWidths();

        // Top border
        $output[] = $this->renderTopBorder($columnWidths, $styleChars);

        // Headers
        if ($this->headers !== []) {
            $output[] = $this->renderRow($this->headers, $columnWidths, $styleChars);
            $output[] = $this->renderHeaderSeparator($columnWidths, $styleChars);
        }

        // Data rows
        foreach ($this->rows as $row) {
            if ($row === self::TABLE_SEPARATOR) {
                $output[] = $this->renderSeparator($columnWidths, $styleChars);
            } elseif (is_array($row)) {
                $output[] = $this->renderRow($row, $columnWidths, $styleChars);
            }
        }

        // Bottom border
        $output[] = $this->renderBottomBorder($columnWidths, $styleChars);

        return implode(PHP_EOL, $output);
    }

    /**
     * @return array<int>
     */
    private function calculateColumnWidths(): array
    {
        $widths = [];

        // Initialize with headers
        foreach ($this->headers as $i => $header) {
            $widths[$i] = mb_strlen((string) $header);
        }

        // Consider row content
        foreach ($this->rows as $row) {
            if ($row !== self::TABLE_SEPARATOR && is_array($row)) {
                foreach ($row as $i => $cell) {
                    $cellLength = mb_strlen((string) $cell);
                    $widths[$i] = max($widths[$i] ?? 0, $cellLength);
                }
            }
        }

        return $widths;
    }

    /**
     * @param  array<int>  $columnWidths
     * @param  array<string, string>  $styleChars
     */
    private function renderTopBorder(array $columnWidths, array $styleChars): string
    {
        $line = '';

        if ($this->headerTitle !== '') {
            // Create line with vertical separators crossing the title
            $line .= $styleChars['top_left'];

            // Calculate separator positions
            $currentPos = 0;
            $separatorPositions = [];
            foreach ($columnWidths as $i => $width) {
                $currentPos += $width + 2; // +2 for spaces
                if ($i < count($columnWidths) - 1) {
                    $separatorPositions[] = $currentPos;
                    $currentPos += 1; // +1 for separator
                }
            }

            $totalWidth = $currentPos;
            $titleWithSpaces = ' '.$this->headerTitle.' ';
            $titleLength = mb_strlen($titleWithSpaces);

            if ($titleLength <= $totalWidth) {
                $remainingWidth = $totalWidth - $titleLength;
                $leftPadding = (int) ($remainingWidth / 2);
                $rightPadding = $remainingWidth - $leftPadding;

                // Build line character by character
                for ($pos = 0; $pos < $totalWidth; $pos++) {
                    if (in_array($pos, $separatorPositions)) {
                        $line .= $styleChars['top_mid'];
                    } elseif ($pos >= $leftPadding && $pos < $leftPadding + $titleLength) {
                        $titleIndex = $pos - $leftPadding;
                        $line .= $titleWithSpaces[$titleIndex] ?? $styleChars['horizontal'];
                    } else {
                        $line .= $styleChars['horizontal'];
                    }
                }
            } else {
                // If title is too long, use normal format
                foreach ($columnWidths as $i => $width) {
                    $line .= str_repeat($styleChars['horizontal'], $width + 2); // +2 for spaces
                    if ($i < count($columnWidths) - 1) {
                        $line .= $styleChars['top_mid'];
                    }
                }
            }

            $line .= $styleChars['top_right'];
        } else {
            $line .= $styleChars['top_left'];
            foreach ($columnWidths as $i => $width) {
                $line .= str_repeat($styleChars['horizontal'], $width + 2); // +2 for spaces
                if ($i < count($columnWidths) - 1) {
                    $line .= $styleChars['top_mid'];
                }
            }
            $line .= $styleChars['top_right'];
        }

        return $line;
    }

    /**
     * @param  array<int>  $columnWidths
     * @param  array<string, string>  $styleChars
     */
    private function renderHeaderSeparator(array $columnWidths, array $styleChars): string
    {
        $line = $styleChars['mid_left'];
        foreach ($columnWidths as $i => $width) {
            $line .= str_repeat($styleChars['horizontal'], $width + 2); // +2 for spaces
            if ($i < count($columnWidths) - 1) {
                $line .= $styleChars['mid_mid'];
            }
        }

        return $line.$styleChars['mid_right'];
    }

    /**
     * @param  array<int>  $columnWidths
     * @param  array<string, string>  $styleChars
     */
    private function renderSeparator(array $columnWidths, array $styleChars): string
    {
        $separatorLeft = $styleChars['separator_left'] ?? $styleChars['mid_left'];
        $separatorMid = $styleChars['separator_mid'] ?? $styleChars['mid_mid'];
        $separatorRight = $styleChars['separator_right'] ?? $styleChars['mid_right'];
        $separatorHorizontal = $styleChars['separator_horizontal'] ?? $styleChars['horizontal'];

        $line = $separatorLeft;
        foreach ($columnWidths as $i => $width) {
            $line .= str_repeat($separatorHorizontal, $width + 2); // +2 for spaces
            if ($i < count($columnWidths) - 1) {
                $line .= $separatorMid;
            }
        }

        return $line.$separatorRight;
    }

    /**
     * @param  array<mixed>  $row
     * @param  array<int>  $columnWidths
     * @param  array<string, string>  $styleChars
     */
    private function renderRow(array $row, array $columnWidths, array $styleChars): string
    {
        $columnSeparator = $styleChars['column_separator'] ?? $styleChars['vertical'];

        $line = $styleChars['vertical']; // Left border (double)
        foreach ($columnWidths as $i => $width) {
            $cellValue = $row[$i] ?? '';
            $cell = is_scalar($cellValue) ? (string) $cellValue : '';
            $cellPadding = $width - mb_strlen($cell);
            $line .= ' '.$cell.str_repeat(' ', $cellPadding);
            if ($i < count($columnWidths) - 1) {
                $line .= ' '.$columnSeparator; // Separator between columns
            }
        } // Right border (double)

        return $line.(' '.$styleChars['vertical']);
    }

    /**
     * @param  array<int>  $columnWidths
     * @param  array<string, string>  $styleChars
     */
    private function renderBottomBorder(array $columnWidths, array $styleChars): string
    {
        $line = '';

        if ($this->footerTitle !== '') {
            // Create line with vertical separators crossing the title
            $line .= $styleChars['bottom_left'];

            // Calculate separator positions
            $currentPos = 0;
            $separatorPositions = [];
            foreach ($columnWidths as $i => $width) {
                $currentPos += $width + 2; // +2 for spaces
                if ($i < count($columnWidths) - 1) {
                    $separatorPositions[] = $currentPos;
                    $currentPos += 1; // +1 for separator
                }
            }

            $totalWidth = $currentPos;
            $titleWithSpaces = ' '.$this->footerTitle.' ';
            $titleLength = mb_strlen($titleWithSpaces);

            if ($titleLength <= $totalWidth) {
                $remainingWidth = $totalWidth - $titleLength;
                $leftPadding = (int) ($remainingWidth / 2);
                $rightPadding = $remainingWidth - $leftPadding;

                // Build line character by character
                for ($pos = 0; $pos < $totalWidth; $pos++) {
                    if (in_array($pos, $separatorPositions)) {
                        $line .= $styleChars['bottom_mid'];
                    } elseif ($pos >= $leftPadding && $pos < $leftPadding + $titleLength) {
                        $titleIndex = $pos - $leftPadding;
                        $line .= $titleWithSpaces[$titleIndex] ?? $styleChars['horizontal'];
                    } else {
                        $line .= $styleChars['horizontal'];
                    }
                }
            } else {
                // If title is too long, use normal format
                foreach ($columnWidths as $i => $width) {
                    $line .= str_repeat($styleChars['horizontal'], $width + 2); // +2 for spaces
                    if ($i < count($columnWidths) - 1) {
                        $line .= $styleChars['bottom_mid'];
                    }
                }
            }

            $line .= $styleChars['bottom_right'];
        } else {
            $line .= $styleChars['bottom_left'];
            foreach ($columnWidths as $i => $width) {
                $line .= str_repeat($styleChars['horizontal'], $width + 2); // +2 for spaces
                if ($i < count($columnWidths) - 1) {
                    $line .= $styleChars['bottom_mid'];
                }
            }
            $line .= $styleChars['bottom_right'];
        }

        return $line;
    }
}


================================================
FILE: src/Support/Facades/TimeWarden.php
================================================
<?php

declare(strict_types=1);

namespace Tomloprod\TimeWarden\Support\Facades;

use Tomloprod\TimeWarden\Group;
use Tomloprod\TimeWarden\Services\TimeWardenManager;
use Tomloprod\TimeWarden\Task;

/**
 * @method static TimeWardenManager reset()
 * @method static TimeWardenManager group(string $groupName)
 * @method static TimeWardenManager task(string $taskName)
 * @method static TimeWardenManager start()
 * @method static Task|null stop()
 * @method static float measure(callable $fn, ?string $taskName = null)
 * @method static array<Group> getGroups()
 * @method static string output()
 *
 * Taskable methods:
 * @method static Task createTask(string $taskName)
 * @method static array<Task> getTasks(string $taskName)
 * @method static Task|null getLastTask()
 * @method static float getDuration()
 */
final class TimeWarden
{
    /**
     * @param  array<mixed>  $args
     */
    public static function __callStatic(string $method, array $args): mixed
    {
        $instance = TimeWardenManager::instance();

        return $instance->$method(...$args);
    }
}


================================================
FILE: src/Support/TimeWardenAlias.php
================================================
<?php

declare(strict_types=1);

use Tomloprod\TimeWarden\Services\TimeWardenManager;

if (! function_exists('timeWarden')) {
    function timeWarden(): TimeWardenManager
    {
        return TimeWardenManager::instance();
    }
}

if (! function_exists('timewarden')) {
    function timewarden(): TimeWardenManager
    {
        return TimeWardenManager::instance();
    }
}


================================================
FILE: src/Task.php
================================================
<?php

declare(strict_types=1);

namespace Tomloprod\TimeWarden;

use DateTime;
use DateTimeImmutable;
use Tomloprod\TimeWarden\Contracts\Taskable;

final class Task
{
    /**
     * Start time in nanoseconds
     */
    private int $startTimestamp = 0;

    /**
     * End time in nanoseconds
     */
    private int $endTimestamp = 0;

    public function __construct(public string $name, private readonly ?Taskable $taskable = null) {}

    public function start(): void
    {
        if (! $this->hasStarted()) {
            /**
             * Force garbage collection before timing starts to ensure accurate
             * measurements. This prevents random GC cycles from affecting
             * benchmark results.
             */
            gc_collect_cycles();

            $this->startTimestamp = hrtime(true);
        }
    }

    public function stop(?callable $fn = null): void
    {
        if (! $this->hasEnded()) {
            $this->endTimestamp = hrtime(true);
        }

        if ($fn !== null) {
            $fn($this);
        }
    }

    public function onExceedsMilliseconds(float $milliseconds, callable $fn): ?self
    {
        $this->stop();

        if ($this->getDuration() > $milliseconds) {
            $fn($this);
        }

        return $this;
    }

    public function onExceedsSeconds(float $seconds, callable $fn): ?self
    {
        $this->stop();

        $durationSeconds = $this->getDuration() / 1000;
        if ($durationSeconds > $seconds) {
            $fn($this);
        }

        return $this;
    }

    public function onExceedsMinutes(float $minutes, callable $fn): ?self
    {
        $this->stop();

        $durationMinutes = $this->getDuration() / 1000 / 60;
        if ($durationMinutes > $minutes) {
            $fn($this);
        }

        return $this;
    }

    public function onExceedsHours(float $hours, callable $fn): ?self
    {
        $this->stop();

        $durationHours = $this->getDuration() / 3600000;
        if ($durationHours > $hours) {
            $fn($this);
        }

        return $this;
    }

    public function getFriendlyDuration(): string
    {
        $durationInMs = $this->getDuration();

        $units = [
            'day' => 24 * 60 * 60 * 1000,
            'h' => 60 * 60 * 1000,
            'min' => 60 * 1000,
            'sec' => 1000,
            'ms' => 1,
        ];

        $timeStrings = [];

        foreach ($units as $name => $divisor) {
            if ($durationInMs >= $divisor) {
                $value = floor($durationInMs / $divisor);
                $durationInMs %= $divisor;
                $timeStrings[] = $value.$name;
            }
        }

        return $timeStrings !== [] ? implode(' ', $timeStrings) : '0ms';
    }

    /**
     * @return float The duration time in milliseconds
     */
    public function getDuration(): float
    {
        // Convert nanoseconds to milliseconds
        return ($this->endTimestamp - $this->startTimestamp) / 1_000_000;
    }

    public function getTaskable(): ?Taskable
    {
        return $this->taskable;
    }

    public function hasStarted(): bool
    {
        return $this->startTimestamp !== 0;
    }

    public function hasEnded(): bool
    {
        return $this->endTimestamp !== 0;
    }

    /**
     * Get the start timestamp in nanoseconds
     */
    public function getStartTimestamp(): int
    {
        return $this->startTimestamp;
    }

    /**
     * Get the end timestamp in nanoseconds
     */
    public function getEndTimestamp(): int
    {
        return $this->endTimestamp;
    }

    public function getStartDateTime(): ?DateTimeImmutable
    {
        if ($this->hasStarted()) {
            // Convert nanoseconds to seconds for DateTime
            $seconds = $this->startTimestamp / 1_000_000_000;

            return new DateTimeImmutable('@'.number_format($seconds, 6, '.', ''));
        }

        return null;
    }

    public function getEndDateTime(): ?DateTimeImmutable
    {
        if ($this->hasEnded()) {
            // Convert nanoseconds to seconds for DateTime
            $seconds = $this->endTimestamp / 1_000_000_000;

            return new DateTimeImmutable('@'.number_format($seconds, 6, '.', ''));
        }

        return null;
    }

    /**
     * Set start timestamp for testing purposes
     *
     * @param  int  $nanoseconds  Nanoseconds
     */
    public function setTestStartTimestamp(int $nanoseconds): void
    {
        $this->startTimestamp = $nanoseconds;
    }

    /**
     * Set end timestamp for testing purposes
     *
     * @param  int  $nanoseconds  Nanoseconds
     */
    public function setTestEndTimestamp(int $nanoseconds): void
    {
        $this->endTimestamp = $nanoseconds;
    }

    /** @return array<string, mixed> */
    public function toArray(): array
    {
        /** @var ?DateTimeImmutable $startDateTime */
        $startDateTime = $this->getStartDateTime();

        /** @var ?DateTimeImmutable $endDateTime */
        $endDateTime = $this->getEndDateTime();

        return [
            'name' => $this->name,
            'duration' => $this->getDuration(),
            'friendly_duration' => $this->getFriendlyDuration(),
            'start_timestamp' => $this->startTimestamp, // nanoseconds
            'end_timestamp' => $this->endTimestamp,     // nanoseconds
            'start_datetime' => ($startDateTime instanceof DateTimeImmutable) ? $startDateTime->format(DateTime::ATOM) : null,
            'end_datetime' => ($endDateTime instanceof DateTimeImmutable) ? $endDateTime->format(DateTime::ATOM) : null,
        ];
    }
}


================================================
FILE: src/TimeWardenSummary.php
================================================
<?php

declare(strict_types=1);

namespace Tomloprod\TimeWarden;

final class TimeWardenSummary
{
    /** @return array<string, mixed> */
    public function toArray(): array
    {
        /** @var array<string, mixed> $tasksInfo */
        $tasksInfo = [];

        if (timeWarden()->getTasks() !== []) {
            $tasksInfo[] = timeWarden()->toArray();
        }

        /** @var Group $group */
        foreach (timeWarden()->getGroups() as $group) {
            $tasksInfo[] = $group->toArray();
        }

        return $tasksInfo;
    }

    public function toJson(): string
    {
        $json = json_encode($this->toArray());

        return ($json === false) ? '[]' : $json;
    }
}


================================================
FILE: tests/ArchTest.php
================================================
<?php

declare(strict_types=1);

arch('globals')
    ->expect(['dd', 'dump', 'ray', 'die', 'var_dump', 'sleep', 'dispatch', 'dispatch_sync'])
    ->not->toBeUsed();

arch('contracts')
    ->expect('Tomloprod\TimeWarden\Contracts')
    ->toBeInterfaces();

arch('concerns')
    ->expect('Tomloprod\TimeWarden\Concerns')
    ->toBeTraits();


================================================
FILE: tests/Contracts/TaskableTest.php
================================================
<?php

declare(strict_types=1);

use Tomloprod\TimeWarden\Concerns\HasTasks;
use Tomloprod\TimeWarden\Contracts\Taskable;

beforeEach(function (): void {
    $this->tasksClass = new class implements Taskable
    {
        use HasTasks;

        public string $name = 'default';
    };
});

it('can add a task', function (): void {
    $task = $this->tasksClass->createTask('TaskName');

    expect($this->tasksClass->getTasks())
        ->toContain($task);
});

it('can retrieve the last task', function (): void {
    $task1 = $this->tasksClass->createTask('TaskName1');
    $task2 = $this->tasksClass->createTask('TaskName2');

    expect($this->tasksClass->getLastTask())
        ->toBe($task2);
});

it('returns null when retrieving the last task if there are no tasks', function (): void {
    expect($this->tasksClass->getLastTask())
        ->toBeNull();
});

it('can obtain an array/json', function (): void {
    $task1 = $this->tasksClass->createTask('TaskName1');
    $task1->setTestStartTimestamp(dateTimeToTimestamp(new DateTimeImmutable('2017-06-05 12:00:00.0000000')));
    $task1->setTestEndTimestamp(dateTimeToTimestamp(new DateTimeImmutable('2017-06-05 12:00:00.0190000')));

    $task2 = $this->tasksClass->createTask('TaskName2');
    $task2->setTestStartTimestamp(dateTimeToTimestamp(new DateTimeImmutable('2017-06-05 12:00:00.0000000')));
    $task2->setTestEndTimestamp(dateTimeToTimestamp(new DateTimeImmutable('2017-06-05 12:00:00.0230000')));

    $summaryArray = [
        'name' => 'default',
        'duration' => 42.0,
        'tasks' => [
            [
                'name' => 'TaskName1',
                'duration' => 19.0,
                'friendly_duration' => '19ms',
                'start_timestamp' => 1496664000000000000,
                'end_timestamp' => 1496664000019000000,
                'start_datetime' => '2017-06-05T12:00:00+00:00',
                'end_datetime' => '2017-06-05T12:00:00+00:00',
            ],
            [
                'name' => 'TaskName2',
                'duration' => 23.0,
                'friendly_duration' => '23ms',
                'start_timestamp' => 1496664000000000000,
                'end_timestamp' => 1496664000023000000,
                'start_datetime' => '2017-06-05T12:00:00+00:00',
                'end_datetime' => '2017-06-05T12:00:00+00:00',
            ],
        ],
    ];

    expect($this->tasksClass->toArray())->toBe($summaryArray);
    expect($this->tasksClass->toJson())->toBe(json_encode($summaryArray));
});


================================================
FILE: tests/GroupTest.php
================================================
<?php

declare(strict_types=1);

use Tomloprod\TimeWarden\Group;

it('can be created with a name', function (): void {
    $group = new Group('GroupName');

    expect($group->name)->toBe('GroupName');
});

it('can add a task', function (): void {
    $group = new Group('GroupName');
    $task = $group->createTask('TaskName');

    expect($group->getTasks())->toContain($task);

    expect($task->getTaskable())->toBe($group);
});

it('can start the last task if it exists', function (): void {
    $group = new Group('GroupName');
    $task = $group->createTask('TaskName');

    $group->start();

    expect($task->hasStarted())->toBeTrue();
});

it('does not start any task if no tasks exist', function (): void {
    $group = new Group('GroupName');

    $group->start();

    expect($group->getLastTask())->toBeNull();
});


================================================
FILE: tests/Services/TimeWardenManagerTest.php
================================================
<?php

declare(strict_types=1);

use Tomloprod\TimeWarden\Group;
use Tomloprod\TimeWarden\Services\TimeWardenManager;
use Tomloprod\TimeWarden\Task;
use Tomloprod\TimeWarden\TimeWardenSummary;

beforeEach(function (): void {
    TimeWardenManager::instance()->reset();
});

it('throws exception on clone', function (): void {
    $instance = TimeWardenManager::instance();

    $closure = fn (): mixed => clone $instance;

    expect($closure)->toThrow(Exception::class, 'Cannot clone singleton');
});

it('throws exception on unserialize', function (): void {
    $instance = TimeWardenManager::instance();

    $closure = fn (): mixed => unserialize(serialize($instance));

    expect($closure)->toThrow(Exception::class, 'Cannot unserialize singleton');
});

it('returns the same instance', function (): void {
    $instance1 = TimeWardenManager::instance();
    $instance2 = TimeWardenManager::instance();

    expect($instance1)->toBe($instance2);
});

it('resets the singleton instance', function (): void {
    $instance1 = TimeWardenManager::instance();
    $instance1->group('Group1');

    $instance1->reset();

    $instance2 = TimeWardenManager::instance();
    $groups = $instance2->getGroups();

    expect($groups)
        ->toBeEmpty()
        ->not->toBe($instance1);
});

it('can create and retrieve groups', function (): void {
    $instance = TimeWardenManager::instance();

    $instance->group('Group1')->task('foo');
    $instance->group('Group2')->task('bar');

    $groups = $instance->getGroups();

    expect($groups)->toHaveCount(2);

    expect($groups[0])->toBeInstanceOf(Group::class);
    expect($groups[1])->toBeInstanceOf(Group::class);

    expect($groups[0]->name)->toBe('Group1');
    expect($groups[1]->name)->toBe('Group2');
});

it('overwrite last group if doesn\'t have tasks when a new group is created', function (): void {
    $instance = TimeWardenManager::instance();

    $instance->group('Group1');
    $instance->group('Group2');
    $instance->group('Group3');

    $groups = $instance->getGroups();

    expect($groups)->toHaveCount(1);
    expect($groups[0]->name)->toBe('Group3');
    expect($groups[0])->toBeInstanceOf(Group::class);
});

it('can create tasks of timewarden instance', function (): void {
    $instance = TimeWardenManager::instance();

    $instance->task('Task1');

    $tasks = $instance->getTasks();

    expect($tasks)->toHaveCount(1);

    expect($tasks[0])->toBeInstanceOf(Task::class);
});

it('can create tasks inside group', function (): void {
    $instance = TimeWardenManager::instance();

    $instance->group('Group1')->task('Task1');

    $tasks = $instance->getGroups()[0]->getTasks();

    $timewardenTasks = $instance->getTasks();

    expect($tasks)->toHaveCount(1);

    expect($tasks[0])->toBeInstanceOf(Task::class);

    expect($timewardenTasks)->toHaveCount(0);
});

it('overwrite last task if was never started when a new task is created', function (): void {
    $instance = TimeWardenManager::instance();

    $instance->task('Task1')->task('Task2');

    $tasks = $instance->getTasks();

    expect($tasks)->toHaveCount(1);

    expect($tasks[0]->name)->toBe('Task2');

    expect($tasks[0])->toBeInstanceOf(Task::class);
});

it('stop last task if was never ended when a new task is created', function (): void {
    $instance = TimeWardenManager::instance();

    $instance->task('Task1')->start()->task('Task2');

    $tasks = $instance->getTasks();

    expect($tasks)->toHaveCount(2);

    // Task 1
    expect($tasks[0]->name)->toBe('Task1');

    expect($tasks[0]->hasStarted())->toBeTrue();

    expect($tasks[0]->hasEnded())->toBeTrue();

    expect($tasks[0])->toBeInstanceOf(Task::class);

    // Task 2
    expect($tasks[1]->name)->toBe('Task2');

    expect($tasks[1]->hasStarted())->toBeFalse();

    expect($tasks[1]->hasEnded())->toBeFalse();

    expect($tasks[1])->toBeInstanceOf(Task::class);

    $instance->start();

    expect($tasks[1]->hasStarted())->toBeTrue();

    expect($tasks[1]->hasEnded())->toBeFalse();

    $instance->stop();

    expect($tasks[1]->hasStarted())->toBeTrue();

    expect($tasks[1]->hasEnded())->toBeTrue();
});

test('output returns tasks and groups', function (): void {
    timeWarden()->task('Task 1')->start();

    timeWarden()->task('Task 2')->start();

    timeWarden()->stop();

    timeWarden()->group('Group 1')->task('G1 - Task 1')->start();

    timeWarden()->task('G1 - Task 2')->start();

    timeWarden()->task('G1 - Task 3')->start();

    timeWarden()->group('Group 2')->task('G2 - Task 1')->start();

    $output = timeWarden()->output();

    expect($output)
        ->toBeString()
        ->toContain('default')
        ->toContain('Task 1')
        ->toContain('Task 2')
        ->toContain('G1 - Task 1')
        ->toContain('G1 - Task 2')
        ->toContain('G1 - Task 3')
        ->toContain('Group 1')
        ->toContain('Group 2')
        ->toContain('G2 - Task 1');
});

it('can obtain a TimeWardenSummary', function (): void {
    $instance = TimeWardenManager::instance();

    $instance->task('Task1')->task('Task2');

    expect($instance->getSummary())->toBeInstanceOf(TimeWardenSummary::class);
});

it('can measure execution time of a callable with default task name', function (): void {
    $instance = TimeWardenManager::instance();

    $executed = false;

    $duration = $instance->measure(function () use (&$executed): void {
        $executed = true;
    });

    expect($duration)->toBeFloat();
    expect($executed)->toBeTrue();

    $tasks = $instance->getTasks();
    expect($tasks)->toHaveCount(1);
    expect($tasks[0]->name)->toBe('callable');
    expect($tasks[0])->toBeInstanceOf(Task::class);
});

it('can measure execution time of a callable with custom task name', function (): void {
    $instance = TimeWardenManager::instance();

    $executed = false;

    $duration = $instance->measure(function () use (&$executed): void {
        $executed = true;
    }, 'custom-task');

    expect($duration)->toBeFloat();
    expect($executed)->toBeTrue();

    $tasks = $instance->getTasks();
    expect($tasks)->toHaveCount(1);
    expect($tasks[0]->name)->toBe('custom-task');
    expect($tasks[0])->toBeInstanceOf(Task::class);
});

it('can measure execution time inside a group', function (): void {
    $instance = TimeWardenManager::instance();

    $instance->group('Test Group');

    $executed = false;

    $duration = $instance->measure(function () use (&$executed): void {
        $executed = true;
    }, 'group-task');

    expect($duration)->toBeFloat();
    expect($executed)->toBeTrue();

    $groups = $instance->getGroups();
    expect($groups)->toHaveCount(1);

    $tasks = $groups[0]->getTasks();
    expect($tasks)->toHaveCount(1);
    expect($tasks[0]->name)->toBe('group-task');
    expect($tasks[0])->toBeInstanceOf(Task::class);

    // TimeWarden instance should have no tasks (they're in the group)
    expect($instance->getTasks())->toHaveCount(0);
});

it('ensures task is stopped even if callable throws exception', function (): void {
    $instance = TimeWardenManager::instance();

    $exceptionThrown = false;

    try {
        $instance->measure(function (): void {
            throw new Exception('Test exception');
        }, 'exception-task');
    } catch (Exception $e) {
        $exceptionThrown = true;
        expect($e->getMessage())->toBe('Test exception');
    }

    expect($exceptionThrown)->toBeTrue();

    $tasks = $instance->getTasks();
    expect($tasks)->toHaveCount(1);
    expect($tasks[0]->name)->toBe('exception-task');
    expect($tasks[0])->toBeInstanceOf(Task::class);
});


================================================
FILE: tests/Support/Facades/TimeWardenTest.php
================================================
<?php

declare(strict_types=1);

use Tomloprod\TimeWarden\Services\TimeWardenManager;
use Tomloprod\TimeWarden\Support\Facades\TimeWarden;
use Tomloprod\TimeWarden\Task;

beforeEach(function (): void {
    TimeWarden::reset();
});

test('facade returns the same instance', function (): void {
    $instance1 = TimeWardenManager::instance();
    $instance2 = TimeWarden::instance();

    expect($instance1)->toBe($instance2);
});

it('can create tasks using TimeWarden facade', function (): void {
    TimeWarden::task('Task1');

    $tasks = TimeWarden::instance()->getTasks();

    expect($tasks)->toHaveCount(1);

    expect($tasks[0])->toBeInstanceOf(Task::class);
});


================================================
FILE: tests/Support/TimeWardenAliasTest.php
================================================
<?php

declare(strict_types=1);

use Tomloprod\TimeWarden\Services\TimeWardenManager;

test('timeWarden (w uppercase) alias return instance of TimeWarden', function (): void {
    expect(timeWarden())
        ->toBeInstanceOf(TimeWardenManager::class);
});

test('timewarden (w lowercase) alias return instance of TimeWarden', function (): void {
    expect(timewarden())
        ->toBeInstanceOf(TimeWardenManager::class);
});


================================================
FILE: tests/TaskTest.php
================================================
<?php

declare(strict_types=1);

use Tomloprod\TimeWarden\Group;
use Tomloprod\TimeWarden\Task;

it('can be created with a name', function (): void {
    $task = new Task('TaskName');

    expect($task->name)->toBe('TaskName');
});

it('starts and stops task', function (): void {
    $task = new Task('TaskName');

    expect($task->hasStarted())->toBeFalse();
    expect($task->hasEnded())->toBeFalse();

    $task->start();

    expect($task->hasStarted())->toBeTrue();
    expect($task->hasEnded())->toBeFalse();

    $task->stop();

    expect($task->hasStarted())->toBeTrue();
    expect($task->hasEnded())->toBeTrue();
});

it('stop task with callable when does not exceed execution time', function (): void {
    $task = new Task('TaskName');
    $task->setTestStartTimestamp(dateTimeToTimestamp(new DateTimeImmutable('2017-06-05 12:00:00.0000000')));
    $task->setTestEndTimestamp(dateTimeToTimestamp(new DateTimeImmutable('2017-06-05 12:00:00.0190000')));

    /** @var bool $callableIsExecuted */
    $callableIsExecuted = false;

    $task->stop(static function (Task $task) use (&$callableIsExecuted): void {
        $task->onExceedsMilliseconds(20, static function () use (&$callableIsExecuted): void {
            $callableIsExecuted = true;
        });
    });

    expect($task->getDuration())->toBeLessThan(20);
    expect($callableIsExecuted)->toBeFalse();
});

it('stop task with callable when exceeds execution time', function (): void {
    $task = new Task('TaskName');
    $task->setTestStartTimestamp(dateTimeToTimestamp(new DateTimeImmutable('2017-06-05 12:00:00.0000000')));
    $task->setTestEndTimestamp(dateTimeToTimestamp(new DateTimeImmutable('2017-06-05 12:00:00.0210000')));

    /** @var bool $callableIsExecuted */
    $callableIsExecuted = false;

    $task->stop(static function (Task $task) use (&$callableIsExecuted): void {
        $task->onExceedsMilliseconds(20, static function () use (&$callableIsExecuted): void {
            $callableIsExecuted = true;
        });
    });

    expect($task->getDuration())->toBe((float) 21);
    expect($callableIsExecuted)->toBeTrue();
});

it('calculates duration correctly (without usleep)', function (): void {
    $task = new Task('TaskName');
    $task->setTestStartTimestamp(dateTimeToTimestamp(new DateTimeImmutable('2017-06-05 12:00:00.000000')));
    $task->setTestEndTimestamp(dateTimeToTimestamp(new DateTimeImmutable('2017-06-05 12:00:00.020000')));

    expect($task->getDuration())
        ->toBeGreaterThanOrEqual(20)
        ->toBeLessThanOrEqual(21);
});

it('calculates duration correctly (with usleep)', function (): void {
    $task = new Task('TaskName');
    $task->start();

    // Sleep 7ms.
    usleep(7 * 1000);

    $task->stop();

    expect($task->getDuration())
        ->toBeGreaterThanOrEqual(7)
        ->toBeLessThanOrEqual(8);
})->onlyOnLinux();

it('returns duration as 0 if not started or not stopped', function (): void {
    $task = new Task('TaskName');
    $duration = $task->getDuration();

    expect($duration)->toBe(0.0);
});

test('getFriendlyDuration', function (): void {
    $task = new Task('Task');

    $task->setTestStartTimestamp(dateTimeToTimestamp(new DateTimeImmutable('2017-06-05 12:00:00.000000')));
    $task->setTestEndTimestamp(dateTimeToTimestamp(new DateTimeImmutable('2017-06-05 13:10:20.030000')));

    expect($task->getFriendlyDuration())->toContain('1h 10min 20sec 30ms');
});

/**
 * Convert DateTime to nanoseconds for testing
 *
 * @return int Nanoseconds
 */
function dateTimeToTimestamp(DateTimeImmutable $datetime): int
{
    // Convert DateTime to nanoseconds for testing
    $seconds = $datetime->getTimestamp();
    $microseconds = (int) $datetime->format('u');

    return ($seconds * 1_000_000_000) + ($microseconds * 1000);
}

test('onExceedsMilliseconds (exceeds test)', function (): void {
    $task = new Task('Task');
    $task->setTestStartTimestamp(dateTimeToTimestamp(new DateTimeImmutable('2017-06-05 12:00:00.1000000')));
    $task->setTestEndTimestamp(dateTimeToTimestamp(new DateTimeImmutable('2017-06-05 12:00:00.102000')));

    /** @var bool $timeExceeds */
    $timeExceeds = false;

    $task->onExceedsMilliseconds(1, static function () use (&$timeExceeds): void {
        $timeExceeds = true;
    });

    expect($timeExceeds)->toBeTrue();
});

test('onExceedsMilliseconds (does not exceeds test)', function (): void {
    $task2 = new Task('Task');
    $task2->setTestStartTimestamp(dateTimeToTimestamp(new DateTimeImmutable('2017-06-05 12:00:00.1000000')));
    $task2->setTestEndTimestamp(dateTimeToTimestamp(new DateTimeImmutable('2017-06-05 12:00:00.101000')));

    /** @var bool $timeExceeds */
    $timeExceeds = false;

    $task2->onExceedsMilliseconds(1, static function () use (&$timeExceeds): void {
        $timeExceeds = true;
    });

    expect($timeExceeds)->toBeFalse();
});

test('onExceedsSeconds (exceeds test)', function (): void {
    $task = new Task('Task');
    $task->setTestStartTimestamp(dateTimeToTimestamp(new DateTimeImmutable('2017-06-05 12:00:00.0000000')));
    $task->setTestEndTimestamp(dateTimeToTimestamp(new DateTimeImmutable('2017-06-05 12:00:10.0000000')));

    /** @var bool $timeExceeds */
    $timeExceeds = false;

    $task->onExceedsSeconds(9, static function () use (&$timeExceeds): void {
        $timeExceeds = true;
    });

    expect($timeExceeds)->toBeTrue();
});

test('onExceedsSeconds (does not exceeds test)', function (): void {
    $task2 = new Task('Task');
    $task2->setTestStartTimestamp(dateTimeToTimestamp(new DateTimeImmutable('2017-06-05 12:00:00.0000000')));
    $task2->setTestEndTimestamp(dateTimeToTimestamp(new DateTimeImmutable('2017-06-05 12:00:10.0000000')));

    /** @var bool $timeExceeds */
    $timeExceeds = false;

    $task2->onExceedsSeconds(10, static function () use (&$timeExceeds): void {
        $timeExceeds = true;
    });

    expect($timeExceeds)->toBeFalse();
});

test('onExceedsMinutes (exceeds test)', function (): void {
    $task = new Task('Task 1 exceeds execution time');
    $task->setTestStartTimestamp(dateTimeToTimestamp(new DateTimeImmutable('2017-06-05 12:00:00.0000000')));
    $task->setTestEndTimestamp(dateTimeToTimestamp(new DateTimeImmutable('2017-06-05 12:10:00.0000000')));

    /** @var bool $timeExceeds */
    $timeExceeds = false;

    $task->onExceedsMinutes(9, static function () use (&$timeExceeds): void {
        $timeExceeds = true;
    });

    expect($timeExceeds)->toBeTrue();
});

test('onExceedsMinutes (does not exceeds test)', function (): void {
    $task2 = new Task('Task');
    $task2->setTestStartTimestamp(dateTimeToTimestamp(new DateTimeImmutable('2017-06-05 12:00:00.0000000')));
    $task2->setTestEndTimestamp(dateTimeToTimestamp(new DateTimeImmutable('2017-06-05 12:10:00.0000000')));

    /** @var bool $timeExceeds */
    $timeExceeds = false;

    $task2->onExceedsMinutes(10, static function () use (&$timeExceeds): void {
        $timeExceeds = true;
    });

    expect($timeExceeds)->toBeFalse();
});

test('onExceedsHours (exceeds test)', function (): void {
    $task = new Task('Task');
    $task->setTestStartTimestamp(dateTimeToTimestamp(new DateTimeImmutable('2017-06-05 12:00:00.000000')));
    $task->setTestEndTimestamp(dateTimeToTimestamp(new DateTimeImmutable('2017-06-05 14:01:15.000000')));

    /** @var bool $timeExceeds */
    $timeExceeds = false;

    $task->onExceedsHours(2, static function () use (&$timeExceeds): void {
        $timeExceeds = true;
    });

    expect($timeExceeds)->toBeTrue();
});

test('onExceedsHours (does not exceeds test)', function (): void {
    $task2 = new Task('Task');
    $task2->setTestStartTimestamp(dateTimeToTimestamp(new DateTimeImmutable('2017-06-05 12:00:00.000000')));
    $task2->setTestEndTimestamp(dateTimeToTimestamp(new DateTimeImmutable('2017-06-05 12:01:15.000000')));

    /** @var bool $timeExceeds */
    $timeExceeds = false;

    $task2->onExceedsHours(2, static function () use (&$timeExceeds): void {
        $timeExceeds = true;
    });

    expect($timeExceeds)->toBeFalse();
});

test('getTaskable', function (): void {
    $group = new Group('GroupName');
    $task = new Task('TaskName', $group);

    expect($task->getTaskable())->toBe($group);
});

test('getters start and end timestamp', function (): void {
    $task = new Task('Task getStartTimestamp');

    $startTimestamp = dateTimeToTimestamp(new DateTimeImmutable('2017-06-05 12:00:00.000000'));
    $endTimestamp = dateTimeToTimestamp(new DateTimeImmutable('2017-06-05 12:00:00.000000'));

    $task->setTestStartTimestamp($startTimestamp);
    $task->setTestEndTimestamp($endTimestamp);

    expect($task->getStartTimestamp())->toBe($startTimestamp);

    expect($task->getEndTimestamp())->toBe($endTimestamp);
});

test('getStartDateTime return DateTime on started tasks', function (): void {
    $task = new Task('Task getStartDateTime -> DateTime');

    $date = '2017-06-05 12:00:00.000000';

    $startTimestamp = dateTimeToTimestamp(new DateTimeImmutable($date));

    $task->setTestStartTimestamp($startTimestamp);

    $startDateTime = $task->getStartDateTime();

    expect($startDateTime)->toBeInstanceOf(DateTimeImmutable::class);

    expect($startDateTime->format('Y-m-d H:i:s.u'))->toBe($date);
});

test('getStartDateTime return null on non started tasks', function (): void {
    $task = new Task('Task getStartDateTime -> null');

    $endDateTime = $task->getStartDateTime();

    expect($endDateTime)->toBeNull();
});

test('getEndDateTime returns DateTime on ended tasks', function (): void {
    $task = new Task('Task getEndDateTime -> DateTime');

    $date = '2017-06-05 12:00:00.000000';

    $timestamp = dateTimeToTimestamp(new DateTimeImmutable($date));

    $task->setTestStartTimestamp($timestamp);
    $task->setTestEndTimestamp($timestamp);

    $endDateTime = $task->getEndDateTime();

    expect($endDateTime)->toBeInstanceOf(DateTimeImmutable::class);

    expect($endDateTime->format('Y-m-d H:i:s.u'))->toBe($date);
});

test('getEndDateTime returns null on non started tasks', function (): void {
    $task = new Task('Task getEndDateTime -> null');

    $endDateTime = $task->getEndDateTime();

    expect($endDateTime)->toBeNull();
});


================================================
FILE: tests/TimeWardenSummaryTest.php
================================================
<?php

declare(strict_types=1);

use Tomloprod\TimeWarden\TimeWardenSummary;

it('can obtain an array/json', function (): void {
    timeWarden()->reset();

    timeWarden()->task('Generic Task')->start()->stop();

    $task = timeWarden()->getTasks()[0];
    $task->setTestStartTimestamp(dateTimeToTimestamp(new DateTimeImmutable('2017-06-05 12:00:00.0000000')));
    $task->setTestEndTimestamp(dateTimeToTimestamp(new DateTimeImmutable('2017-06-05 12:00:00.0000000')));

    timeWarden()->group('Group1')->task('TaskName1')->start()->stop();

    $groupTask = timeWarden()->getGroups()[0]->getTasks()[0];
    $groupTask->setTestStartTimestamp(dateTimeToTimestamp(new DateTimeImmutable('2017-06-05 12:00:00.0000000')));
    $groupTask->setTestEndTimestamp(dateTimeToTimestamp(new DateTimeImmutable('2017-06-05 12:00:00.0320000')));

    /** @var TimeWardenSummary $summary */
    $summary = timeWarden()->getSummary();

    $summaryArray = [
        [
            'name' => 'default',
            'duration' => 0.0,
            'tasks' => [
                [
                    'name' => 'Generic Task',
                    'duration' => 0.0,
                    'friendly_duration' => '0ms',
                    'start_timestamp' => 1496664000000000000,
                    'end_timestamp' => 1496664000000000000,
                    'start_datetime' => '2017-06-05T12:00:00+00:00',
                    'end_datetime' => '2017-06-05T12:00:00+00:00',
                ],
            ],
        ],
        [
            'name' => 'Group1',
            'duration' => 32.0,
            'tasks' => [
                [
                    'name' => 'TaskName1',
                    'duration' => 32.0,
                    'friendly_duration' => '32ms',
                    'start_timestamp' => 1496664000000000000,
                    'end_timestamp' => 1496664000032000000,
                    'start_datetime' => '2017-06-05T12:00:00+00:00',
                    'end_datetime' => '2017-06-05T12:00:00+00:00',
                ],
            ],
        ],
    ];

    expect($summary->toArray())->toBe($summaryArray);

    expect($summary->toJson())->toBe(json_encode($summaryArray));
});
Download .txt
gitextract_0ftg2r_n/

├── .editorconfig
├── .gitattributes
├── .github/
│   ├── FUNDING.yml
│   └── workflows/
│       ├── formats.yml
│       └── tests.yml
├── .gitignore
├── CHANGELOG.md
├── CONTRIBUTING.md
├── LICENSE.md
├── README.md
├── composer.json
├── phpstan.neon.dist
├── phpunit.xml.dist
├── pint.json
├── rector.php
├── src/
│   ├── Concerns/
│   │   └── HasTasks.php
│   ├── Contracts/
│   │   └── Taskable.php
│   ├── Group.php
│   ├── Services/
│   │   └── TimeWardenManager.php
│   ├── Support/
│   │   ├── Console/
│   │   │   └── Table.php
│   │   ├── Facades/
│   │   │   └── TimeWarden.php
│   │   └── TimeWardenAlias.php
│   ├── Task.php
│   └── TimeWardenSummary.php
└── tests/
    ├── ArchTest.php
    ├── Contracts/
    │   └── TaskableTest.php
    ├── GroupTest.php
    ├── Services/
    │   └── TimeWardenManagerTest.php
    ├── Support/
    │   ├── Facades/
    │   │   └── TimeWardenTest.php
    │   └── TimeWardenAliasTest.php
    ├── TaskTest.php
    └── TimeWardenSummaryTest.php
Download .txt
SYMBOL INDEX (75 symbols across 10 files)

FILE: src/Concerns/HasTasks.php
  type HasTasks (line 9) | trait HasTasks
    method createTask (line 16) | public function createTask(string $taskName): Task
    method getTasks (line 28) | public function getTasks(): array
    method getDuration (line 36) | public function getDuration(): float
    method getLastTask (line 48) | public function getLastTask(): ?Task
    method toArray (line 56) | public function toArray(): array
    method toJson (line 73) | public function toJson(): string

FILE: src/Contracts/Taskable.php
  type Taskable (line 9) | interface Taskable
    method createTask (line 11) | public function createTask(string $taskName): Task;
    method getTasks (line 16) | public function getTasks(): array;
    method getLastTask (line 18) | public function getLastTask(): ?Task;
    method getDuration (line 20) | public function getDuration(): float;
    method toArray (line 23) | public function toArray(): array;
    method toJson (line 25) | public function toJson(): string;

FILE: src/Group.php
  class Group (line 10) | final class Group implements Taskable
    method __construct (line 14) | public function __construct(public string $name) {}
    method start (line 16) | public function start(): void

FILE: src/Services/TimeWardenManager.php
  class TimeWardenManager (line 15) | final class TimeWardenManager implements Taskable
    method __construct (line 28) | private function __construct() {}
    method __clone (line 30) | public function __clone()
    method __wakeup (line 35) | public function __wakeup()
    method instance (line 43) | public static function instance(): self
    method reset (line 52) | public function reset(): self
    method group (line 59) | public function group(string $groupName): self
    method task (line 73) | public function task(string $taskName): self
    method start (line 97) | public function start(): self
    method stop (line 109) | public function stop(?callable $fn = null): self
    method measure (line 128) | public function measure(callable $fn, string $taskName = 'callable'): ...
    method getGroups (line 148) | public function getGroups(): array
    method getSummary (line 153) | public function getSummary(): TimeWardenSummary
    method output (line 160) | public function output(): string
    method getActiveTaskable (line 233) | private function getActiveTaskable(): Taskable
    method getLastGroup (line 238) | private function getLastGroup(): ?Group

FILE: src/Support/Console/Table.php
  class Table (line 10) | final class Table
    method separator (line 64) | public static function separator(): string
    method setHeaders (line 72) | public function setHeaders(array $headers): self
    method setRows (line 82) | public function setRows(array $rows): self
    method setStyle (line 89) | public function setStyle(string $style): self
    method setHeaderTitle (line 96) | public function setHeaderTitle(string $title): self
    method setFooterTitle (line 103) | public function setFooterTitle(string $title): self
    method render (line 110) | public function render(): string
    method calculateColumnWidths (line 150) | private function calculateColumnWidths(): array
    method renderTopBorder (line 176) | private function renderTopBorder(array $columnWidths, array $styleChar...
    method renderHeaderSeparator (line 244) | private function renderHeaderSeparator(array $columnWidths, array $sty...
    method renderSeparator (line 261) | private function renderSeparator(array $columnWidths, array $styleChar...
    method renderRow (line 284) | private function renderRow(array $row, array $columnWidths, array $sty...
    method renderBottomBorder (line 306) | private function renderBottomBorder(array $columnWidths, array $styleC...

FILE: src/Support/Facades/TimeWarden.php
  class TimeWarden (line 27) | final class TimeWarden
    method __callStatic (line 32) | public static function __callStatic(string $method, array $args): mixed

FILE: src/Support/TimeWardenAlias.php
  function timeWarden (line 8) | function timeWarden(): TimeWardenManager
  function timewarden (line 15) | function timewarden(): TimeWardenManager

FILE: src/Task.php
  class Task (line 11) | final class Task
    method __construct (line 23) | public function __construct(public string $name, private readonly ?Tas...
    method start (line 25) | public function start(): void
    method stop (line 39) | public function stop(?callable $fn = null): void
    method onExceedsMilliseconds (line 50) | public function onExceedsMilliseconds(float $milliseconds, callable $f...
    method onExceedsSeconds (line 61) | public function onExceedsSeconds(float $seconds, callable $fn): ?self
    method onExceedsMinutes (line 73) | public function onExceedsMinutes(float $minutes, callable $fn): ?self
    method onExceedsHours (line 85) | public function onExceedsHours(float $hours, callable $fn): ?self
    method getFriendlyDuration (line 97) | public function getFriendlyDuration(): string
    method getDuration (line 125) | public function getDuration(): float
    method getTaskable (line 131) | public function getTaskable(): ?Taskable
    method hasStarted (line 136) | public function hasStarted(): bool
    method hasEnded (line 141) | public function hasEnded(): bool
    method getStartTimestamp (line 149) | public function getStartTimestamp(): int
    method getEndTimestamp (line 157) | public function getEndTimestamp(): int
    method getStartDateTime (line 162) | public function getStartDateTime(): ?DateTimeImmutable
    method getEndDateTime (line 174) | public function getEndDateTime(): ?DateTimeImmutable
    method setTestStartTimestamp (line 191) | public function setTestStartTimestamp(int $nanoseconds): void
    method setTestEndTimestamp (line 201) | public function setTestEndTimestamp(int $nanoseconds): void
    method toArray (line 207) | public function toArray(): array

FILE: src/TimeWardenSummary.php
  class TimeWardenSummary (line 7) | final class TimeWardenSummary
    method toArray (line 10) | public function toArray(): array
    method toJson (line 27) | public function toJson(): string

FILE: tests/TaskTest.php
  function dateTimeToTimestamp (line 112) | function dateTimeToTimestamp(DateTimeImmutable $datetime): int
Condensed preview — 32 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (82K chars).
[
  {
    "path": ".editorconfig",
    "chars": 336,
    "preview": "; This file is for unifying the coding style for different editors and IDEs.\n; More information at http://editorconfig.o"
  },
  {
    "path": ".gitattributes",
    "chars": 462,
    "preview": "/docs              export-ignore\n/tests             export-ignore\n/scripts           export-ignore\n/.github           ex"
  },
  {
    "path": ".github/FUNDING.yml",
    "chars": 50,
    "preview": "custom: https://www.paypal.com/paypalme/tomloprod\n"
  },
  {
    "path": ".github/workflows/formats.yml",
    "chars": 1398,
    "preview": "name: Formats\n\non: ['push', 'pull_request']\n\njobs:\n  ci:\n    runs-on: ${{ matrix.os }}\n\n    strategy:\n      fail-fast: t"
  },
  {
    "path": ".github/workflows/tests.yml",
    "chars": 1373,
    "preview": "name: Tests\n\non: ['push', 'pull_request']\n\njobs:\n  ci:\n    runs-on: ${{ matrix.os }}\n    strategy:\n      fail-fast: true"
  },
  {
    "path": ".gitignore",
    "chars": 127,
    "preview": "/.phpunit.result.cache\n/.phpunit.cache\n/.php-cs-fixer.cache\n/.php-cs-fixer.php\n/composer.lock\n/phpunit.xml\n/vendor/\n*.sw"
  },
  {
    "path": "CHANGELOG.md",
    "chars": 320,
    "preview": "## Version 1.0.1\n> 20 May, 2024\n\n- test: created task belongs to group (`getTaskable()`) by @tomloprod in https://github"
  },
  {
    "path": "CONTRIBUTING.md",
    "chars": 1168,
    "preview": "# 🧑‍🤝‍🧑 Contributing\n\nContributions are welcome, and are accepted via pull requests.\nPlease review these guidelines befo"
  },
  {
    "path": "LICENSE.md",
    "chars": 1095,
    "preview": "The MIT License (MIT)\n\nCopyright (c) Tomás López <tomloprod@gmail.com>\n\nPermission is hereby granted, free of charge, to"
  },
  {
    "path": "README.md",
    "chars": 12240,
    "preview": "<p align=\"center\">\n    <p align=\"center\">\n        <a href=\"https://github.com/tomloprod/time-warden/actions\"><img alt=\"G"
  },
  {
    "path": "composer.json",
    "chars": 1893,
    "preview": "{\n    \"name\": \"tomloprod/time-warden\",\n    \"description\": \"TimeWarden is a lightweight PHP library that enables you to m"
  },
  {
    "path": "phpstan.neon.dist",
    "chars": 92,
    "preview": "parameters:\n    level: max\n    paths:\n        - src\n\n    reportUnmatchedIgnoredErrors: true\n"
  },
  {
    "path": "phpunit.xml.dist",
    "chars": 514,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<phpunit xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n    xsi:noNamespac"
  },
  {
    "path": "pint.json",
    "chars": 1872,
    "preview": "{\n    \"preset\": \"laravel\",\n    \"rules\": {\n        \"array_push\": true,\n        \"backtick_to_shell_exec\": true,\n        \"d"
  },
  {
    "path": "rector.php",
    "chars": 413,
    "preview": "<?php\n\ndeclare(strict_types=1);\n\nuse Rector\\Config\\RectorConfig;\n\nreturn RectorConfig::configure()\n    ->withPaths([\n   "
  },
  {
    "path": "src/Concerns/HasTasks.php",
    "chars": 1553,
    "preview": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Tomloprod\\TimeWarden\\Concerns;\n\nuse Tomloprod\\TimeWarden\\Task;\n\ntrait HasTask"
  },
  {
    "path": "src/Contracts/Taskable.php",
    "chars": 474,
    "preview": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Tomloprod\\TimeWarden\\Contracts;\n\nuse Tomloprod\\TimeWarden\\Task;\n\ninterface Ta"
  },
  {
    "path": "src/Group.php",
    "chars": 486,
    "preview": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Tomloprod\\TimeWarden;\n\nuse Tomloprod\\TimeWarden\\Concerns\\HasTasks;\nuse Tomlop"
  },
  {
    "path": "src/Services/TimeWardenManager.php",
    "chars": 6076,
    "preview": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Tomloprod\\TimeWarden\\Services;\n\nuse Exception;\nuse Tomloprod\\TimeWarden\\Conce"
  },
  {
    "path": "src/Support/Console/Table.php",
    "chars": 11821,
    "preview": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Tomloprod\\TimeWarden\\Support\\Console;\n\n/**\n * @codeCoverageIgnore\n */\nfinal c"
  },
  {
    "path": "src/Support/Facades/TimeWarden.php",
    "chars": 1075,
    "preview": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Tomloprod\\TimeWarden\\Support\\Facades;\n\nuse Tomloprod\\TimeWarden\\Group;\nuse To"
  },
  {
    "path": "src/Support/TimeWardenAlias.php",
    "chars": 376,
    "preview": "<?php\n\ndeclare(strict_types=1);\n\nuse Tomloprod\\TimeWarden\\Services\\TimeWardenManager;\n\nif (! function_exists('timeWarden"
  },
  {
    "path": "src/Task.php",
    "chars": 5616,
    "preview": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Tomloprod\\TimeWarden;\n\nuse DateTime;\nuse DateTimeImmutable;\nuse Tomloprod\\Tim"
  },
  {
    "path": "src/TimeWardenSummary.php",
    "chars": 697,
    "preview": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Tomloprod\\TimeWarden;\n\nfinal class TimeWardenSummary\n{\n    /** @return array<"
  },
  {
    "path": "tests/ArchTest.php",
    "chars": 339,
    "preview": "<?php\n\ndeclare(strict_types=1);\n\narch('globals')\n    ->expect(['dd', 'dump', 'ray', 'die', 'var_dump', 'sleep', 'dispatc"
  },
  {
    "path": "tests/Contracts/TaskableTest.php",
    "chars": 2517,
    "preview": "<?php\n\ndeclare(strict_types=1);\n\nuse Tomloprod\\TimeWarden\\Concerns\\HasTasks;\nuse Tomloprod\\TimeWarden\\Contracts\\Taskable"
  },
  {
    "path": "tests/GroupTest.php",
    "chars": 830,
    "preview": "<?php\n\ndeclare(strict_types=1);\n\nuse Tomloprod\\TimeWarden\\Group;\n\nit('can be created with a name', function (): void {\n "
  },
  {
    "path": "tests/Services/TimeWardenManagerTest.php",
    "chars": 7636,
    "preview": "<?php\n\ndeclare(strict_types=1);\n\nuse Tomloprod\\TimeWarden\\Group;\nuse Tomloprod\\TimeWarden\\Services\\TimeWardenManager;\nus"
  },
  {
    "path": "tests/Support/Facades/TimeWardenTest.php",
    "chars": 672,
    "preview": "<?php\n\ndeclare(strict_types=1);\n\nuse Tomloprod\\TimeWarden\\Services\\TimeWardenManager;\nuse Tomloprod\\TimeWarden\\Support\\F"
  },
  {
    "path": "tests/Support/TimeWardenAliasTest.php",
    "chars": 428,
    "preview": "<?php\n\ndeclare(strict_types=1);\n\nuse Tomloprod\\TimeWarden\\Services\\TimeWardenManager;\n\ntest('timeWarden (w uppercase) al"
  },
  {
    "path": "tests/TaskTest.php",
    "chars": 10255,
    "preview": "<?php\n\ndeclare(strict_types=1);\n\nuse Tomloprod\\TimeWarden\\Group;\nuse Tomloprod\\TimeWarden\\Task;\n\nit('can be created with"
  },
  {
    "path": "tests/TimeWardenSummaryTest.php",
    "chars": 2185,
    "preview": "<?php\n\ndeclare(strict_types=1);\n\nuse Tomloprod\\TimeWarden\\TimeWardenSummary;\n\nit('can obtain an array/json', function ()"
  }
]

About this extraction

This page contains the full source code of the tomloprod/time-warden GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 32 files (74.6 KB), approximately 20.2k tokens, and a symbol index with 75 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!