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));
});
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
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.