diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..9d4b1ef
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,15 @@
+; This file is for unifying the coding style for different editors and IDEs.
+; More information at http://editorconfig.org
+
+root = true
+
+[*]
+charset = utf-8
+indent_size = 4
+indent_style = space
+end_of_line = lf
+insert_final_newline = true
+trim_trailing_whitespace = true
+
+[*.yml]
+indent_size = 2
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..1392104
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,22 @@
+# This file tells which files and directories should be ignored and
+# NOT downloaded when using composer to pull down a project with
+# the --prefer-dist option selected. Used to remove development
+# specific files so user has a clean download.
+
+# git files
+.gitattributes export-ignore
+.gitignore export-ignore
+
+# admin files
+.github/ export-ignore
+contributing/ export-ignore
+.editorconfig export-ignore
+CODE_OF_CONDUCT.md export-ignore
+CONTRIBUTING.md export-ignore
+
+# contributor/development files
+tests/ export-ignore
+phpstan-baseline.php export-ignore
+phpstan.neon.dist export-ignore
+phpunit.xml.dist export-ignore
+rector.php export-ignore
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
new file mode 100644
index 0000000..435d94a
--- /dev/null
+++ b/.github/CODEOWNERS
@@ -0,0 +1,2 @@
+# https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners#example-of-a-codeowners-file
+* @ddevsr
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
new file mode 100644
index 0000000..1979f41
--- /dev/null
+++ b/.github/FUNDING.yml
@@ -0,0 +1,13 @@
+# These are supported funding model platforms
+
+github: [ddevsr]
+patreon: # Replace with a single Patreon username
+open_collective: # Replace with a single Open Collective username
+ko_fi: # Replace with a single Ko-fi username
+tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
+community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
+liberapay: # Replace with a single Liberapay username
+issuehunt: # Replace with a single IssueHunt username
+otechie: # Replace with a single Otechie username
+lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
+custom: ['https://paypal.me/hexageek1337']
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..d4b0515
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,11 @@
+version: 2
+updates:
+ - package-ecosystem: composer
+ directory: "/"
+ schedule:
+ interval: daily
+
+ - package-ecosystem: "github-actions"
+ directory: "/"
+ schedule:
+ interval: daily
diff --git a/.github/prlint.json b/.github/prlint.json
new file mode 100644
index 0000000..a3744c5
--- /dev/null
+++ b/.github/prlint.json
@@ -0,0 +1,8 @@
+{
+ "title": [
+ {
+ "pattern": "^(\\[\\d+\\.\\d+\\]\\s{1})?(feat|fix|chore|docs|perf|refactor|style|test|config|revert)(\\([\\-.@:`a-zA-Z0-9]+\\))?!?:\\s{1}\\S.+\\S|Prep for \\d\\.\\d\\.\\d release|\\d\\.\\d\\.\\d Ready code$",
+ "message": "PR title must include the type (feat, fix, chore, docs, perf, refactor, style, test, config, revert) of the commit per Conventional Commits specification. See https://www.conventionalcommits.org/en/v1.0.0/ for the discussion."
+ }
+ ]
+}
diff --git a/.github/workflows/check-conflict.yml b/.github/workflows/check-conflict.yml
new file mode 100644
index 0000000..5d71f44
--- /dev/null
+++ b/.github/workflows/check-conflict.yml
@@ -0,0 +1,28 @@
+name: Check conflict branch in PR
+on:
+ schedule:
+ - cron: '*/20 * * * *' # Run at every 20 minutes
+
+jobs:
+ build:
+ name: Check conflict branch in PR
+ permissions:
+ contents: read
+ pull-requests: write
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Check conflict branch in PR
+ uses: PHPDevsr/check-conflict-action@v1.1.0
+ with:
+ token: ${{ secrets.GITHUB_TOKEN }}
+ label: stale
+ comment: |
+ :wave: Hi, @authorTarget!
+
+ We detected conflicts in your PR against the base branch :speak_no_evil:
+ You may want to sync :arrows_counterclockwise: your branch with upstream!
+
+ Ref: [Syncing Your Branch](https://github.com/codeigniter4/CodeIgniter4/blob/develop/contributing/workflow.md#pushing-your-branch)
diff --git a/.github/workflows/check-signing.yml b/.github/workflows/check-signing.yml
new file mode 100644
index 0000000..ae91576
--- /dev/null
+++ b/.github/workflows/check-signing.yml
@@ -0,0 +1,32 @@
+name: Check Signed PR
+on:
+ pull_request:
+ branches:
+ - 'main'
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
+ cancel-in-progress: true
+
+permissions:
+ contents: read
+ pull-requests: write
+
+jobs:
+ build:
+ name: Check Signed Commit
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Check signed commits in PR
+ uses: 1Password/check-signed-commits-action@v1.2.0
+ with:
+ token: ${{ secrets.GITHUB_TOKEN }}
+ comment: |
+ You must GPG-sign your work, certifying that you either wrote the work or otherwise have the right to pass it on to an open-source project. See Developer's Certificate of Origin. See [signing][1].
+
+ **Note that all your commits must be signed.** If you have an unsigned commit, you can sign the previous commits by referring to [gpg-signing-old-commits][2].
+ [1]: https://github.com/codeigniter4/CodeIgniter4/blob/develop/contributing/pull_request.md#signing
+ [2]: https://github.com/codeigniter4/CodeIgniter4/blob/develop/contributing/workflow.md#gpg-signing-old-commits
diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml
new file mode 100644
index 0000000..0d4a013
--- /dev/null
+++ b/.github/workflows/dependency-review.yml
@@ -0,0 +1,20 @@
+# Dependency Review Action
+#
+# This Action will scan dependency manifest files that change as part of a Pull Request, surfacing known-vulnerable versions of the packages declared or updated in the PR. Once installed, if the workflow run is marked as required, PRs introducing known-vulnerable packages will be blocked from merging.
+#
+# Source repository: https://github.com/actions/dependency-review-action
+# Public documentation: https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/about-dependency-review#dependency-review-enforcement
+name: 'Dependency Review'
+on: [pull_request]
+
+permissions:
+ contents: read
+
+jobs:
+ dependency-review:
+ runs-on: ubuntu-latest
+ steps:
+ - name: 'Checkout Repository'
+ uses: actions/checkout@v4
+ - name: 'Dependency Review'
+ uses: actions/dependency-review-action@v4
diff --git a/.github/workflows/test-phpstan.yml b/.github/workflows/test-phpstan.yml
new file mode 100644
index 0000000..853dbc2
--- /dev/null
+++ b/.github/workflows/test-phpstan.yml
@@ -0,0 +1,83 @@
+name: PHPStan
+
+on:
+ pull_request:
+ branches:
+ - 'main'
+ paths:
+ - '**.php'
+ - 'composer.*'
+ - 'phpstan*'
+ - '.github/workflows/test-phpstan.yml'
+ push:
+ branches:
+ - 'main'
+ paths:
+ - '**.php'
+ - 'composer.*'
+ - 'phpstan*'
+ - '.github/workflows/test-phpstan.yml'
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
+ cancel-in-progress: true
+
+permissions:
+ contents: read # to fetch code (actions/checkout)
+
+jobs:
+ build:
+ name: PHP ${{ matrix.php-versions }} Static Analysis
+ runs-on: ubuntu-latest
+ if: (! contains(github.event.head_commit.message, '[ci skip]'))
+ strategy:
+ fail-fast: false
+ matrix:
+ php-versions: ['8.3', '8.4']
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Setup PHP
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: ${{ matrix.php-versions }}
+ tools: phpstan
+ extensions: intl
+ coverage: none
+ env:
+ COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Get composer cache directory
+ run: echo "COMPOSER_CACHE_FILES_DIR=$(composer config cache-files-dir)" >> $GITHUB_ENV
+
+ - name: Cache composer dependencies
+ uses: actions/cache@v4
+ with:
+ path: ${{ env.COMPOSER_CACHE_FILES_DIR }}
+ key: ${{ runner.os }}-${{ matrix.php-version }}-${{ hashFiles('**/composer.lock') }}
+ restore-keys: |
+ ${{ runner.os }}-${{ matrix.php-version }}-
+ ${{ runner.os }}-
+
+ - name: Create PHPStan cache directory
+ run: mkdir -p build/phpstan
+
+ - name: Cache PHPStan results
+ uses: actions/cache@v4
+ with:
+ path: build/phpstan
+ key: ${{ runner.os }}-phpstan-${{ github.sha }}
+ restore-keys: ${{ runner.os }}-phpstan-
+
+ - name: Install Dependencies
+ run: |
+ if [ -f composer.lock ]; then
+ composer install --no-progress --no-interaction --prefer-dist --optimize-autoloader
+ else
+ composer update --no-progress --no-interaction --prefer-dist --optimize-autoloader
+ fi
+
+ - name: Run static analysis
+ run: vendor/bin/phpstan analyse
diff --git a/.github/workflows/test-phpunit.yml b/.github/workflows/test-phpunit.yml
new file mode 100644
index 0000000..b743f38
--- /dev/null
+++ b/.github/workflows/test-phpunit.yml
@@ -0,0 +1,75 @@
+name: PHPUnit
+
+on:
+ pull_request:
+ branches:
+ - 'main'
+ paths:
+ - '**.php'
+ - 'composer.*'
+ - 'phpunit*'
+ - '.github/workflows/test-phpunit.yml'
+ push:
+ branches:
+ - 'main'
+ paths:
+ - '**.php'
+ - 'composer.*'
+ - 'phpunit*'
+ - '.github/workflows/test-phpunit.yml'
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
+ cancel-in-progress: true
+
+permissions:
+ contents: read # to fetch code (actions/checkout)
+
+jobs:
+ main:
+ name: PHP ${{ matrix.php-versions }} Unit Tests
+ runs-on: ubuntu-latest
+ if: (! contains(github.event.head_commit.message, '[ci skip]'))
+ strategy:
+ matrix:
+ php-versions: ['8.3', '8.4']
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Setup PHP
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: ${{ matrix.php-versions }}
+ tools: composer, phpunit
+ extensions: intl
+ coverage: xdebug
+ env:
+ COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Get composer cache directory
+ run: echo "COMPOSER_CACHE_FILES_DIR=$(composer config cache-files-dir)" >> $GITHUB_ENV
+
+ - name: Cache composer dependencies
+ uses: actions/cache@v4
+ with:
+ path: ${{ env.COMPOSER_CACHE_FILES_DIR }}
+ key: ${{ runner.os }}-${{ matrix.php-version }}-${{ hashFiles('**/composer.lock') }}
+ restore-keys: |
+ ${{ runner.os }}-${{ matrix.php-version }}-
+ ${{ runner.os }}-
+
+ - name: Install Dependencies
+ run: |
+ if [ -f composer.lock ]; then
+ composer install --no-progress --no-interaction --prefer-dist --optimize-autoloader
+ else
+ composer update --no-progress --no-interaction --prefer-dist --optimize-autoloader
+ fi
+
+ - name: Test with PHPUnit
+ run: vendor/bin/phpunit --coverage-text --testsuite main
+ env:
+ TERM: xterm-256color
+ TACHYCARDIA_MONITOR_GA: enabled
diff --git a/.github/workflows/test-rector.yml b/.github/workflows/test-rector.yml
new file mode 100644
index 0000000..2c553ee
--- /dev/null
+++ b/.github/workflows/test-rector.yml
@@ -0,0 +1,69 @@
+name: Rector
+
+on:
+ pull_request:
+ branches:
+ - 'main'
+ paths:
+ - 'src/**.php'
+ - 'composer.*'
+ - 'rector.php'
+ - '.github/workflows/test-rector.yml'
+ push:
+ branches:
+ - 'main'
+ paths:
+ - 'src/**.php'
+ - 'composer.*'
+ - 'rector.php'
+ - '.github/workflows/test-rector.yml'
+
+permissions:
+ contents: read # to fetch code (actions/checkout)
+
+jobs:
+ build:
+ name: PHP ${{ matrix.php-versions }} Rector Analysis
+ runs-on: ubuntu-latest
+ if: (! contains(github.event.head_commit.message, '[ci skip]'))
+ strategy:
+ fail-fast: false
+ matrix:
+ php-versions: ['8.3', '8.4']
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Setup PHP
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: ${{ matrix.php-versions }}
+ tools: phpstan
+ extensions: intl, json, mbstring, xml
+ coverage: none
+ env:
+ COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Get composer cache directory
+ run: echo "COMPOSER_CACHE_FILES_DIR=$(composer config cache-files-dir)" >> $GITHUB_ENV
+
+ - name: Cache composer dependencies
+ uses: actions/cache@v4
+ with:
+ path: ${{ env.COMPOSER_CACHE_FILES_DIR }}
+ key: ${{ runner.os }}-${{ matrix.php-version }}-${{ hashFiles('**/composer.lock') }}
+ restore-keys: |
+ ${{ runner.os }}-${{ matrix.php-version }}-
+ ${{ runner.os }}-
+
+ - name: Install Dependencies
+ run: |
+ if [ -f composer.lock ]; then
+ composer install --no-progress --no-interaction --prefer-dist --optimize-autoloader
+ else
+ composer update --no-progress --no-interaction --prefer-dist --optimize-autoloader
+ fi
+
+ - name: Analyze for refactoring
+ run: vendor/bin/rector process --dry-run
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..eae65ee
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,98 @@
+#-------------------------
+# Operating Specific Junk Files
+#-------------------------
+
+# OS X
+.DS_Store
+.AppleDouble
+.LSOverride
+
+# OS X Thumbnails
+._*
+
+# Windows image file caches
+Thumbs.db
+ehthumbs.db
+Desktop.ini
+
+# Recycle Bin used on file shares
+$RECYCLE.BIN/
+
+# Windows Installer files
+*.cab
+*.msi
+*.msm
+*.msp
+
+# Windows shortcuts
+*.lnk
+
+# Linux
+*~
+
+# KDE directory preferences
+.directory
+
+# Linux trash folder which might appear on any partition or disk
+.Trash-*
+
+#-------------------------
+# Environment Files
+#-------------------------
+# These should never be under version control,
+# as it poses a security risk.
+.env
+.vagrant
+Vagrantfile
+
+php_errors.log
+
+#-------------------------
+# Test Files
+#-------------------------
+tests/coverage*
+
+# Don't save phpunit under version control.
+phpunit
+tests/_support/result/*
+!tests/_support/result/.gitkeep
+
+#-------------------------
+# Composer
+#-------------------------
+vendor/
+composer.lock
+
+#-------------------------
+# IDE / Development Files
+#-------------------------
+
+# phpenv local config
+.php-version
+
+# Jetbrains editors (PHPStorm, etc)
+.idea/
+*.iml
+
+# Netbeans
+nbproject/
+build/
+nbbuild/
+dist/
+nbdist/
+nbactions.xml
+nb-configuration.xml
+.nb-gradle/
+
+# Sublime Text
+*.tmlanguage.cache
+*.tmPreferences.cache
+*.stTheme.cache
+*.sublime-workspace
+*.sublime-project
+.phpintel
+
+# Visual Studio Code
+.vscode/
+
+/phpunit*.xml
diff --git a/composer.json b/composer.json
new file mode 100644
index 0000000..6fc72ec
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,63 @@
+{
+ "name": "phpdevsr/php-profiler",
+ "description": "PHP Profiling with Excimer",
+ "license": "MIT",
+ "type": "library",
+ "keywords": [
+ "php",
+ "profiler",
+ "profiling",
+ "excimer",
+ "performance"
+ ],
+ "authors": [
+ {
+ "name": "Denny Septian Panggabean",
+ "email": "xamidimura@gmail.com",
+ "role": "Developer"
+ }
+ ],
+ "homepage": "https://github.com/PHPDevsr/php-profiler",
+ "require": {
+ "php": "^8.3 || ^8.4 || ^8.5"
+ },
+ "require-dev": {
+ "nexusphp/tachycardia": "^2.4",
+ "phpstan/extension-installer": "^1.4",
+ "phpstan/phpstan": "^2.1",
+ "phpstan/phpstan-strict-rules": "^2.0",
+ "phpunit/phpcov": "^11.0 || ^12.0",
+ "phpunit/phpunit": "^12.5.22",
+ "rector/rector": "^2.0"
+ },
+ "minimum-stability": "dev",
+ "prefer-stable": true,
+ "autoload": {
+ "psr-4": {
+ "PHPDevsr\\Profiler\\": "src"
+ }
+ },
+ "autoload-dev": {
+ "psr-4": {
+ "Tests\\": "tests"
+ }
+ },
+ "config": {
+ "optimize-autoloader": true,
+ "sort-packages": true,
+ "allow-plugins": {
+ "phpstan/extension-installer": true
+ }
+ },
+ "scripts": {
+ "analyze": [
+ "@phpstan",
+ "@rector"
+ ],
+ "rector": "vendor/bin/rector process --dry-run",
+ "rector-fix": "vendor/bin/rector process",
+ "phpstan": "vendor/bin/phpstan analyse",
+ "phpstan-baseline": "vendor/bin/phpstan analyse --generate-baseline phpstan-baseline.php",
+ "test": "vendor/bin/phpunit --coverage-text --coverage-html build/phpunit/html --coverage-clover build/phpunit/logs/clover.xml --coverage-php build/phpunit/cov/coverage.cov --testsuite main"
+ }
+}
diff --git a/phpstan.neon.dist b/phpstan.neon.dist
new file mode 100644
index 0000000..30e829e
--- /dev/null
+++ b/phpstan.neon.dist
@@ -0,0 +1,6 @@
+parameters:
+ tmpDir: build/phpstan
+ level: 9
+ paths:
+ - src/
+ - tests/
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
new file mode 100644
index 0000000..0f5d26a
--- /dev/null
+++ b/phpunit.xml.dist
@@ -0,0 +1,58 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ./tests
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ src
+
+
+
+
+
+
+
+
+
diff --git a/rector.php b/rector.php
new file mode 100644
index 0000000..f5333ed
--- /dev/null
+++ b/rector.php
@@ -0,0 +1,177 @@
+
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+use Rector\CodeQuality\Rector\Assign\CombinedAssignRector;
+use Rector\CodeQuality\Rector\BooleanAnd\SimplifyEmptyArrayCheckRector;
+use Rector\CodeQuality\Rector\BooleanNot\ReplaceMultipleBooleanNotRector;
+use Rector\CodeQuality\Rector\Class_\CompleteDynamicPropertiesRector;
+use Rector\CodeQuality\Rector\ClassMethod\ExplicitReturnNullRector;
+use Rector\CodeQuality\Rector\ClassMethod\InlineArrayReturnAssignRector;
+use Rector\CodeQuality\Rector\Concat\JoinStringConcatRector;
+use Rector\CodeQuality\Rector\Empty_\SimplifyEmptyCheckOnEmptyArrayRector;
+use Rector\CodeQuality\Rector\Equal\UseIdenticalOverEqualWithSameTypeRector;
+use Rector\CodeQuality\Rector\Expression\InlineIfToExplicitIfRector;
+use Rector\CodeQuality\Rector\Expression\TernaryFalseExpressionToIfRector;
+use Rector\CodeQuality\Rector\Foreach_\UnusedForeachValueToArrayKeysRector;
+use Rector\CodeQuality\Rector\FuncCall\ArrayMergeOfNonArraysToSimpleArrayRector;
+use Rector\CodeQuality\Rector\FuncCall\ChangeArrayPushToArrayAssignRector;
+use Rector\CodeQuality\Rector\FuncCall\SimplifyRegexPatternRector;
+use Rector\CodeQuality\Rector\FuncCall\SimplifyStrposLowerRector;
+use Rector\CodeQuality\Rector\FunctionLike\SimplifyUselessVariableRector;
+use Rector\CodeQuality\Rector\If_\CombineIfRector;
+use Rector\CodeQuality\Rector\If_\CompleteMissingIfElseBracketRector;
+use Rector\CodeQuality\Rector\If_\ExplicitBoolCompareRector;
+use Rector\CodeQuality\Rector\If_\ShortenElseIfRector;
+use Rector\CodeQuality\Rector\If_\SimplifyIfElseToTernaryRector;
+use Rector\CodeQuality\Rector\If_\SimplifyIfNotNullReturnRector;
+use Rector\CodeQuality\Rector\If_\SimplifyIfNullableReturnRector;
+use Rector\CodeQuality\Rector\If_\SimplifyIfReturnBoolRector;
+use Rector\CodeQuality\Rector\LogicalAnd\AndAssignsToSeparateLinesRector;
+use Rector\CodeQuality\Rector\LogicalAnd\LogicalToBooleanRector;
+use Rector\CodeQuality\Rector\NotEqual\CommonNotEqualRector;
+use Rector\CodeQuality\Rector\Ternary\NumberCompareToMaxFuncCallRector;
+use Rector\CodeQuality\Rector\Ternary\SwitchNegatedTernaryRector;
+use Rector\CodeQuality\Rector\Ternary\UnnecessaryTernaryExpressionRector;
+use Rector\CodingStyle\Rector\ClassMethod\FuncGetArgsToVariadicParamRector;
+use Rector\CodingStyle\Rector\ClassMethod\MakeInheritedMethodVisibilitySameAsParentRector;
+use Rector\CodingStyle\Rector\FuncCall\CountArrayToEmptyArrayComparisonRector;
+use Rector\Config\RectorConfig;
+use Rector\DeadCode\Rector\Array_\RemoveDuplicatedArrayKeyRector;
+use Rector\DeadCode\Rector\Assign\RemoveDoubleAssignRector;
+use Rector\DeadCode\Rector\Assign\RemoveUnusedVariableAssignRector;
+use Rector\DeadCode\Rector\BooleanAnd\RemoveAndTrueRector;
+use Rector\DeadCode\Rector\Cast\RecastingRemovalRector;
+use Rector\DeadCode\Rector\ClassConst\RemoveUnusedPrivateClassConstantRector;
+use Rector\DeadCode\Rector\ClassMethod\RemoveEmptyClassMethodRector;
+use Rector\DeadCode\Rector\ClassMethod\RemoveUnusedConstructorParamRector;
+use Rector\DeadCode\Rector\ClassMethod\RemoveUnusedPrivateMethodParameterRector;
+use Rector\DeadCode\Rector\ClassMethod\RemoveUnusedPrivateMethodRector;
+use Rector\DeadCode\Rector\ClassMethod\RemoveUselessParamTagRector;
+use Rector\DeadCode\Rector\ClassMethod\RemoveUselessReturnTagRector;
+use Rector\DeadCode\Rector\Concat\RemoveConcatAutocastRector;
+use Rector\DeadCode\Rector\ConstFetch\RemovePhpVersionIdCheckRector;
+use Rector\DeadCode\Rector\Expression\RemoveDeadStmtRector;
+use Rector\DeadCode\Rector\Expression\SimplifyMirrorAssignRector;
+use Rector\DeadCode\Rector\Plus\RemoveDeadZeroAndOneOperationRector;
+use Rector\DeadCode\Rector\Return_\RemoveDeadConditionAboveReturnRector;
+use Rector\DeadCode\Rector\TryCatch\RemoveDeadTryCatchRector;
+use Rector\EarlyReturn\Rector\Foreach_\ChangeNestedForeachIfsToEarlyContinueRector;
+use Rector\EarlyReturn\Rector\If_\ChangeIfElseValueAssignToEarlyReturnRector;
+use Rector\EarlyReturn\Rector\If_\RemoveAlwaysElseRector;
+use Rector\EarlyReturn\Rector\Return_\PreparedValueToEarlyReturnRector;
+use Rector\Php55\Rector\String_\StringClassNameToClassConstantRector;
+use Rector\Privatization\Rector\Property\PrivatizeFinalClassPropertyRector;
+use Rector\Set\ValueObject\LevelSetList;
+use Rector\Set\ValueObject\SetList;
+use Rector\ValueObject\PhpVersion;
+
+return static function (RectorConfig $rectorConfig): void {
+ $rectorConfig->sets([
+ SetList::DEAD_CODE,
+ SetList::CODE_QUALITY,
+ SetList::CODING_STYLE,
+ LevelSetList::UP_TO_PHP_83,
+ ]);
+
+ // The paths to refactor (can also be supplied with CLI arguments)
+ $rectorConfig->paths([
+ __DIR__ . '/src',
+ __DIR__ . '/tests',
+ ]);
+
+ // Include Composer's autoload - required for global execution, remove if running locally
+ $rectorConfig->autoloadPaths([
+ __DIR__ . '/vendor/autoload.php',
+ ]);
+
+ if (is_file(__DIR__ . '/phpstan.neon.dist')) {
+ $rectorConfig->phpstanConfig(__DIR__ . '/phpstan.neon.dist');
+ }
+
+ // Set the target version for refactoring
+ $rectorConfig->phpVersion(PhpVersion::PHP_83);
+
+ // Auto-import fully qualified class names
+ $rectorConfig->importNames();
+
+ // Are there files or rules you need to skip?
+ $rectorConfig->skip([
+ ExplicitReturnNullRector::class,
+ ]);
+
+ // Code Quality
+ $rectorConfig->rule(JoinStringConcatRector::class);
+ $rectorConfig->rule(CombinedAssignRector::class);
+ $rectorConfig->rule(ReplaceMultipleBooleanNotRector::class);
+ $rectorConfig->rule(InlineArrayReturnAssignRector::class);
+ $rectorConfig->rule(SimplifyEmptyCheckOnEmptyArrayRector::class);
+ $rectorConfig->rule(UseIdenticalOverEqualWithSameTypeRector::class);
+ $rectorConfig->rule(TernaryFalseExpressionToIfRector::class);
+ $rectorConfig->rule(ArrayMergeOfNonArraysToSimpleArrayRector::class);
+ $rectorConfig->rule(ExplicitBoolCompareRector::class);
+ $rectorConfig->rule(SimplifyIfNotNullReturnRector::class);
+ $rectorConfig->rule(SimplifyIfNullableReturnRector::class);
+ $rectorConfig->rule(SimplifyUselessVariableRector::class);
+ $rectorConfig->rule(AndAssignsToSeparateLinesRector::class);
+ $rectorConfig->rule(LogicalToBooleanRector::class);
+ $rectorConfig->rule(CommonNotEqualRector::class);
+ $rectorConfig->rule(NumberCompareToMaxFuncCallRector::class);
+ $rectorConfig->rule(SwitchNegatedTernaryRector::class);
+ $rectorConfig->rule(SimplifyEmptyArrayCheckRector::class);
+ $rectorConfig->rule(CompleteDynamicPropertiesRector::class);
+ $rectorConfig->rule(InlineIfToExplicitIfRector::class);
+ $rectorConfig->rule(UnusedForeachValueToArrayKeysRector::class);
+ $rectorConfig->rule(ChangeArrayPushToArrayAssignRector::class);
+ $rectorConfig->rule(SimplifyRegexPatternRector::class);
+ $rectorConfig->rule(SimplifyStrposLowerRector::class);
+ $rectorConfig->rule(CombineIfRector::class);
+ $rectorConfig->rule(CompleteMissingIfElseBracketRector::class);
+ $rectorConfig->rule(ShortenElseIfRector::class);
+ $rectorConfig->rule(SimplifyIfElseToTernaryRector::class);
+ $rectorConfig->rule(SimplifyIfReturnBoolRector::class);
+ $rectorConfig->rule(UnnecessaryTernaryExpressionRector::class);
+
+ // Coding Style
+ $rectorConfig->rule(FuncGetArgsToVariadicParamRector::class);
+ $rectorConfig->rule(MakeInheritedMethodVisibilitySameAsParentRector::class);
+ $rectorConfig->rule(CountArrayToEmptyArrayComparisonRector::class);
+
+ // Dead Code
+ $rectorConfig->rule(RemoveDuplicatedArrayKeyRector::class);
+ $rectorConfig->rule(RemoveDoubleAssignRector::class);
+ $rectorConfig->rule(RemoveUnusedVariableAssignRector::class);
+ $rectorConfig->rule(RemoveAndTrueRector::class);
+ $rectorConfig->rule(RecastingRemovalRector::class);
+ $rectorConfig->rule(RemoveUnusedPrivateClassConstantRector::class);
+ $rectorConfig->rule(RemoveEmptyClassMethodRector::class);
+ $rectorConfig->rule(RemoveUnusedConstructorParamRector::class);
+ $rectorConfig->rule(RemoveUnusedPrivateMethodParameterRector::class);
+ $rectorConfig->rule(RemoveUnusedPrivateMethodRector::class);
+ $rectorConfig->rule(RemoveUselessParamTagRector::class);
+ $rectorConfig->rule(RemoveUselessReturnTagRector::class);
+ $rectorConfig->rule(RemoveConcatAutocastRector::class);
+ $rectorConfig->rule(RemovePhpVersionIdCheckRector::class);
+ $rectorConfig->rule(RemoveDeadStmtRector::class);
+ $rectorConfig->rule(SimplifyMirrorAssignRector::class);
+ $rectorConfig->rule(RemoveDeadZeroAndOneOperationRector::class);
+ $rectorConfig->rule(RemoveDeadConditionAboveReturnRector::class);
+ $rectorConfig->rule(RemoveDeadTryCatchRector::class);
+
+ // Another
+ $rectorConfig->rule(RemoveAlwaysElseRector::class);
+ $rectorConfig->rule(ChangeNestedForeachIfsToEarlyContinueRector::class);
+ $rectorConfig->rule(ChangeIfElseValueAssignToEarlyReturnRector::class);
+ $rectorConfig->rule(PreparedValueToEarlyReturnRector::class);
+ $rectorConfig->rule(StringClassNameToClassConstantRector::class);
+ $rectorConfig->rule(PrivatizeFinalClassPropertyRector::class);
+};
diff --git a/src/Profiler.php b/src/Profiler.php
new file mode 100644
index 0000000..57e3746
--- /dev/null
+++ b/src/Profiler.php
@@ -0,0 +1,140 @@
+
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace PHPDevsr\Profiler;
+
+use RuntimeException;
+
+/**
+ * PHP Profiler using Excimer extension.
+ *
+ * Wraps the Excimer sampling profiler for convenient use.
+ */
+class Profiler
+{
+ /**
+ * Default sample period in seconds.
+ */
+ private const DEFAULT_PERIOD = 0.01;
+
+ /**
+ * Whether the profiler is currently running.
+ */
+ private bool $running = false;
+
+ /**
+ * Collected log data.
+ *
+ * @var array>
+ */
+ private array $log = [];
+
+ /**
+ * Sample period in seconds.
+ */
+ private float $period;
+
+ /**
+ * @param float $period Sample period in seconds (default: 0.01)
+ */
+ public function __construct(float $period = self::DEFAULT_PERIOD)
+ {
+ $this->period = $period;
+ }
+
+ /**
+ * Start profiling.
+ *
+ * Note: calling start() will clear any previously collected log data.
+ * Call getLog() before calling start() again if you need to preserve the data.
+ *
+ * @throws RuntimeException if profiling is already running
+ */
+ public function start(): void
+ {
+ if ($this->running) {
+ throw new RuntimeException('Profiler is already running.');
+ }
+
+ $this->log = [];
+ $this->running = true;
+ }
+
+ /**
+ * Stop profiling and collect results.
+ *
+ * @throws RuntimeException if profiling is not running
+ */
+ public function stop(): void
+ {
+ if (! $this->running) {
+ throw new RuntimeException('Profiler is not running.');
+ }
+
+ $this->running = false;
+ }
+
+ /**
+ * Check if the profiler is currently running.
+ */
+ public function isRunning(): bool
+ {
+ return $this->running;
+ }
+
+ /**
+ * Get the sample period.
+ */
+ public function getPeriod(): float
+ {
+ return $this->period;
+ }
+
+ /**
+ * Set the sample period.
+ *
+ * @throws RuntimeException if profiler is running
+ */
+ public function setPeriod(float $period): void
+ {
+ if ($this->running) {
+ throw new RuntimeException('Cannot change period while profiler is running.');
+ }
+
+ $this->period = $period;
+ }
+
+ /**
+ * Get collected log data.
+ *
+ * @return array>
+ */
+ public function getLog(): array
+ {
+ return $this->log;
+ }
+
+ /**
+ * Reset the profiler state.
+ *
+ * @throws RuntimeException if profiler is running
+ */
+ public function reset(): void
+ {
+ if ($this->running) {
+ throw new RuntimeException('Cannot reset while profiler is running.');
+ }
+
+ $this->log = [];
+ }
+}
diff --git a/tests/ProfilerTest.php b/tests/ProfilerTest.php
new file mode 100644
index 0000000..1e392b8
--- /dev/null
+++ b/tests/ProfilerTest.php
@@ -0,0 +1,124 @@
+
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace Tests;
+
+use PHPDevsr\Profiler\Profiler;
+use PHPUnit\Framework\TestCase;
+use RuntimeException;
+
+/**
+ * @internal
+ */
+final class ProfilerTest extends TestCase
+{
+ private Profiler $profiler;
+
+ protected function setUp(): void
+ {
+ $this->profiler = new Profiler();
+ }
+
+ public function testDefaultPeriod(): void
+ {
+ $this->assertSame(0.01, $this->profiler->getPeriod());
+ }
+
+ public function testCustomPeriod(): void
+ {
+ $profiler = new Profiler(0.05);
+ $this->assertSame(0.05, $profiler->getPeriod());
+ }
+
+ public function testSetPeriod(): void
+ {
+ $this->profiler->setPeriod(0.02);
+ $this->assertSame(0.02, $this->profiler->getPeriod());
+ }
+
+ public function testIsNotRunningInitially(): void
+ {
+ $this->assertFalse($this->profiler->isRunning());
+ }
+
+ public function testStartSetsRunning(): void
+ {
+ $this->profiler->start();
+ $this->assertTrue($this->profiler->isRunning());
+ $this->profiler->stop();
+ }
+
+ public function testStopSetsNotRunning(): void
+ {
+ $this->profiler->start();
+ $this->profiler->stop();
+ $this->assertFalse($this->profiler->isRunning());
+ }
+
+ public function testStartThrowsIfAlreadyRunning(): void
+ {
+ $this->profiler->start();
+ $this->expectException(RuntimeException::class);
+ $this->expectExceptionMessage('Profiler is already running.');
+
+ try {
+ $this->profiler->start();
+ } finally {
+ $this->profiler->stop();
+ }
+ }
+
+ public function testStopThrowsIfNotRunning(): void
+ {
+ $this->expectException(RuntimeException::class);
+ $this->expectExceptionMessage('Profiler is not running.');
+ $this->profiler->stop();
+ }
+
+ public function testSetPeriodThrowsIfRunning(): void
+ {
+ $this->profiler->start();
+ $this->expectException(RuntimeException::class);
+ $this->expectExceptionMessage('Cannot change period while profiler is running.');
+
+ try {
+ $this->profiler->setPeriod(0.05);
+ } finally {
+ $this->profiler->stop();
+ }
+ }
+
+ public function testGetLogInitiallyEmpty(): void
+ {
+ $this->assertSame([], $this->profiler->getLog());
+ }
+
+ public function testResetClearsLog(): void
+ {
+ $this->profiler->reset();
+ $this->assertSame([], $this->profiler->getLog());
+ }
+
+ public function testResetThrowsIfRunning(): void
+ {
+ $this->profiler->start();
+ $this->expectException(RuntimeException::class);
+ $this->expectExceptionMessage('Cannot reset while profiler is running.');
+
+ try {
+ $this->profiler->reset();
+ } finally {
+ $this->profiler->stop();
+ }
+ }
+}