Compare commits
No commits in common. "main" and "7.0.2" have entirely different histories.
|
|
@ -8,7 +8,6 @@
|
|||
/infection.json5.dist export-ignore
|
||||
/phpstan.neon export-ignore
|
||||
/phpunit.xml export-ignore
|
||||
/renovate.json export-ignore
|
||||
/tests export-ignore
|
||||
/tools export-ignore
|
||||
/tools/* binary
|
||||
|
|
|
|||
|
|
@ -1,26 +0,0 @@
|
|||
# https://github.com/marketplace/actions/api-surface-check-for-php
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- ".editorconfig"
|
||||
- "LICENSE"
|
||||
- "**.md"
|
||||
- "**.png"
|
||||
- "**.svg"
|
||||
- "tests/**"
|
||||
|
||||
name: "API Surface Check"
|
||||
|
||||
permissions: {}
|
||||
|
||||
concurrency:
|
||||
group: api-surface-${{ github.event.pull_request.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
signal:
|
||||
name: Signal
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: "true"
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
# https://github.com/marketplace/actions/api-surface-check-for-php
|
||||
|
||||
on:
|
||||
# zizmor: ignore[dangerous-triggers] workflow_run is required to comment on
|
||||
# PRs from forks; this workflow only runs a pinned action and does not check
|
||||
# out PR-controlled code.
|
||||
workflow_run:
|
||||
workflows: ["API Surface Check"]
|
||||
types: [completed]
|
||||
|
||||
name: "API Surface Comment"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: api-surface-comment-${{ github.event.workflow_run.head_repository.full_name }}-${{ github.event.workflow_run.head_branch }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
check-api-surface:
|
||||
name: Check API Surface
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: write # required to post the API surface diff comment on the PR
|
||||
steps:
|
||||
- uses: composer/api-surface-check@bba9f1cb940df6e02271b1b6158679ac0b3f39d3 # 1.0.1
|
||||
|
|
@ -1,243 +0,0 @@
|
|||
# https://docs.github.com/en/actions
|
||||
|
||||
on:
|
||||
- pull_request
|
||||
- push
|
||||
|
||||
name: CI
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
COMPOSER_ROOT_VERSION: 8.2.x-dev
|
||||
PHP_VERSION: 8.4
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
coding-guidelines:
|
||||
name: Coding Guidelines
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
fetch-depth: 1
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
persist-credentials: false
|
||||
|
||||
- name: Use local branch
|
||||
shell: bash
|
||||
env:
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
HEAD_REF: ${{ github.head_ref }}
|
||||
REF_NAME: ${{ github.ref_name }}
|
||||
run: |
|
||||
if [ "$EVENT_NAME" = "pull_request" ]; then
|
||||
BRANCH="$HEAD_REF"
|
||||
else
|
||||
BRANCH="$REF_NAME"
|
||||
fi
|
||||
git branch -D "$BRANCH" 2>/dev/null || true
|
||||
git branch "$BRANCH" HEAD
|
||||
git checkout "$BRANCH"
|
||||
|
||||
- name: Install PHP
|
||||
uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2
|
||||
with:
|
||||
php-version: ${{ env.PHP_VERSION }}
|
||||
coverage: none
|
||||
tools: none
|
||||
|
||||
- name: Run PHP-CS-Fixer
|
||||
run: ./tools/php-cs-fixer fix --dry-run --show-progress=dots --using-cache=no --verbose
|
||||
|
||||
static-analysis:
|
||||
name: Static Analysis
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
fetch-depth: 1
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
persist-credentials: false
|
||||
|
||||
- name: Use local branch
|
||||
shell: bash
|
||||
env:
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
HEAD_REF: ${{ github.head_ref }}
|
||||
REF_NAME: ${{ github.ref_name }}
|
||||
run: |
|
||||
if [ "$EVENT_NAME" = "pull_request" ]; then
|
||||
BRANCH="$HEAD_REF"
|
||||
else
|
||||
BRANCH="$REF_NAME"
|
||||
fi
|
||||
git branch -D "$BRANCH" 2>/dev/null || true
|
||||
git branch "$BRANCH" HEAD
|
||||
git checkout "$BRANCH"
|
||||
|
||||
- name: Install PHP
|
||||
uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2
|
||||
with:
|
||||
php-version: ${{ env.PHP_VERSION }}
|
||||
coverage: none
|
||||
tools: none
|
||||
|
||||
- name: Install dependencies with Composer
|
||||
run: ./tools/composer update --no-interaction --no-ansi --no-progress
|
||||
|
||||
- name: Run PHPStan
|
||||
run: ./tools/phpstan analyse --no-progress --error-format=github
|
||||
|
||||
tests:
|
||||
name: Tests
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
php-version:
|
||||
- 8.4
|
||||
- 8.5
|
||||
- 8.6
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
fetch-depth: 1
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
persist-credentials: false
|
||||
|
||||
- name: Use local branch
|
||||
shell: bash
|
||||
env:
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
HEAD_REF: ${{ github.head_ref }}
|
||||
REF_NAME: ${{ github.ref_name }}
|
||||
run: |
|
||||
if [ "$EVENT_NAME" = "pull_request" ]; then
|
||||
BRANCH="$HEAD_REF"
|
||||
else
|
||||
BRANCH="$REF_NAME"
|
||||
fi
|
||||
git branch -D "$BRANCH" 2>/dev/null || true
|
||||
git branch "$BRANCH" HEAD
|
||||
git checkout "$BRANCH"
|
||||
|
||||
- name: Install PHP with extensions
|
||||
uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2
|
||||
with:
|
||||
php-version: ${{ matrix.php-version }}
|
||||
coverage: none
|
||||
tools: none
|
||||
|
||||
- name: Get Composer cache directory
|
||||
id: composer-cache
|
||||
shell: bash
|
||||
run: |
|
||||
echo "dir=$(composer config cache-files-dir)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Cache Composer cache directory
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: ${{ steps.composer-cache.outputs.dir }}
|
||||
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
|
||||
restore-keys: ${{ runner.os }}-composer-
|
||||
|
||||
- name: Install dependencies with Composer
|
||||
run: ./tools/composer update --no-ansi --no-interaction --no-progress
|
||||
|
||||
- name: Run tests with PHPUnit
|
||||
run: vendor/bin/phpunit
|
||||
|
||||
code-coverage:
|
||||
name: Code Coverage
|
||||
|
||||
needs:
|
||||
- tests
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
fetch-depth: 1
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
persist-credentials: false
|
||||
|
||||
- name: Use local branch
|
||||
shell: bash
|
||||
env:
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
HEAD_REF: ${{ github.head_ref }}
|
||||
REF_NAME: ${{ github.ref_name }}
|
||||
run: |
|
||||
if [ "$EVENT_NAME" = "pull_request" ]; then
|
||||
BRANCH="$HEAD_REF"
|
||||
else
|
||||
BRANCH="$REF_NAME"
|
||||
fi
|
||||
git branch -D "$BRANCH" 2>/dev/null || true
|
||||
git branch "$BRANCH" HEAD
|
||||
git checkout "$BRANCH"
|
||||
|
||||
- name: Install PHP with extensions
|
||||
uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2
|
||||
with:
|
||||
php-version: ${{ matrix.php-version }}
|
||||
coverage: xdebug
|
||||
tools: none
|
||||
|
||||
- name: Get Composer cache directory
|
||||
id: composer-cache
|
||||
shell: bash
|
||||
run: |
|
||||
echo "dir=$(composer config cache-files-dir)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Cache Composer cache directory
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: ${{ steps.composer-cache.outputs.dir }}
|
||||
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
|
||||
restore-keys: ${{ runner.os }}-composer-
|
||||
|
||||
- name: Install dependencies with Composer
|
||||
run: ./tools/composer update --no-ansi --no-interaction --no-progress
|
||||
|
||||
- name: Collect code coverage with PHPUnit
|
||||
run: vendor/bin/phpunit --log-junit test-results.xml --coverage-openclover=code-coverage.xml
|
||||
|
||||
- name: Upload test results to Codecov.io
|
||||
if: ${{ !cancelled() }}
|
||||
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
report_type: test_results
|
||||
disable_search: true
|
||||
files: ./test-results.xml
|
||||
|
||||
- name: Upload code coverage data to Codecov.io
|
||||
if: ${{ !cancelled() }}
|
||||
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
report_type: coverage
|
||||
disable_search: true
|
||||
files: ./code-coverage.xml
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
# https://help.github.com/en/categories/automating-your-workflow-with-github-actions
|
||||
|
||||
on:
|
||||
- pull_request
|
||||
- push
|
||||
|
||||
name: CI
|
||||
|
||||
env:
|
||||
COMPOSER_ROOT_VERSION: 7.0.x-dev
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
coding-guidelines:
|
||||
name: Coding Guidelines
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: 8.3
|
||||
coverage: none
|
||||
|
||||
- name: Run PHP-CS-Fixer
|
||||
run: ./tools/php-cs-fixer fix --dry-run --show-progress=dots --using-cache=no --verbose
|
||||
|
||||
static-analysis:
|
||||
name: Static Analysis
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: 8.3
|
||||
coverage: none
|
||||
|
||||
- name: Install dependencies with Composer
|
||||
run: ./tools/composer update --no-interaction --no-ansi --no-progress
|
||||
|
||||
- name: Run PHPStan
|
||||
run: ./tools/phpstan analyse --no-progress --error-format=github
|
||||
|
||||
tests:
|
||||
name: Tests
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
php-version:
|
||||
- 8.3
|
||||
- 8.4
|
||||
- 8.5
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install PHP with extensions
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: ${{ matrix.php-version }}
|
||||
coverage: xdebug
|
||||
|
||||
- name: Install dependencies with Composer
|
||||
run: ./tools/composer update --no-ansi --no-interaction --no-progress
|
||||
|
||||
- name: Run tests with PHPUnit
|
||||
run: ./vendor/bin/phpunit --log-junit test-results.xml --coverage-clover=code-coverage.xml
|
||||
|
||||
- name: Upload test results to Codecov.io
|
||||
if: ${{ !cancelled() }}
|
||||
uses: codecov/test-results-action@v1
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
disable_search: true
|
||||
files: ./test-results.xml
|
||||
|
||||
- name: Upload code coverage data to Codecov.io
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
disable_search: true
|
||||
files: ./code-coverage.xml
|
||||
|
|
@ -7,13 +7,6 @@ on:
|
|||
|
||||
name: Release
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Release
|
||||
|
|
@ -21,16 +14,14 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
|
||||
permissions:
|
||||
contents: write # required to create GitHub releases
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install PHP with extensions
|
||||
uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: 8.3
|
||||
coverage: none
|
||||
|
|
@ -38,12 +29,15 @@ jobs:
|
|||
tools: none
|
||||
|
||||
- name: Determine tag
|
||||
run: echo "RELEASE_TAG=${GITHUB_REF#refs/tags/}" >> "$GITHUB_ENV"
|
||||
run: echo "RELEASE_TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
|
||||
|
||||
- name: Parse ChangeLog
|
||||
run: build/scripts/extract-release-notes.php "${RELEASE_TAG}" > release-notes.md
|
||||
run: build/scripts/extract-release-notes.php ${{ env.RELEASE_TAG }} > release-notes.md
|
||||
|
||||
- name: Create release
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: gh release create "${RELEASE_TAG}" --title "sebastian/exporter ${RELEASE_TAG}" --notes-file release-notes.md
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
tag: ${{ env.RELEASE_TAG }}
|
||||
name: sebastian/exporter ${{ env.RELEASE_TAG }}
|
||||
bodyFile: release-notes.md
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<phive xmlns="https://phar.io/phive">
|
||||
<phar name="php-cs-fixer" version="^3.71" installed="3.95.1" location="./tools/php-cs-fixer" copy="true"/>
|
||||
<phar name="php-cs-fixer" version="^3.71" installed="3.87.2" location="./tools/php-cs-fixer" copy="true"/>
|
||||
<phar name="composer" version="^2.8" installed="2.8.12" location="./tools/composer" copy="true"/>
|
||||
<phar name="infection" version="^0.29" installed="0.29.14" location="./tools/infection" copy="true"/>
|
||||
</phive>
|
||||
|
|
|
|||
58
ChangeLog.md
58
ChangeLog.md
|
|
@ -2,55 +2,6 @@
|
|||
|
||||
All notable changes are documented in this file using the [Keep a CHANGELOG](https://keepachangelog.com/) principles.
|
||||
|
||||
## [8.2.0] - 2026-08-07
|
||||
|
||||
### Added
|
||||
|
||||
* `ObjectExporter` interface, `ObjectExporterChain`, and `ExportContext` for customizing how objects are exported (an object exporter can be passed to `Exporter`'s constructor and is consulted before the default representation of an object is used)
|
||||
* `Exporter::shortenedExport()` and `Exporter::shortenedRecursiveExport()` use the representation provided by an object exporter as well; it is collapsed to a single line and shortened when it is longer than the configured maximum length
|
||||
* `Exporter::hasCustomRepresentationFor()` for checking whether an object exporter provides the representation for an object
|
||||
|
||||
## [8.1.1] - 2026-07-13
|
||||
|
||||
### Fixed
|
||||
|
||||
* A lone `\n` (LF) followed by a lone `\r` (CR) was incorrectly treated as a single `\n\r` line ending when exporting a string; these are now treated as two separate line endings
|
||||
|
||||
## [8.1.0] - 2026-05-21
|
||||
|
||||
### Changed
|
||||
|
||||
* [#57](https://github.com/sebastianbergmann/exporter/issues/57): Preserve `SplObjectStorage` iterator position instead of resetting to first element
|
||||
* [#91](https://github.com/sebastianbergmann/exporter/pull/91): Make binary string output readable for mostly-printable values
|
||||
|
||||
## [8.0.3] - 2026-05-20
|
||||
|
||||
### Fixed
|
||||
|
||||
* [#90](https://github.com/sebastianbergmann/exporter/issues/90): `Exporter::toArray()` silently drops a private property that is redeclared in a derived class
|
||||
|
||||
## [8.0.2] - 2026-04-15
|
||||
|
||||
* [#74](https://github.com/sebastianbergmann/exporter/pull/74): Int cast warning when exporting large floats
|
||||
|
||||
## [8.0.1] - 2026-04-10
|
||||
|
||||
### Changed
|
||||
|
||||
* Explicitly handle `NAN`, `INF`, and `-INF` in `exportFloat()`
|
||||
|
||||
## [8.0.0] - 2026-02-06
|
||||
|
||||
### Removed
|
||||
|
||||
* This component is no longer supported on PHP 8.3
|
||||
|
||||
## [7.0.3] - 2026-05-20
|
||||
|
||||
### Fixed
|
||||
|
||||
* [#90](https://github.com/sebastianbergmann/exporter/issues/90): `Exporter::toArray()` silently drops a private property that is redeclared in a derived class
|
||||
|
||||
## [7.0.2] - 2025-09-24
|
||||
|
||||
### Changed
|
||||
|
|
@ -69,15 +20,6 @@ All notable changes are documented in this file using the [Keep a CHANGELOG](htt
|
|||
|
||||
* This component is no longer supported on PHP 8.2
|
||||
|
||||
[8.2.0]: https://github.com/sebastianbergmann/exporter/compare/8.1.1...main
|
||||
[8.1.1]: https://github.com/sebastianbergmann/exporter/compare/8.1.0...8.1.1
|
||||
[8.1.0]: https://github.com/sebastianbergmann/exporter/compare/8.0.3...8.1.0
|
||||
[8.0.3]: https://github.com/sebastianbergmann/exporter/compare/8.0.2...8.0.3
|
||||
[8.0.2]: https://github.com/sebastianbergmann/exporter/compare/8.0.1...8.0.2
|
||||
[8.0.1]: https://github.com/sebastianbergmann/exporter/compare/8.0.0...8.0.1
|
||||
[8.0.0]: https://github.com/sebastianbergmann/exporter/compare/7.0...8.0.0
|
||||
[7.0.3]: https://github.com/sebastianbergmann/exporter/compare/7.0.2...7.0
|
||||
[7.0.3]: https://github.com/sebastianbergmann/exporter/compare/7.0.2...7.0.3
|
||||
[7.0.2]: https://github.com/sebastianbergmann/exporter/compare/7.0.1...7.0.2
|
||||
[7.0.1]: https://github.com/sebastianbergmann/exporter/compare/7.0.0...7.0.1
|
||||
[7.0.0]: https://github.com/sebastianbergmann/exporter/compare/6.3...7.0.0
|
||||
|
|
|
|||
2
LICENSE
2
LICENSE
|
|
@ -1,6 +1,6 @@
|
|||
BSD 3-Clause License
|
||||
|
||||
Copyright (c) 2002-2026, Sebastian Bergmann
|
||||
Copyright (c) 2002-2025, Sebastian Bergmann
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
|
|
|
|||
276
README.md
276
README.md
|
|
@ -25,19 +25,19 @@ composer require --dev sebastian/exporter
|
|||
Exporting:
|
||||
|
||||
```php
|
||||
<?php declare(strict_types=1);
|
||||
<?php
|
||||
use SebastianBergmann\Exporter\Exporter;
|
||||
|
||||
$exporter = new Exporter;
|
||||
|
||||
/*
|
||||
Exception Object #4 (
|
||||
'message' => '',
|
||||
'string' => '',
|
||||
'code' => 0,
|
||||
'file' => '/home/sebastianbergmann/test.php',
|
||||
'line' => 34,
|
||||
'previous' => null,
|
||||
Exception Object &0000000078de0f0d000000002003a261 (
|
||||
'message' => ''
|
||||
'string' => ''
|
||||
'code' => 0
|
||||
'file' => '/home/sebastianbergmann/test.php'
|
||||
'line' => 34
|
||||
'previous' => null
|
||||
)
|
||||
*/
|
||||
|
||||
|
|
@ -49,7 +49,7 @@ print $exporter->export(new Exception);
|
|||
Exporting simple types:
|
||||
|
||||
```php
|
||||
<?php declare(strict_types=1);
|
||||
<?php
|
||||
use SebastianBergmann\Exporter\Exporter;
|
||||
|
||||
$exporter = new Exporter;
|
||||
|
|
@ -85,66 +85,64 @@ print $exporter->export(chr(0) . chr(1) . chr(2) . chr(3) . chr(4) . chr(5));
|
|||
Exporting complex types:
|
||||
|
||||
```php
|
||||
<?php declare(strict_types=1);
|
||||
<?php
|
||||
use SebastianBergmann\Exporter\Exporter;
|
||||
|
||||
$exporter = new Exporter;
|
||||
|
||||
/*
|
||||
Array &0 [
|
||||
0 => Array &1 [
|
||||
0 => 1,
|
||||
1 => 2,
|
||||
2 => 3,
|
||||
],
|
||||
1 => Array &2 [
|
||||
0 => '',
|
||||
1 => 0,
|
||||
2 => false,
|
||||
],
|
||||
]
|
||||
*/
|
||||
|
||||
print $exporter->export([[1, 2, 3], ['', 0, false]]);
|
||||
|
||||
/*
|
||||
Array &0 [
|
||||
'self' => Array &1 [
|
||||
'self' => Array &1,
|
||||
],
|
||||
]
|
||||
*/
|
||||
|
||||
$array = [];
|
||||
$array['self'] = &$array;
|
||||
|
||||
print $exporter->export($array);
|
||||
|
||||
/*
|
||||
stdClass Object #4 (
|
||||
'self' => stdClass Object #4,
|
||||
Array &0 (
|
||||
0 => Array &1 (
|
||||
0 => 1
|
||||
1 => 2
|
||||
2 => 3
|
||||
)
|
||||
1 => Array &2 (
|
||||
0 => ''
|
||||
1 => 0
|
||||
2 => false
|
||||
)
|
||||
)
|
||||
*/
|
||||
|
||||
$obj = new stdClass;
|
||||
$obj->self = $obj;
|
||||
print $exporter->export(array(array(1,2,3), array("",0,FALSE)));
|
||||
|
||||
/*
|
||||
Array &0 (
|
||||
'self' => Array &1 (
|
||||
'self' => Array &1
|
||||
)
|
||||
)
|
||||
*/
|
||||
|
||||
$array = array();
|
||||
$array['self'] = &$array;
|
||||
print $exporter->export($array);
|
||||
|
||||
/*
|
||||
stdClass Object &0000000003a66dcc0000000025e723e2 (
|
||||
'self' => stdClass Object &0000000003a66dcc0000000025e723e2
|
||||
)
|
||||
*/
|
||||
|
||||
$obj = new stdClass();
|
||||
$obj->self = $obj;
|
||||
print $exporter->export($obj);
|
||||
```
|
||||
|
||||
Compact exports:
|
||||
|
||||
```php
|
||||
<?php declare(strict_types=1);
|
||||
<?php
|
||||
use SebastianBergmann\Exporter\Exporter;
|
||||
|
||||
$exporter = new Exporter;
|
||||
|
||||
// []
|
||||
print $exporter->shortenedExport([]);
|
||||
// Array ()
|
||||
print $exporter->shortenedExport(array());
|
||||
|
||||
// [...]
|
||||
print $exporter->shortenedExport([1, 2, 3, 4, 5]);
|
||||
// Array (...)
|
||||
print $exporter->shortenedExport(array(1,2,3,4,5));
|
||||
|
||||
// stdClass Object ()
|
||||
print $exporter->shortenedExport(new stdClass);
|
||||
|
|
@ -152,7 +150,7 @@ print $exporter->shortenedExport(new stdClass);
|
|||
// Exception Object (...)
|
||||
print $exporter->shortenedExport(new Exception);
|
||||
|
||||
// 'this\nis\na\nsuper\nlong\nstr...nspace'
|
||||
// this\nis\na\nsuper\nlong\nstring\nt...\nspace
|
||||
print $exporter->shortenedExport(
|
||||
<<<LONG_STRING
|
||||
this
|
||||
|
|
@ -175,181 +173,3 @@ space
|
|||
LONG_STRING
|
||||
);
|
||||
```
|
||||
|
||||
## Customizing How Objects Are Exported
|
||||
|
||||
By default, an object is exported property by property. Implement the `ObjectExporter` interface to control how objects of a type are represented:
|
||||
|
||||
```php
|
||||
<?php declare(strict_types=1);
|
||||
use SebastianBergmann\Exporter\ExportContext;
|
||||
use SebastianBergmann\Exporter\Exporter;
|
||||
use SebastianBergmann\Exporter\ObjectExporter;
|
||||
|
||||
final class MoneyExporter implements ObjectExporter
|
||||
{
|
||||
public function handles(object $object): bool
|
||||
{
|
||||
return $object instanceof Money;
|
||||
}
|
||||
|
||||
public function export(object $object, Exporter $exporter, int $indentation, ExportContext $context): string
|
||||
{
|
||||
assert($object instanceof Money);
|
||||
|
||||
return sprintf('Money (%d %s)', $object->amount(), $object->currency());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
An object exporter is registered by passing it to `Exporter`'s constructor. It is consulted before the default representation of an object is used:
|
||||
|
||||
```php
|
||||
<?php declare(strict_types=1);
|
||||
use SebastianBergmann\Exporter\Exporter;
|
||||
|
||||
$exporter = new Exporter(objectExporter: new MoneyExporter);
|
||||
|
||||
// Money (1999 EUR)
|
||||
print $exporter->export(new Money(1999, 'EUR'));
|
||||
|
||||
/*
|
||||
stdClass Object #6 (
|
||||
'net' => Money (1999 EUR),
|
||||
'gross' => Money (2379 EUR),
|
||||
)
|
||||
*/
|
||||
|
||||
$price = new stdClass;
|
||||
$price->net = new Money(1999, 'EUR');
|
||||
$price->gross = new Money(2379, 'EUR');
|
||||
|
||||
print $exporter->export($price);
|
||||
```
|
||||
|
||||
`ObjectExporterChain` composes multiple object exporters into a single one. They are consulted in the order in which they are composed into the chain and the first one whose `handles()` method returns `true` is asked for the representation:
|
||||
|
||||
```php
|
||||
<?php declare(strict_types=1);
|
||||
use SebastianBergmann\Exporter\Exporter;
|
||||
use SebastianBergmann\Exporter\ObjectExporterChain;
|
||||
|
||||
$exporter = new Exporter(
|
||||
objectExporter: new ObjectExporterChain(
|
||||
[
|
||||
new BasketExporter,
|
||||
new MoneyExporter,
|
||||
],
|
||||
),
|
||||
);
|
||||
```
|
||||
|
||||
Enums are objects and are therefore passed to the object exporters as well. When no object exporter handles an object, the default representation is used for it.
|
||||
|
||||
### Repeated Occurrences
|
||||
|
||||
When the same object occurs more than once, the default representation is only used for the first occurrence; subsequent occurrences are replaced with a reference to the object:
|
||||
|
||||
```php
|
||||
/*
|
||||
Array &0 [
|
||||
0 => Money Object #8 (
|
||||
'amount' => 1999,
|
||||
'currency' => 'EUR',
|
||||
),
|
||||
1 => Money Object #8,
|
||||
]
|
||||
*/
|
||||
|
||||
$money = new Money(1999, 'EUR');
|
||||
|
||||
print (new Exporter)->export([$money, $money]);
|
||||
```
|
||||
|
||||
An object exporter is responsible for the entire representation of the object it handles. Every occurrence of such an object is therefore exported by it:
|
||||
|
||||
```php
|
||||
/*
|
||||
Array &0 [
|
||||
0 => Money (1999 EUR),
|
||||
1 => Money (1999 EUR),
|
||||
]
|
||||
*/
|
||||
|
||||
print $exporter->export([$money, $money]);
|
||||
```
|
||||
|
||||
The exception is an object that is (indirectly) nested in itself: it cannot be exported this way without recursing infinitely and is replaced with a reference to the object.
|
||||
|
||||
### Exporting Nested Values
|
||||
|
||||
An object exporter may use the `Exporter` it is given to export values that are nested in the object it handles. The `ExportContext` it is given must be passed on when it does. Otherwise, the export of these nested values starts over with an empty context and, for instance, assigns references to arrays that are already in use elsewhere in the same export:
|
||||
|
||||
```php
|
||||
<?php declare(strict_types=1);
|
||||
use SebastianBergmann\Exporter\ExportContext;
|
||||
use SebastianBergmann\Exporter\Exporter;
|
||||
use SebastianBergmann\Exporter\ObjectExporter;
|
||||
|
||||
final class BasketExporter implements ObjectExporter
|
||||
{
|
||||
public function handles(object $object): bool
|
||||
{
|
||||
return $object instanceof Basket;
|
||||
}
|
||||
|
||||
public function export(object $object, Exporter $exporter, int $indentation, ExportContext $context): string
|
||||
{
|
||||
assert($object instanceof Basket);
|
||||
|
||||
return 'Basket ' . $exporter->export($object->items(), $indentation, $context);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```php
|
||||
/*
|
||||
Array &0 [
|
||||
'basket' => Basket Array &1 [
|
||||
0 => Money (1999 EUR),
|
||||
],
|
||||
]
|
||||
*/
|
||||
|
||||
print $exporter->export(['basket' => new Basket([new Money(1999, 'EUR')])]);
|
||||
```
|
||||
|
||||
### Compact Exports
|
||||
|
||||
`Exporter::shortenedExport()` and `Exporter::shortenedRecursiveExport()` consult object exporters as well. The representation an object exporter provides is collapsed to a single line and shortened when it is longer than the configured maximum length:
|
||||
|
||||
```php
|
||||
<?php declare(strict_types=1);
|
||||
use SebastianBergmann\Exporter\Exporter;
|
||||
|
||||
$exporter = new Exporter(objectExporter: new MoneyExporter);
|
||||
|
||||
// Money (1999 EUR)
|
||||
print $exporter->shortenedExport(new Money(1999, 'EUR'));
|
||||
```
|
||||
|
||||
Bear in mind that these representations are used where a short, single-line, and stable string is required. The representation of a data set is built using `Exporter::shortenedRecursiveExport()`, for instance, and is part of the name of a test that uses a data provider. An object exporter should therefore provide a representation that is compact and that does not change from one export to the next.
|
||||
|
||||
### Custom Representations
|
||||
|
||||
`Exporter::hasCustomRepresentationFor()` tells whether an object exporter provides the representation for an object. This is meant for code that renders objects itself and wants to use the representation an object exporter provides when there is one:
|
||||
|
||||
```php
|
||||
<?php declare(strict_types=1);
|
||||
use SebastianBergmann\Exporter\Exporter;
|
||||
|
||||
$exporter = new Exporter(objectExporter: new MoneyExporter);
|
||||
|
||||
// true
|
||||
var_dump($exporter->hasCustomRepresentationFor(new Money(1999, 'EUR')));
|
||||
|
||||
// false
|
||||
var_dump($exporter->hasCustomRepresentationFor(new stdClass));
|
||||
```
|
||||
|
||||
Bear in mind that an object that is (indirectly) nested in itself is replaced with a reference to the object instead of being exported by an object exporter again.
|
||||
|
|
|
|||
32
SECURITY.md
32
SECURITY.md
|
|
@ -1,10 +1,30 @@
|
|||
# Security Policy
|
||||
|
||||
The library that is developed in this repository is a first-party dependency of [PHPUnit](https://github.com/sebastianbergmann/phpunit).
|
||||
[PHPUnit's Security Policy](https://github.com/sebastianbergmann/phpunit/security/policy) applies to this library as well.
|
||||
|
||||
If you believe you have found a security vulnerability in this library, please report it to me through coordinated disclosure.
|
||||
|
||||
Use my [PGP key](https://sebastian-bergmann.de/gpg.asc) for encrypted email, for example when your report includes proof-of-concept exploits against third-party systems.
|
||||
If you believe you have found a security vulnerability in the library that is developed in this repository, please report it to us through coordinated disclosure.
|
||||
|
||||
**Please do not report security vulnerabilities through public GitHub issues, discussions, or pull requests.**
|
||||
|
||||
Instead, please email `sebastian@phpunit.de`.
|
||||
|
||||
Please include as much of the information listed below as you can to help us better understand and resolve the issue:
|
||||
|
||||
* The type of issue
|
||||
* Full paths of source file(s) related to the manifestation of the issue
|
||||
* The location of the affected source code (tag/branch/commit or direct URL)
|
||||
* Any special configuration required to reproduce the issue
|
||||
* Step-by-step instructions to reproduce the issue
|
||||
* Proof-of-concept or exploit code (if possible)
|
||||
* Impact of the issue, including how an attacker might exploit the issue
|
||||
|
||||
This information will help us triage your report more quickly.
|
||||
|
||||
## Web Context
|
||||
|
||||
The library that is developed in this repository was either extracted from [PHPUnit](https://github.com/sebastianbergmann/phpunit) or developed specifically as a dependency for PHPUnit.
|
||||
|
||||
The library is developed with a focus on development environments and the command-line. No specific testing or hardening with regard to using the library in an HTTP or web context or with untrusted input data is performed. The library might also contain functionality that intentionally exposes internal application data for debugging purposes.
|
||||
|
||||
If the library is used in a web application, the application developer is responsible for filtering inputs or escaping outputs as necessary and for verifying that the used functionality is safe for use within the intended context.
|
||||
|
||||
Vulnerabilities specific to the use outside a development context will be fixed as applicable, provided that the fix does not have an averse effect on the primary use case for development purposes.
|
||||
|
||||
|
|
|
|||
|
|
@ -27,13 +27,6 @@
|
|||
<arg value="update"/>
|
||||
</exec>
|
||||
|
||||
<exec executable="${basedir}/tools/composer" taskname="composer">
|
||||
<arg value="self-update"/>
|
||||
<arg value="--no-interaction"/>
|
||||
<arg value="--no-progress"/>
|
||||
<arg value="--no-ansi"/>
|
||||
</exec>
|
||||
|
||||
<exec executable="${basedir}/tools/composer" dir="${basedir}/tools/.phpstan" taskname="composer">
|
||||
<arg value="update"/>
|
||||
<arg value="--no-interaction"/>
|
||||
|
|
|
|||
|
|
@ -32,20 +32,19 @@
|
|||
},
|
||||
"config": {
|
||||
"platform": {
|
||||
"php": "8.4.1"
|
||||
"php": "8.3.0"
|
||||
},
|
||||
"optimize-autoloader": true,
|
||||
"sort-packages": true
|
||||
},
|
||||
"minimum-stability": "dev",
|
||||
"prefer-stable": true,
|
||||
"require": {
|
||||
"php": ">=8.4",
|
||||
"php": ">=8.3",
|
||||
"ext-mbstring": "*",
|
||||
"sebastian/recursion-context": "^8.0"
|
||||
"sebastian/recursion-context": "^7.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^13.3"
|
||||
"phpunit/phpunit": "^12.0"
|
||||
},
|
||||
"autoload": {
|
||||
"classmap": [
|
||||
|
|
@ -59,7 +58,7 @@
|
|||
},
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-main": "8.2-dev"
|
||||
"dev-main": "7.0-dev"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,61 +0,0 @@
|
|||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"config:recommended",
|
||||
"helpers:pinGitHubActionDigestsToSemver",
|
||||
":configMigration",
|
||||
":maintainLockFilesWeekly"
|
||||
],
|
||||
"timezone": "Europe/Berlin",
|
||||
"baseBranchPatterns": [
|
||||
"main",
|
||||
"/^\\d+\\.\\d+$/"
|
||||
],
|
||||
"rebaseWhen": "conflicted",
|
||||
"prHourlyLimit": 2,
|
||||
"prConcurrentLimit": 10,
|
||||
"dependencyDashboard": true,
|
||||
"labels": ["type/dependencies"],
|
||||
"platformAutomerge": true,
|
||||
"osvVulnerabilityAlerts": true,
|
||||
"dependencyDashboardOSVVulnerabilitySummary": "all",
|
||||
"vulnerabilityAlerts": {
|
||||
"enabled": true,
|
||||
"labels": ["type/security", "type/dependencies"]
|
||||
},
|
||||
"lockFileMaintenance": {
|
||||
"enabled": true,
|
||||
"automerge": true,
|
||||
"schedule": ["before 6am on monday"]
|
||||
},
|
||||
"packageRules": [
|
||||
{
|
||||
"description": "Group GitHub Actions updates per branch into one PR, automerge",
|
||||
"matchManagers": ["github-actions"],
|
||||
"groupName": "github-actions",
|
||||
"automerge": true
|
||||
},
|
||||
{
|
||||
"description": "Automerge minor and patch updates after they have settled",
|
||||
"matchUpdateTypes": ["minor", "patch", "pin", "digest"],
|
||||
"automerge": true
|
||||
},
|
||||
{
|
||||
"description": "Do not update the PHP platform requirement in composer.json",
|
||||
"matchManagers": ["composer"],
|
||||
"matchDepNames": ["php"],
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"description": "Do not suggest major updates for Composer dependencies",
|
||||
"matchManagers": ["composer"],
|
||||
"matchUpdateTypes": ["major"],
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"description": "Bump composer.json ranges when updating dependencies",
|
||||
"matchManagers": ["composer"],
|
||||
"rangeStrategy": "bump"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,82 +0,0 @@
|
|||
<?php declare(strict_types=1);
|
||||
/*
|
||||
* This file is part of sebastian/exporter.
|
||||
*
|
||||
* (c) Sebastian Bergmann <sebastian@phpunit.de>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
namespace SebastianBergmann\Exporter;
|
||||
|
||||
use SebastianBergmann\RecursionContext\Context as RecursionContext;
|
||||
use SplObjectStorage;
|
||||
|
||||
/**
|
||||
* Carries the state that is shared by all steps of a single export operation.
|
||||
*
|
||||
* An implementation of ObjectExporter that exports values nested in the object
|
||||
* it handles must pass the instance of this class it is given to the Exporter
|
||||
* it delegates to. Otherwise, the export of these nested values starts over
|
||||
* with an empty context and, for instance, assigns references to arrays that
|
||||
* are already in use elsewhere in the same export.
|
||||
*/
|
||||
final class ExportContext
|
||||
{
|
||||
private RecursionContext $recursionContext;
|
||||
|
||||
/**
|
||||
* @var SplObjectStorage<object, null>
|
||||
*/
|
||||
private SplObjectStorage $exportedByObjectExporter;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->recursionContext = new RecursionContext;
|
||||
$this->exportedByObjectExporter = new SplObjectStorage;
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T of array|object
|
||||
*
|
||||
* @param T $value
|
||||
*
|
||||
* @param-out T $value
|
||||
*/
|
||||
public function add(array|object &$value): int
|
||||
{
|
||||
return $this->recursionContext->add($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T of array|object
|
||||
*
|
||||
* @param T $value
|
||||
*
|
||||
* @param-out T $value
|
||||
*/
|
||||
public function contains(array|object &$value): false|int
|
||||
{
|
||||
return $this->recursionContext->contains($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the export of an object by an ObjectExporter is in progress.
|
||||
*
|
||||
* This is the case when the object is (indirectly) nested in itself.
|
||||
*/
|
||||
public function isBeingExportedByObjectExporter(object $object): bool
|
||||
{
|
||||
return $this->exportedByObjectExporter->offsetExists($object);
|
||||
}
|
||||
|
||||
public function beginExportByObjectExporter(object $object): void
|
||||
{
|
||||
$this->exportedByObjectExporter->offsetSet($object);
|
||||
}
|
||||
|
||||
public function endExportByObjectExporter(object $object): void
|
||||
{
|
||||
$this->exportedByObjectExporter->offsetUnset($object);
|
||||
}
|
||||
}
|
||||
331
src/Exporter.php
331
src/Exporter.php
|
|
@ -21,24 +21,16 @@ use function ini_set;
|
|||
use function is_array;
|
||||
use function is_bool;
|
||||
use function is_float;
|
||||
use function is_infinite;
|
||||
use function is_nan;
|
||||
use function is_object;
|
||||
use function is_resource;
|
||||
use function is_string;
|
||||
use function mb_strlen;
|
||||
use function mb_substr;
|
||||
use function ord;
|
||||
use function preg_match;
|
||||
use function preg_match_all;
|
||||
use function preg_replace_callback;
|
||||
use function spl_object_id;
|
||||
use function sprintf;
|
||||
use function str_contains;
|
||||
use function str_repeat;
|
||||
use function str_replace;
|
||||
use function strlen;
|
||||
use function strpbrk;
|
||||
use function strtr;
|
||||
use function var_export;
|
||||
use BackedEnum;
|
||||
|
|
@ -61,17 +53,15 @@ final readonly class Exporter
|
|||
* @var positive-int
|
||||
*/
|
||||
private int $maxLengthForStrings;
|
||||
private ?ObjectExporter $objectExporter;
|
||||
|
||||
/**
|
||||
* @param non-negative-int $shortenArraysLongerThan
|
||||
* @param positive-int $maxLengthForStrings
|
||||
*/
|
||||
public function __construct(int $shortenArraysLongerThan = 0, int $maxLengthForStrings = 40, ?ObjectExporter $objectExporter = null)
|
||||
public function __construct(int $shortenArraysLongerThan = 0, int $maxLengthForStrings = 40)
|
||||
{
|
||||
$this->shortenArraysLongerThan = $shortenArraysLongerThan;
|
||||
$this->maxLengthForStrings = $maxLengthForStrings;
|
||||
$this->objectExporter = $objectExporter;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -86,23 +76,15 @@ final readonly class Exporter
|
|||
* - Strings are always quoted with single quotes
|
||||
* - Carriage returns and newlines are normalized to \n
|
||||
* - Recursion and repeated rendering is treated properly
|
||||
*
|
||||
* An implementation of ObjectExporter must pass the ExportContext it is
|
||||
* given to this method when it exports values that are nested in the
|
||||
* object it handles.
|
||||
*
|
||||
* @throws ObjectNotSupportedException
|
||||
*/
|
||||
public function export(mixed $value, int $indentation = 0, ?ExportContext $context = null): string
|
||||
public function export(mixed $value, int $indentation = 0): string
|
||||
{
|
||||
return $this->recursiveExport($value, $indentation, $context);
|
||||
return $this->recursiveExport($value, $indentation);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<mixed> $data
|
||||
* @param positive-int $maxLengthForStrings
|
||||
*
|
||||
* @throws ObjectNotSupportedException
|
||||
*/
|
||||
public function shortenedRecursiveExport(array &$data, int $maxLengthForStrings = 40, ?RecursionContext $processed = null): string
|
||||
{
|
||||
|
|
@ -136,13 +118,7 @@ final readonly class Exporter
|
|||
* Newlines are replaced by the visible string '\n'.
|
||||
* Contents of arrays and objects (if any) are replaced by '...'.
|
||||
*
|
||||
* The representation a custom object exporter provides for an object is
|
||||
* used, but it is collapsed to a single line and shortened when it is
|
||||
* longer than $maxLengthForStrings.
|
||||
*
|
||||
* @param positive-int $maxLengthForStrings
|
||||
*
|
||||
* @throws ObjectNotSupportedException
|
||||
*/
|
||||
public function shortenedExport(mixed $value, int $maxLengthForStrings = 40): string
|
||||
{
|
||||
|
|
@ -151,20 +127,13 @@ final readonly class Exporter
|
|||
}
|
||||
|
||||
if (is_string($value)) {
|
||||
return $this->shorten($this->exportString($value), $maxLengthForStrings);
|
||||
}
|
||||
$string = str_replace("\n", '', $this->exportString($value));
|
||||
|
||||
if ($this->objectExporter !== null &&
|
||||
is_object($value) &&
|
||||
$this->objectExporter->handles($value)) {
|
||||
$context = new ExportContext;
|
||||
if (mb_strlen($string) > $maxLengthForStrings) {
|
||||
return mb_substr($string, 0, $maxLengthForStrings - 10) . '...' . mb_substr($string, -7);
|
||||
}
|
||||
|
||||
$context->beginExportByObjectExporter($value);
|
||||
|
||||
return $this->shorten(
|
||||
$this->objectExporter->export($value, $this, 0, $context),
|
||||
$maxLengthForStrings,
|
||||
);
|
||||
return $string;
|
||||
}
|
||||
|
||||
if ($value instanceof BackedEnum) {
|
||||
|
|
@ -202,26 +171,6 @@ final readonly class Exporter
|
|||
return $this->export($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether a custom object exporter provides the representation
|
||||
* for an object.
|
||||
*
|
||||
* This is intended for code that renders objects itself and wants to use
|
||||
* the representation a custom object exporter provides when there is one.
|
||||
*
|
||||
* Bear in mind that an object that is (indirectly) nested in itself is
|
||||
* replaced with a reference to the object instead of being exported by a
|
||||
* custom object exporter again.
|
||||
*/
|
||||
public function hasCustomRepresentationFor(object $object): bool
|
||||
{
|
||||
if ($this->objectExporter === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->objectExporter->handles($object);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an object to an array containing all of its private, protected
|
||||
* and public properties.
|
||||
|
|
@ -234,43 +183,29 @@ final readonly class Exporter
|
|||
return (array) $value;
|
||||
}
|
||||
|
||||
$properties = (array) $value;
|
||||
$shadowed = $this->shadowedPropertyNames($properties);
|
||||
$array = [];
|
||||
|
||||
foreach ($properties as $key => $val) {
|
||||
$key = (string) $key;
|
||||
$array = [];
|
||||
|
||||
foreach ((array) $value as $key => $val) {
|
||||
// Exception traces commonly reference hundreds to thousands of
|
||||
// objects currently loaded in memory. Including them in the result
|
||||
// has a severe negative performance impact.
|
||||
if ($key === "\0Error\0trace" || $key === "\0Exception\0trace") {
|
||||
if ("\0Error\0trace" === $key || "\0Exception\0trace" === $key) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// properties are transformed to keys in the following way:
|
||||
// private $propertyName => "\0ClassName\0propertyName"
|
||||
// protected $propertyName => "\0*\0propertyName"
|
||||
// public $propertyName => "propertyName"
|
||||
if (preg_match('/\0.+\0(.+)/', (string) $key, $matches) === 1) {
|
||||
$key = $matches[1];
|
||||
}
|
||||
|
||||
// See https://github.com/php/php-src/commit/5721132
|
||||
if ($key === "\0gcdata") {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Properties are transformed to keys in the following way:
|
||||
// private $propertyName => "\0DeclaringClassName\0propertyName"
|
||||
// protected $propertyName => "\0*\0propertyName"
|
||||
// public $propertyName => "propertyName"
|
||||
//
|
||||
// A private property that is redeclared in a derived class and the
|
||||
// private property of the same name that it shadows both exist,
|
||||
// independently of each other. To keep one from overwriting the
|
||||
// other, the name of a shadowed private property is prefixed with
|
||||
// the name of the class that declares it.
|
||||
if (preg_match('/^\0([^\0]+)\0([^\0]+)$/', $key, $matches) === 1) {
|
||||
if ($matches[1] !== '*' && isset($shadowed[$matches[2]])) {
|
||||
$key = $matches[1] . '::' . $matches[2];
|
||||
} else {
|
||||
$key = $matches[2];
|
||||
}
|
||||
}
|
||||
|
||||
$array[$key] = $val;
|
||||
}
|
||||
|
||||
|
|
@ -278,12 +213,6 @@ final readonly class Exporter
|
|||
// above (fast) mechanism nor with reflection in Zend.
|
||||
// Format the output similarly to print_r() in this case
|
||||
if ($value instanceof SplObjectStorage) {
|
||||
$key = null;
|
||||
|
||||
if ($value->valid()) {
|
||||
$key = $value->key();
|
||||
}
|
||||
|
||||
foreach ($value as $_value) {
|
||||
$array['Object #' . spl_object_id($_value)] = [
|
||||
'obj' => $_value,
|
||||
|
|
@ -291,9 +220,7 @@ final readonly class Exporter
|
|||
];
|
||||
}
|
||||
|
||||
if ($key !== null) {
|
||||
$value->seek($key);
|
||||
}
|
||||
$value->rewind();
|
||||
}
|
||||
|
||||
return $array;
|
||||
|
|
@ -309,54 +236,15 @@ final readonly class Exporter
|
|||
|
||||
if (!$value instanceof stdClass) {
|
||||
// using ReflectionClass prevents initialization of potential lazy objects
|
||||
return count(new ReflectionClass($value)->getProperties());
|
||||
return count((new ReflectionClass($value))->getProperties());
|
||||
}
|
||||
|
||||
return count(new ReflectionObject($value)->getProperties());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns, as keys of the returned array, the names of properties that
|
||||
* are declared more than once in the inheritance chain of an object.
|
||||
*
|
||||
* This can only happen when a derived class redeclares a private property
|
||||
* that one of its parent classes also declares.
|
||||
*
|
||||
* @param array<array-key, mixed> $properties
|
||||
*
|
||||
* @return array<string, true>
|
||||
*/
|
||||
private function shadowedPropertyNames(array $properties): array
|
||||
{
|
||||
$seen = [];
|
||||
$shadowed = [];
|
||||
|
||||
foreach ($properties as $key => $unused) {
|
||||
$key = (string) $key;
|
||||
|
||||
if ($key === "\0Error\0trace" || $key === "\0Exception\0trace" || $key === "\0gcdata") {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (preg_match('/^\0[^\0]+\0([^\0]+)$/', $key, $matches) === 1) {
|
||||
$key = $matches[1];
|
||||
}
|
||||
|
||||
if (isset($seen[$key])) {
|
||||
$shadowed[$key] = true;
|
||||
}
|
||||
|
||||
$seen[$key] = true;
|
||||
}
|
||||
|
||||
return $shadowed;
|
||||
return count((new ReflectionObject($value))->getProperties());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<mixed> $data
|
||||
* @param positive-int $maxLengthForStrings
|
||||
*
|
||||
* @throws ObjectNotSupportedException
|
||||
*/
|
||||
private function shortenedCountedRecursiveExport(array &$data, RecursionContext $processed, int &$counter, int $maxLengthForStrings): string
|
||||
{
|
||||
|
|
@ -393,10 +281,7 @@ final readonly class Exporter
|
|||
return implode(', ', $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ObjectNotSupportedException
|
||||
*/
|
||||
private function recursiveExport(mixed &$value, int $indentation = 0, ?ExportContext $context = null): string
|
||||
private function recursiveExport(mixed &$value, int $indentation = 0, ?RecursionContext $processed = null): string
|
||||
{
|
||||
if ($value === null) {
|
||||
return 'null';
|
||||
|
|
@ -423,62 +308,55 @@ final readonly class Exporter
|
|||
);
|
||||
}
|
||||
|
||||
if ($value instanceof BackedEnum) {
|
||||
return sprintf(
|
||||
'%s Enum #%d (%s, %s)',
|
||||
$value::class,
|
||||
spl_object_id($value),
|
||||
$value->name,
|
||||
$this->export($value->value),
|
||||
);
|
||||
}
|
||||
|
||||
if ($value instanceof UnitEnum) {
|
||||
return sprintf(
|
||||
'%s Enum #%d (%s)',
|
||||
$value::class,
|
||||
spl_object_id($value),
|
||||
$value->name,
|
||||
);
|
||||
}
|
||||
|
||||
if (is_string($value)) {
|
||||
return $this->exportString($value);
|
||||
}
|
||||
|
||||
if ($context === null) {
|
||||
$context = new ExportContext;
|
||||
if ($processed === null) {
|
||||
$processed = new RecursionContext;
|
||||
}
|
||||
|
||||
if (is_array($value)) {
|
||||
return $this->exportArray($value, $context, $indentation);
|
||||
return $this->exportArray($value, $processed, $indentation);
|
||||
}
|
||||
|
||||
if (is_object($value)) {
|
||||
return $this->exportObject($value, $context, $indentation);
|
||||
return $this->exportObject($value, $processed, $indentation);
|
||||
}
|
||||
|
||||
return var_export($value, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Collapses a representation to a single line and shortens it when it is
|
||||
* longer than $maxLengthForStrings.
|
||||
*
|
||||
* @param positive-int $maxLengthForStrings
|
||||
*/
|
||||
private function shorten(string $string, int $maxLengthForStrings): string
|
||||
{
|
||||
$string = str_replace(["\r", "\n"], '', $string);
|
||||
|
||||
if (mb_strlen($string) > $maxLengthForStrings) {
|
||||
return mb_substr($string, 0, $maxLengthForStrings - 10) . '...' . mb_substr($string, -7);
|
||||
}
|
||||
|
||||
return $string;
|
||||
}
|
||||
|
||||
private function exportFloat(float $value): string
|
||||
{
|
||||
if (is_nan($value)) {
|
||||
return 'NAN';
|
||||
}
|
||||
|
||||
if (is_infinite($value)) {
|
||||
return $value > 0 ? 'INF' : '-INF';
|
||||
}
|
||||
|
||||
$precisionBackup = ini_get('precision');
|
||||
|
||||
ini_set('precision', '-1');
|
||||
|
||||
$valueAsString = (string) $value;
|
||||
$valueAsString = @(string) $value;
|
||||
|
||||
ini_set('precision', $precisionBackup);
|
||||
|
||||
// Add '.0' only if decimals and scientific notation are absent.
|
||||
if (strpbrk($valueAsString, '.E') === false) {
|
||||
if ((string) @(int) $value === $valueAsString) {
|
||||
return $valueAsString . '.0';
|
||||
}
|
||||
|
||||
|
|
@ -488,58 +366,34 @@ final readonly class Exporter
|
|||
private function exportString(string $value): string
|
||||
{
|
||||
// Match for most non-printable chars somewhat taking multibyte chars into account
|
||||
$unprintableCount = preg_match_all('/[^\x09-\x0d\x1b\x20-\xff]/', $value);
|
||||
|
||||
if ($unprintableCount === false || $unprintableCount === 0) {
|
||||
return "'" .
|
||||
strtr(
|
||||
$value,
|
||||
[
|
||||
"\r\n" => '\r\n' . "\n",
|
||||
"\r" => '\r' . "\n",
|
||||
"\n" => '\n' . "\n",
|
||||
],
|
||||
) .
|
||||
"'";
|
||||
}
|
||||
|
||||
// A NUL byte or a high ratio of unprintable bytes signals truly
|
||||
// binary data; keep the compact hex dump in those cases.
|
||||
if (str_contains($value, "\x00") || ($unprintableCount / strlen($value)) > 0.3) {
|
||||
if (preg_match('/[^\x09-\x0d\x1b\x20-\xff]/', $value) === 1) {
|
||||
return 'Binary String: 0x' . bin2hex($value);
|
||||
}
|
||||
|
||||
// Mostly printable: keep printable bytes visible and escape only
|
||||
// the offending ones inline using PHP-style \xNN escapes.
|
||||
return 'Binary String: "' .
|
||||
preg_replace_callback(
|
||||
'/[\x00-\x1f\x7f"\\\\]/',
|
||||
static fn (array $m): string => match ($m[0]) {
|
||||
"\t" => '\t',
|
||||
"\n" => '\n',
|
||||
"\r" => '\r',
|
||||
'"' => '\"',
|
||||
'\\' => '\\\\',
|
||||
default => sprintf('\x%02x', ord($m[0])),
|
||||
},
|
||||
return "'" .
|
||||
strtr(
|
||||
$value,
|
||||
[
|
||||
"\r\n" => '\r\n' . "\n",
|
||||
"\n\r" => '\n\r' . "\n",
|
||||
"\r" => '\r' . "\n",
|
||||
"\n" => '\n' . "\n",
|
||||
],
|
||||
) .
|
||||
'"';
|
||||
"'";
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<mixed> $value
|
||||
*
|
||||
* @throws ObjectNotSupportedException
|
||||
*/
|
||||
private function exportArray(array &$value, ExportContext $context, int $indentation): string
|
||||
private function exportArray(array &$value, RecursionContext $processed, int $indentation): string
|
||||
{
|
||||
if (($key = $context->contains($value)) !== false) {
|
||||
if (($key = $processed->contains($value)) !== false) {
|
||||
return 'Array &' . $key;
|
||||
}
|
||||
|
||||
$array = $value;
|
||||
$key = $context->add($value);
|
||||
$key = $processed->add($value);
|
||||
$values = '';
|
||||
|
||||
if (count($array) > 0) {
|
||||
|
|
@ -552,7 +406,7 @@ final readonly class Exporter
|
|||
$this->recursiveExport($k, $indentation)
|
||||
. ' => ' .
|
||||
/** @phpstan-ignore offsetAccess.invalidOffset */
|
||||
$this->recursiveExport($value[$k], $indentation + 1, $context)
|
||||
$this->recursiveExport($value[$k], $indentation + 1, $processed)
|
||||
. ",\n";
|
||||
}
|
||||
|
||||
|
|
@ -562,68 +416,15 @@ final readonly class Exporter
|
|||
return 'Array &' . (string) $key . ' [' . $values . ']';
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ObjectNotSupportedException
|
||||
*/
|
||||
private function exportObject(object $value, ExportContext $context, int $indentation): string
|
||||
private function exportObject(object $value, RecursionContext $processed, int $indentation): string
|
||||
{
|
||||
$class = $value::class;
|
||||
|
||||
if ($this->objectExporter !== null) {
|
||||
// An object that is (indirectly) nested in itself cannot be
|
||||
// exported by a custom object exporter without recursing
|
||||
// infinitely and is therefore replaced with a reference to the
|
||||
// object.
|
||||
if ($context->isBeingExportedByObjectExporter($value)) {
|
||||
return $class . ' Object #' . spl_object_id($value);
|
||||
}
|
||||
|
||||
// A custom object exporter is responsible for the entire
|
||||
// representation of the object it handles. Therefore, it is asked
|
||||
// for that representation before the recursion context is
|
||||
// consulted: every occurrence of such an object is exported the
|
||||
// same way instead of repeated occurrences being replaced with a
|
||||
// reference to the object.
|
||||
if ($this->objectExporter->handles($value)) {
|
||||
$context->beginExportByObjectExporter($value);
|
||||
|
||||
try {
|
||||
return $this->objectExporter->export($value, $this, $indentation, $context);
|
||||
} finally {
|
||||
$context->endExportByObjectExporter($value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Enums are handled after a custom object exporter has been consulted
|
||||
// so that the representation of an enum can be customized, but before
|
||||
// the recursion context is consulted because an enum case is a
|
||||
// singleton for which a reference to a previous occurrence would be
|
||||
// less informative than the representation itself.
|
||||
if ($value instanceof BackedEnum) {
|
||||
return sprintf(
|
||||
'%s Enum #%d (%s, %s)',
|
||||
$class,
|
||||
spl_object_id($value),
|
||||
$value->name,
|
||||
$this->export($value->value),
|
||||
);
|
||||
}
|
||||
|
||||
if ($value instanceof UnitEnum) {
|
||||
return sprintf(
|
||||
'%s Enum #%d (%s)',
|
||||
$class,
|
||||
spl_object_id($value),
|
||||
$value->name,
|
||||
);
|
||||
}
|
||||
|
||||
if ($context->contains($value) !== false) {
|
||||
if ($processed->contains($value) !== false) {
|
||||
return $class . ' Object #' . spl_object_id($value);
|
||||
}
|
||||
|
||||
$context->add($value);
|
||||
$processed->add($value);
|
||||
|
||||
$array = $this->toArray($value);
|
||||
$buffer = '';
|
||||
|
|
@ -637,7 +438,7 @@ final readonly class Exporter
|
|||
. ' ' .
|
||||
$this->recursiveExport($k, $indentation)
|
||||
. ' => ' .
|
||||
$this->recursiveExport($v, $indentation + 1, $context)
|
||||
$this->recursiveExport($v, $indentation + 1, $processed)
|
||||
. ",\n";
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,25 +0,0 @@
|
|||
<?php declare(strict_types=1);
|
||||
/*
|
||||
* This file is part of sebastian/exporter.
|
||||
*
|
||||
* (c) Sebastian Bergmann <sebastian@phpunit.de>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
namespace SebastianBergmann\Exporter;
|
||||
|
||||
interface ObjectExporter
|
||||
{
|
||||
public function handles(object $object): bool;
|
||||
|
||||
/**
|
||||
* Exports an object this object exporter handles.
|
||||
*
|
||||
* The ExportContext must be passed on to Exporter::export() when values
|
||||
* that are nested in the object are exported using $exporter.
|
||||
*
|
||||
* @throws ObjectNotSupportedException
|
||||
*/
|
||||
public function export(object $object, Exporter $exporter, int $indentation, ExportContext $context): string;
|
||||
}
|
||||
|
|
@ -1,50 +0,0 @@
|
|||
<?php declare(strict_types=1);
|
||||
/*
|
||||
* This file is part of sebastian/exporter.
|
||||
*
|
||||
* (c) Sebastian Bergmann <sebastian@phpunit.de>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
namespace SebastianBergmann\Exporter;
|
||||
|
||||
use function array_any;
|
||||
|
||||
final readonly class ObjectExporterChain implements ObjectExporter
|
||||
{
|
||||
/**
|
||||
* @var non-empty-list<ObjectExporter>
|
||||
*/
|
||||
private array $exporters;
|
||||
|
||||
/**
|
||||
* @param non-empty-list<ObjectExporter> $exporters
|
||||
*/
|
||||
public function __construct(array $exporters)
|
||||
{
|
||||
$this->exporters = $exporters;
|
||||
}
|
||||
|
||||
public function handles(object $object): bool
|
||||
{
|
||||
return array_any(
|
||||
$this->exporters,
|
||||
static fn (ObjectExporter $exporter) => $exporter->handles($object),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ObjectNotSupportedException
|
||||
*/
|
||||
public function export(object $object, Exporter $exporter, int $indentation, ExportContext $context): string
|
||||
{
|
||||
foreach ($this->exporters as $objectExporter) {
|
||||
if ($objectExporter->handles($object)) {
|
||||
return $objectExporter->export($object, $exporter, $indentation, $context);
|
||||
}
|
||||
}
|
||||
|
||||
throw new ObjectNotSupportedException;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
<?php declare(strict_types=1);
|
||||
/*
|
||||
* This file is part of sebastian/exporter.
|
||||
*
|
||||
* (c) Sebastian Bergmann <sebastian@phpunit.de>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
namespace SebastianBergmann\Exporter;
|
||||
|
||||
use Throwable;
|
||||
|
||||
interface Exception extends Throwable
|
||||
{
|
||||
}
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
<?php declare(strict_types=1);
|
||||
/*
|
||||
* This file is part of sebastian/exporter.
|
||||
*
|
||||
* (c) Sebastian Bergmann <sebastian@phpunit.de>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
namespace SebastianBergmann\Exporter;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
final class ObjectNotSupportedException extends RuntimeException implements Exception
|
||||
{
|
||||
}
|
||||
|
|
@ -21,6 +21,7 @@ use function is_resource;
|
|||
use function is_string;
|
||||
use function mb_internal_encoding;
|
||||
use function mb_language;
|
||||
use function method_exists;
|
||||
use function preg_replace;
|
||||
use function range;
|
||||
use function str_repeat;
|
||||
|
|
@ -28,9 +29,9 @@ use Error;
|
|||
use Exception;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\Attributes\RequiresPhp;
|
||||
use PHPUnit\Framework\Attributes\RequiresPhpExtension;
|
||||
use PHPUnit\Framework\Attributes\Small;
|
||||
use PHPUnit\Framework\Attributes\UsesClass;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use ReflectionClass;
|
||||
use SebastianBergmann\RecursionContext\Context;
|
||||
|
|
@ -38,8 +39,6 @@ use SplObjectStorage;
|
|||
use stdClass;
|
||||
|
||||
#[CoversClass(Exporter::class)]
|
||||
#[UsesClass(ExportContext::class)]
|
||||
#[UsesClass(ObjectExporterChain::class)]
|
||||
#[Small]
|
||||
final class ExporterTest extends TestCase
|
||||
{
|
||||
|
|
@ -90,7 +89,6 @@ final class ExporterTest extends TestCase
|
|||
'float 1 - 2 / 3' => [1 - 2 / 3, '0.33333333333333337', 0],
|
||||
'float 5.5E+123' => [5.5E+123, '5.5E+123', 0],
|
||||
'float 5.5E-123' => [5.5E-123, '5.5E-123', 0],
|
||||
'float 9.2233720368547758E+18' => [9.2233720368547758E+18, '9.223372036854776E+18', 0],
|
||||
'float NAN' => [NAN, 'NAN', 0],
|
||||
'float INF' => [INF, 'INF', 0],
|
||||
'float -INF' => [-INF, '-INF', 0],
|
||||
|
|
@ -148,9 +146,7 @@ Array &0 [
|
|||
EOF,
|
||||
2,
|
||||
],
|
||||
// A real newline is inserted after each line ending so the
|
||||
// escaped text wraps readably; \r\n stays one break while a lone
|
||||
// \n followed by a lone \r are two separate line endings.
|
||||
// \n\r and \r is converted to \n
|
||||
'export multiline text' => ["this\nis\na\nvery\nvery\nvery\nvery\nvery\nvery\rlong\n\rtext",
|
||||
<<<'EOF'
|
||||
'this\n
|
||||
|
|
@ -162,27 +158,8 @@ very\n
|
|||
very\n
|
||||
very\n
|
||||
very\r
|
||||
long\n
|
||||
\r
|
||||
long\n\r
|
||||
text'
|
||||
EOF,
|
||||
0,
|
||||
],
|
||||
// A lone \n (Unix) followed by a lone \r (old Mac) are two
|
||||
// distinct line endings and must each produce their own break.
|
||||
'lone newline followed by lone carriage return' => ["a\n\rb",
|
||||
<<<'EOF'
|
||||
'a\n
|
||||
\r
|
||||
b'
|
||||
EOF,
|
||||
0,
|
||||
],
|
||||
// \r\n (Windows) is a single line ending and stays one break.
|
||||
'carriage return followed by newline' => ["a\r\nb",
|
||||
<<<'EOF'
|
||||
'a\r\n
|
||||
b'
|
||||
EOF,
|
||||
0,
|
||||
],
|
||||
|
|
@ -204,8 +181,7 @@ very\n
|
|||
very\n
|
||||
very\n
|
||||
very\r
|
||||
long\n
|
||||
\r
|
||||
long\n\r
|
||||
text',
|
||||
'object' => stdClass Object #%d (
|
||||
'foo' => 'bar',
|
||||
|
|
@ -255,7 +231,7 @@ EOF,
|
|||
0,
|
||||
],
|
||||
[
|
||||
implode('', array_map(chr(...), range(0x0E, 0x1F))),
|
||||
implode('', array_map('chr', range(0x0E, 0x1F))),
|
||||
'Binary String: 0x0e0f101112131415161718191a1b1c1d1e1f',
|
||||
0,
|
||||
],
|
||||
|
|
@ -264,41 +240,6 @@ EOF,
|
|||
'Binary String: 0x0009',
|
||||
0,
|
||||
],
|
||||
'mostly printable string with a single control byte' => [
|
||||
'Hello' . chr(0x01) . 'World',
|
||||
'Binary String: "Hello\x01World"',
|
||||
0,
|
||||
],
|
||||
'mostly printable string with several control bytes' => [
|
||||
'BEGIN' . chr(0x01) . 'DATA' . chr(0x1F) . 'END',
|
||||
'Binary String: "BEGIN\x01DATA\x1fEND"',
|
||||
0,
|
||||
],
|
||||
'mostly printable string with embedded tab' => [
|
||||
'Tab' . chr(0x09) . 'Sep' . chr(0x01),
|
||||
'Binary String: "Tab\tSep\x01"',
|
||||
0,
|
||||
],
|
||||
'mostly printable string with embedded newline' => [
|
||||
'Line1' . chr(0x0A) . 'Line2' . chr(0x01),
|
||||
'Binary String: "Line1\nLine2\x01"',
|
||||
0,
|
||||
],
|
||||
'mostly printable string with embedded carriage return' => [
|
||||
'A' . chr(0x0D) . 'B' . chr(0x01),
|
||||
'Binary String: "A\rB\x01"',
|
||||
0,
|
||||
],
|
||||
'mixed string with embedded double quote and backslash' => [
|
||||
'a"b\\c' . chr(0x07) . 'd',
|
||||
'Binary String: "a\"b\\\\c\x07d"',
|
||||
0,
|
||||
],
|
||||
'mixed string preserves UTF-8 high bytes' => [
|
||||
'café' . chr(0x01),
|
||||
'Binary String: "café\x01"',
|
||||
0,
|
||||
],
|
||||
[
|
||||
'',
|
||||
"''",
|
||||
|
|
@ -410,9 +351,9 @@ EOF,
|
|||
public static function provideNonBinaryMultibyteStrings(): array
|
||||
{
|
||||
return [
|
||||
[implode('', array_map(chr(...), range(0x09, 0x0D))), 9],
|
||||
[implode('', array_map(chr(...), range(0x20, 0x7F))), 96],
|
||||
[implode('', array_map(chr(...), range(0x80, 0xFF))), 128],
|
||||
[implode('', array_map('chr', range(0x09, 0x0D))), 9],
|
||||
[implode('', array_map('chr', range(0x20, 0x7F))), 96],
|
||||
[implode('', array_map('chr', range(0x80, 0xFF))), 128],
|
||||
];
|
||||
}
|
||||
|
||||
|
|
@ -493,8 +434,7 @@ very\n
|
|||
very\n
|
||||
very\n
|
||||
very\r
|
||||
long\n
|
||||
\r
|
||||
long\n\r
|
||||
text',
|
||||
'object' => stdClass Object #%d (
|
||||
'foo' => 'bar',
|
||||
|
|
@ -519,8 +459,7 @@ very\n
|
|||
very\n
|
||||
very\n
|
||||
very\r
|
||||
long\n
|
||||
\r
|
||||
long\n\r
|
||||
text',
|
||||
'object' => stdClass Object #%d,
|
||||
'objectagain' => stdClass Object #%d,
|
||||
|
|
@ -546,7 +485,7 @@ EOF;
|
|||
{
|
||||
$this->assertSame(
|
||||
$expected,
|
||||
$this->trimNewline(new Exporter(0, $maxLengthForStrings)->shortenedExport($value)),
|
||||
$this->trimNewline((new Exporter(0, $maxLengthForStrings))->shortenedExport($value)),
|
||||
);
|
||||
|
||||
$this->assertSame(
|
||||
|
|
@ -605,19 +544,6 @@ EOF;
|
|||
$this->assertEquals([], (new Exporter)->toArray((object) $array));
|
||||
}
|
||||
|
||||
public function testPrivatePropertyRedeclaredInDerivedClassIsNotOverwritten(): void
|
||||
{
|
||||
$this->assertSame(
|
||||
[
|
||||
ParentClassWithPrivateProperty::class . '::property' => 'parent value',
|
||||
ChildClassWithPrivateProperty::class . '::property' => 'child value',
|
||||
],
|
||||
(new Exporter)->toArray(
|
||||
new ChildClassWithPrivateProperty('parent value', 'child value'),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<mixed> $value
|
||||
* @param non-negative-int $limit
|
||||
|
|
@ -625,7 +551,7 @@ EOF;
|
|||
#[DataProvider('shortenedRecursiveExportProvider')]
|
||||
public function testShortenedRecursiveExport(array $value, string $expected, int $limit): void
|
||||
{
|
||||
$this->assertEquals($expected, new Exporter($limit)->shortenedRecursiveExport($value));
|
||||
$this->assertEquals($expected, (new Exporter($limit))->shortenedRecursiveExport($value));
|
||||
}
|
||||
|
||||
public function testShortenedRecursiveOccurredRecursion(): void
|
||||
|
|
@ -641,10 +567,14 @@ EOF;
|
|||
$this->assertEquals('*RECURSION*', (new Exporter)->shortenedRecursiveExport($value, processed: $context));
|
||||
}
|
||||
|
||||
#[RequiresPhp('^8.4')]
|
||||
public function testShortenedExportDoesNotInitializeLazyObject(): void
|
||||
{
|
||||
$reflector = new ReflectionClass(ExampleClass::class);
|
||||
|
||||
assert(method_exists($reflector, 'newLazyProxy'));
|
||||
assert(method_exists($reflector, 'isUninitializedLazyObject'));
|
||||
|
||||
$object = $reflector->newLazyProxy(static fn () => new ExampleClass('bar'));
|
||||
|
||||
(new Exporter)->shortenedExport($object, 10);
|
||||
|
|
@ -652,428 +582,6 @@ EOF;
|
|||
$this->assertTrue($reflector->isUninitializedLazyObject($object));
|
||||
}
|
||||
|
||||
public function testObjectCanBeExportedByCustomObjectExporter(): void
|
||||
{
|
||||
$exporter = new Exporter(
|
||||
0,
|
||||
40,
|
||||
new ObjectExporterChain(
|
||||
[
|
||||
new ObjectExporterThatHandlesEveryObject,
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
$this->assertSame(
|
||||
'stdClass (indentation: 0)',
|
||||
$exporter->export(new stdClass),
|
||||
);
|
||||
}
|
||||
|
||||
public function testObjectCanBeExportedByCustomObjectExporterThatIsNotComposedIntoChain(): void
|
||||
{
|
||||
$exporter = new Exporter(
|
||||
0,
|
||||
40,
|
||||
new ObjectExporterThatHandlesEveryObject,
|
||||
);
|
||||
|
||||
$this->assertSame(
|
||||
'stdClass (indentation: 0)',
|
||||
$exporter->export(new stdClass),
|
||||
);
|
||||
}
|
||||
|
||||
public function testObjectNestedInArrayCanBeExportedByCustomObjectExporter(): void
|
||||
{
|
||||
$exporter = new Exporter(
|
||||
0,
|
||||
40,
|
||||
new ObjectExporterChain(
|
||||
[
|
||||
new ObjectExporterThatHandlesEveryObject,
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
$this->assertSame(
|
||||
<<<'EOT'
|
||||
Array &0 [
|
||||
0 => stdClass (indentation: 1),
|
||||
]
|
||||
EOT,
|
||||
$exporter->export([new stdClass]),
|
||||
);
|
||||
}
|
||||
|
||||
public function testObjectNestedInObjectCanBeExportedByCustomObjectExporter(): void
|
||||
{
|
||||
$exporter = new Exporter(
|
||||
0,
|
||||
40,
|
||||
new ObjectExporterChain(
|
||||
[
|
||||
new ObjectExporterThatHandlesObjectsOfSpecificType(ExampleClass::class),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
$object = new stdClass;
|
||||
$object->nested = new ExampleClass('bar');
|
||||
|
||||
$this->assertStringMatchesFormat(
|
||||
<<<'EOT'
|
||||
stdClass Object #%d (
|
||||
'nested' => SebastianBergmann\Exporter\ExampleClass handled by custom exporter,
|
||||
)
|
||||
EOT,
|
||||
$exporter->export($object),
|
||||
);
|
||||
}
|
||||
|
||||
public function testEveryOccurrenceOfObjectHandledByCustomObjectExporterIsExportedByThatObjectExporter(): void
|
||||
{
|
||||
$exporter = new Exporter(
|
||||
0,
|
||||
40,
|
||||
new ObjectExporterChain(
|
||||
[
|
||||
new ObjectExporterThatHandlesEveryObject,
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
$object = new stdClass;
|
||||
|
||||
$this->assertSame(
|
||||
<<<'EOT'
|
||||
Array &0 [
|
||||
0 => stdClass (indentation: 1),
|
||||
1 => stdClass (indentation: 1),
|
||||
]
|
||||
EOT,
|
||||
$exporter->export([$object, $object]),
|
||||
);
|
||||
}
|
||||
|
||||
public function testRepeatedOccurrenceOfObjectNotHandledByCustomObjectExporterIsReplacedWithReferenceToObject(): void
|
||||
{
|
||||
$exporter = new Exporter(
|
||||
0,
|
||||
40,
|
||||
new ObjectExporterChain(
|
||||
[
|
||||
new ObjectExporterThatHandlesNoObject,
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
$object = new stdClass;
|
||||
|
||||
$this->assertStringMatchesFormat(
|
||||
<<<'EOT'
|
||||
Array &0 [
|
||||
0 => stdClass Object #%d (),
|
||||
1 => stdClass Object #%d,
|
||||
]
|
||||
EOT,
|
||||
$exporter->export([$object, $object]),
|
||||
);
|
||||
}
|
||||
|
||||
public function testDefaultExportIsUsedWhenNoCustomObjectExporterHandlesObject(): void
|
||||
{
|
||||
$exporter = new Exporter(
|
||||
0,
|
||||
40,
|
||||
new ObjectExporterChain(
|
||||
[
|
||||
new ObjectExporterThatHandlesNoObject,
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
$this->assertStringMatchesFormat(
|
||||
'stdClass Object #%d ()',
|
||||
$exporter->export(new stdClass),
|
||||
);
|
||||
}
|
||||
|
||||
public function testEnumCanBeExportedByCustomObjectExporter(): void
|
||||
{
|
||||
$exporter = new Exporter(
|
||||
0,
|
||||
40,
|
||||
new ObjectExporterChain(
|
||||
[
|
||||
new ObjectExporterThatHandlesEveryObject,
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
$this->assertSame(
|
||||
'SebastianBergmann\Exporter\ExampleEnum (indentation: 0)',
|
||||
$exporter->export(ExampleEnum::Value),
|
||||
);
|
||||
|
||||
$this->assertSame(
|
||||
'SebastianBergmann\Exporter\ExampleStringBackedEnum (indentation: 0)',
|
||||
$exporter->export(ExampleStringBackedEnum::Value),
|
||||
);
|
||||
}
|
||||
|
||||
public function testDefaultExportIsUsedForEnumWhenNoCustomObjectExporterHandlesIt(): void
|
||||
{
|
||||
$exporter = new Exporter(
|
||||
0,
|
||||
40,
|
||||
new ObjectExporterChain(
|
||||
[
|
||||
new ObjectExporterThatHandlesNoObject,
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
$this->assertStringMatchesFormat(
|
||||
'SebastianBergmann\Exporter\ExampleEnum Enum #%d (Value)',
|
||||
$exporter->export(ExampleEnum::Value),
|
||||
);
|
||||
|
||||
$this->assertStringMatchesFormat(
|
||||
'SebastianBergmann\Exporter\ExampleStringBackedEnum Enum #%d (Value, \'value\')',
|
||||
$exporter->export(ExampleStringBackedEnum::Value),
|
||||
);
|
||||
}
|
||||
|
||||
public function testShortenedExportUsesRepresentationProvidedByCustomObjectExporter(): void
|
||||
{
|
||||
$exporter = new Exporter(
|
||||
0,
|
||||
40,
|
||||
new ObjectExporterThatHandlesEveryObject,
|
||||
);
|
||||
|
||||
$this->assertSame(
|
||||
'stdClass (indentation: 0)',
|
||||
$exporter->shortenedExport(new stdClass),
|
||||
);
|
||||
|
||||
$this->assertSame(
|
||||
'SebastianBergmann\Exporter\ExampleEnum (indentation: 0)',
|
||||
$exporter->shortenedExport(ExampleEnum::Value, 80),
|
||||
);
|
||||
}
|
||||
|
||||
public function testShortenedExportCollapsesRepresentationProvidedByCustomObjectExporterToSingleLine(): void
|
||||
{
|
||||
$exporter = new Exporter(
|
||||
0,
|
||||
40,
|
||||
new ObjectExporterThatReturnsGivenRepresentation("first\nsecond"),
|
||||
);
|
||||
|
||||
$this->assertSame(
|
||||
'firstsecond',
|
||||
$exporter->shortenedExport(new stdClass),
|
||||
);
|
||||
}
|
||||
|
||||
public function testShortenedExportShortensRepresentationProvidedByCustomObjectExporter(): void
|
||||
{
|
||||
$exporter = new Exporter(
|
||||
0,
|
||||
40,
|
||||
new ObjectExporterThatReturnsGivenRepresentation(str_repeat('a', 41)),
|
||||
);
|
||||
|
||||
$this->assertSame(
|
||||
str_repeat('a', 30) . '...' . str_repeat('a', 7),
|
||||
$exporter->shortenedExport(new stdClass),
|
||||
);
|
||||
}
|
||||
|
||||
public function testShortenedRecursiveExportUsesRepresentationProvidedByCustomObjectExporter(): void
|
||||
{
|
||||
$exporter = new Exporter(
|
||||
0,
|
||||
40,
|
||||
new ObjectExporterThatHandlesEveryObject,
|
||||
);
|
||||
|
||||
$data = [new stdClass, 'foo'];
|
||||
|
||||
$this->assertSame(
|
||||
"stdClass (indentation: 0), 'foo'",
|
||||
$exporter->shortenedRecursiveExport($data),
|
||||
);
|
||||
}
|
||||
|
||||
public function testShortenedExportDoesNotRecurseInfinitelyForObjectNestedInItself(): void
|
||||
{
|
||||
$exporter = new Exporter(
|
||||
0,
|
||||
40,
|
||||
new ObjectExporterThatExportsNestedValues,
|
||||
);
|
||||
|
||||
$node = new Node;
|
||||
$node->children = [$node];
|
||||
|
||||
$this->assertStringMatchesFormat(
|
||||
'Node(Array &0 [ 0 => SebastianBergmann\Exporter\Node Object #%d,])',
|
||||
$exporter->shortenedExport($node, 200),
|
||||
);
|
||||
}
|
||||
|
||||
public function testShortenedExportUsesDefaultRepresentationWhenNoCustomObjectExporterHandlesObject(): void
|
||||
{
|
||||
$exporter = new Exporter(
|
||||
0,
|
||||
40,
|
||||
new ObjectExporterThatHandlesNoObject,
|
||||
);
|
||||
|
||||
$this->assertSame(
|
||||
'stdClass Object ()',
|
||||
$exporter->shortenedExport(new stdClass),
|
||||
);
|
||||
}
|
||||
|
||||
public function testKnowsThatCustomObjectExporterProvidesRepresentationForObjectItHandles(): void
|
||||
{
|
||||
$exporter = new Exporter(
|
||||
0,
|
||||
40,
|
||||
new ObjectExporterChain(
|
||||
[
|
||||
new ObjectExporterThatHandlesObjectsOfSpecificType(ExampleClass::class),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
$this->assertTrue($exporter->hasCustomRepresentationFor(new ExampleClass('bar')));
|
||||
}
|
||||
|
||||
public function testKnowsThatCustomObjectExporterDoesNotProvideRepresentationForObjectItDoesNotHandle(): void
|
||||
{
|
||||
$exporter = new Exporter(
|
||||
0,
|
||||
40,
|
||||
new ObjectExporterChain(
|
||||
[
|
||||
new ObjectExporterThatHandlesObjectsOfSpecificType(ExampleClass::class),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
$this->assertFalse($exporter->hasCustomRepresentationFor(new stdClass));
|
||||
}
|
||||
|
||||
public function testKnowsThatNoCustomObjectExporterProvidesRepresentationForObjectWhenNoneIsConfigured(): void
|
||||
{
|
||||
$this->assertFalse((new Exporter)->hasCustomRepresentationFor(new stdClass));
|
||||
}
|
||||
|
||||
public function testCustomObjectExporterCanExportValuesNestedInObjectItHandles(): void
|
||||
{
|
||||
$exporter = new Exporter(
|
||||
0,
|
||||
40,
|
||||
new ObjectExporterChain(
|
||||
[
|
||||
new ObjectExporterThatExportsNestedValues,
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
$this->assertSame(
|
||||
<<<'EOT'
|
||||
Node(Array &0 [
|
||||
'key' => 'value',
|
||||
])
|
||||
EOT,
|
||||
$exporter->export(new Node(['key' => 'value'])),
|
||||
);
|
||||
}
|
||||
|
||||
public function testArrayExportedByCustomObjectExporterDoesNotReuseReferenceToOtherArray(): void
|
||||
{
|
||||
$exporter = new Exporter(
|
||||
0,
|
||||
40,
|
||||
new ObjectExporterChain(
|
||||
[
|
||||
new ObjectExporterThatExportsNestedValues,
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
$this->assertSame(
|
||||
<<<'EOT'
|
||||
Array &0 [
|
||||
0 => Node(Array &1 [
|
||||
'key' => 'value',
|
||||
]),
|
||||
]
|
||||
EOT,
|
||||
$exporter->export([new Node(['key' => 'value'])]),
|
||||
);
|
||||
}
|
||||
|
||||
public function testObjectNestedInItselfIsNotExportedByCustomObjectExporterAgain(): void
|
||||
{
|
||||
$exporter = new Exporter(
|
||||
0,
|
||||
40,
|
||||
new ObjectExporterChain(
|
||||
[
|
||||
new ObjectExporterThatExportsNestedValues,
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
$node = new Node;
|
||||
$node->children = [$node];
|
||||
|
||||
$this->assertStringMatchesFormat(
|
||||
<<<'EOT'
|
||||
Node(Array &0 [
|
||||
0 => SebastianBergmann\Exporter\Node Object #%d,
|
||||
])
|
||||
EOT,
|
||||
$exporter->export($node),
|
||||
);
|
||||
}
|
||||
|
||||
public function testObjectsNestedInEachOtherAreNotExportedByCustomObjectExporterAgain(): void
|
||||
{
|
||||
$exporter = new Exporter(
|
||||
0,
|
||||
40,
|
||||
new ObjectExporterChain(
|
||||
[
|
||||
new ObjectExporterThatExportsNestedValues,
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
$first = new Node;
|
||||
$second = new Node;
|
||||
$first->children = [$second];
|
||||
$second->children = [$first];
|
||||
|
||||
$this->assertStringMatchesFormat(
|
||||
<<<'EOT'
|
||||
Node(Array &0 [
|
||||
0 => Node(Array &1 [
|
||||
0 => SebastianBergmann\Exporter\Node Object #%d,
|
||||
]),
|
||||
])
|
||||
EOT,
|
||||
$exporter->export($first),
|
||||
);
|
||||
}
|
||||
|
||||
private function trimNewline(string $string): string
|
||||
{
|
||||
return (string) preg_replace('/[ ]*\n/', "\n", $string);
|
||||
|
|
|
|||
|
|
@ -1,80 +0,0 @@
|
|||
<?php declare(strict_types=1);
|
||||
/*
|
||||
* This file is part of sebastian/exporter.
|
||||
*
|
||||
* (c) Sebastian Bergmann <sebastian@phpunit.de>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
namespace SebastianBergmann\Exporter;
|
||||
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\Attributes\Small;
|
||||
use PHPUnit\Framework\Attributes\UsesClass;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use stdClass;
|
||||
|
||||
#[CoversClass(ObjectExporterChain::class)]
|
||||
#[UsesClass(Exporter::class)]
|
||||
#[UsesClass(ExportContext::class)]
|
||||
#[Small]
|
||||
final class ObjectExporterChainTest extends TestCase
|
||||
{
|
||||
public function testHandlesObjectWhenAtLeastOneComposedObjectExporterHandlesIt(): void
|
||||
{
|
||||
$chain = new ObjectExporterChain(
|
||||
[
|
||||
new ObjectExporterThatHandlesNoObject,
|
||||
new ObjectExporterThatHandlesEveryObject,
|
||||
],
|
||||
);
|
||||
|
||||
$this->assertTrue($chain->handles(new stdClass));
|
||||
}
|
||||
|
||||
public function testDoesNotHandleObjectWhenNoComposedObjectExporterHandlesIt(): void
|
||||
{
|
||||
$chain = new ObjectExporterChain(
|
||||
[
|
||||
new ObjectExporterThatHandlesNoObject,
|
||||
],
|
||||
);
|
||||
|
||||
$this->assertFalse($chain->handles(new stdClass));
|
||||
}
|
||||
|
||||
public function testDelegatesExportingToFirstComposedObjectExporterThatHandlesObject(): void
|
||||
{
|
||||
$chain = new ObjectExporterChain(
|
||||
[
|
||||
new ObjectExporterThatHandlesNoObject,
|
||||
new ObjectExporterThatHandlesObjectsOfSpecificType(stdClass::class),
|
||||
new ObjectExporterThatHandlesEveryObject,
|
||||
],
|
||||
);
|
||||
|
||||
$this->assertSame(
|
||||
'stdClass handled by custom exporter',
|
||||
$chain->export(new stdClass, new Exporter, 0, new ExportContext),
|
||||
);
|
||||
|
||||
$this->assertSame(
|
||||
ExampleClass::class . ' (indentation: 0)',
|
||||
$chain->export(new ExampleClass('foo'), new Exporter, 0, new ExportContext),
|
||||
);
|
||||
}
|
||||
|
||||
public function testCannotExportObjectWhenNoComposedObjectExporterHandlesIt(): void
|
||||
{
|
||||
$chain = new ObjectExporterChain(
|
||||
[
|
||||
new ObjectExporterThatHandlesNoObject,
|
||||
],
|
||||
);
|
||||
|
||||
$this->expectException(ObjectNotSupportedException::class);
|
||||
|
||||
$chain->export(new stdClass, new Exporter, 0, new ExportContext);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
<?php declare(strict_types=1);
|
||||
/*
|
||||
* This file is part of sebastian/exporter.
|
||||
*
|
||||
* (c) Sebastian Bergmann <sebastian@phpunit.de>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
namespace SebastianBergmann\Exporter;
|
||||
|
||||
final class ChildClassWithPrivateProperty extends ParentClassWithPrivateProperty
|
||||
{
|
||||
/**
|
||||
* @phpstan-ignore property.onlyWritten
|
||||
*/
|
||||
private string $property;
|
||||
|
||||
public function __construct(string $parentProperty, string $childProperty)
|
||||
{
|
||||
parent::__construct($parentProperty);
|
||||
|
||||
$this->property = $childProperty;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
<?php declare(strict_types=1);
|
||||
/*
|
||||
* This file is part of sebastian/exporter.
|
||||
*
|
||||
* (c) Sebastian Bergmann <sebastian@phpunit.de>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
namespace SebastianBergmann\Exporter;
|
||||
|
||||
/*
|
||||
* Helper to test export of objects that are nested in objects.
|
||||
*/
|
||||
final class Node
|
||||
{
|
||||
/**
|
||||
* @var array<mixed>
|
||||
*/
|
||||
public array $children;
|
||||
|
||||
/**
|
||||
* @param array<mixed> $children
|
||||
*/
|
||||
public function __construct(array $children = [])
|
||||
{
|
||||
$this->children = $children;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
<?php declare(strict_types=1);
|
||||
/*
|
||||
* This file is part of sebastian/exporter.
|
||||
*
|
||||
* (c) Sebastian Bergmann <sebastian@phpunit.de>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
namespace SebastianBergmann\Exporter;
|
||||
|
||||
use function assert;
|
||||
use function sprintf;
|
||||
|
||||
final readonly class ObjectExporterThatExportsNestedValues implements ObjectExporter
|
||||
{
|
||||
public function handles(object $object): bool
|
||||
{
|
||||
return $object instanceof Node;
|
||||
}
|
||||
|
||||
public function export(object $object, Exporter $exporter, int $indentation, ExportContext $context): string
|
||||
{
|
||||
assert($object instanceof Node);
|
||||
|
||||
return sprintf(
|
||||
'Node(%s)',
|
||||
$exporter->export($object->children, $indentation, $context),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
<?php declare(strict_types=1);
|
||||
/*
|
||||
* This file is part of sebastian/exporter.
|
||||
*
|
||||
* (c) Sebastian Bergmann <sebastian@phpunit.de>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
namespace SebastianBergmann\Exporter;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
final readonly class ObjectExporterThatHandlesEveryObject implements ObjectExporter
|
||||
{
|
||||
public function handles(object $object): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function export(object $object, Exporter $exporter, int $indentation, ExportContext $context): string
|
||||
{
|
||||
return sprintf(
|
||||
'%s (indentation: %d)',
|
||||
$object::class,
|
||||
$indentation,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
<?php declare(strict_types=1);
|
||||
/*
|
||||
* This file is part of sebastian/exporter.
|
||||
*
|
||||
* (c) Sebastian Bergmann <sebastian@phpunit.de>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
namespace SebastianBergmann\Exporter;
|
||||
|
||||
final readonly class ObjectExporterThatHandlesNoObject implements ObjectExporter
|
||||
{
|
||||
public function handles(object $object): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function export(object $object, Exporter $exporter, int $indentation, ExportContext $context): string
|
||||
{
|
||||
throw new ObjectNotSupportedException;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
<?php declare(strict_types=1);
|
||||
/*
|
||||
* This file is part of sebastian/exporter.
|
||||
*
|
||||
* (c) Sebastian Bergmann <sebastian@phpunit.de>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
namespace SebastianBergmann\Exporter;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
final readonly class ObjectExporterThatHandlesObjectsOfSpecificType implements ObjectExporter
|
||||
{
|
||||
/**
|
||||
* @var class-string
|
||||
*/
|
||||
private string $type;
|
||||
|
||||
/**
|
||||
* @param class-string $type
|
||||
*/
|
||||
public function __construct(string $type)
|
||||
{
|
||||
$this->type = $type;
|
||||
}
|
||||
|
||||
public function handles(object $object): bool
|
||||
{
|
||||
return $object instanceof $this->type;
|
||||
}
|
||||
|
||||
public function export(object $object, Exporter $exporter, int $indentation, ExportContext $context): string
|
||||
{
|
||||
return sprintf(
|
||||
'%s handled by custom exporter',
|
||||
$object::class,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
<?php declare(strict_types=1);
|
||||
/*
|
||||
* This file is part of sebastian/exporter.
|
||||
*
|
||||
* (c) Sebastian Bergmann <sebastian@phpunit.de>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
namespace SebastianBergmann\Exporter;
|
||||
|
||||
final readonly class ObjectExporterThatReturnsGivenRepresentation implements ObjectExporter
|
||||
{
|
||||
private string $representation;
|
||||
|
||||
public function __construct(string $representation)
|
||||
{
|
||||
$this->representation = $representation;
|
||||
}
|
||||
|
||||
public function handles(object $object): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function export(object $object, Exporter $exporter, int $indentation, ExportContext $context): string
|
||||
{
|
||||
return $this->representation;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
<?php declare(strict_types=1);
|
||||
/*
|
||||
* This file is part of sebastian/exporter.
|
||||
*
|
||||
* (c) Sebastian Bergmann <sebastian@phpunit.de>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
namespace SebastianBergmann\Exporter;
|
||||
|
||||
abstract class ParentClassWithPrivateProperty
|
||||
{
|
||||
/**
|
||||
* @phpstan-ignore property.onlyWritten
|
||||
*/
|
||||
private string $property;
|
||||
|
||||
public function __construct(string $property)
|
||||
{
|
||||
$this->property = $property;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
{
|
||||
"require-dev": {
|
||||
"phpstan/phpstan": "^2.2.7",
|
||||
"phpstan/phpstan": "^2.1.28",
|
||||
"phpstan/extension-installer": "^1.4.3",
|
||||
"phpstan/phpstan-strict-rules": "^2.0.12",
|
||||
"tomasvotruba/type-coverage": "^2.3.0",
|
||||
"ergebnis/phpstan-rules": "^2.13.1"
|
||||
"phpstan/phpstan-strict-rules": "^2.0.6",
|
||||
"tomasvotruba/type-coverage": "^2.0.2",
|
||||
"ergebnis/phpstan-rules": "^2.12.0"
|
||||
},
|
||||
"config": {
|
||||
"allow-plugins": {
|
||||
|
|
|
|||
|
|
@ -4,40 +4,40 @@
|
|||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "3579343022b2a28c7195fe30b1752bfb",
|
||||
"content-hash": "82891e636eae6e96ecd26fa1ee317643",
|
||||
"packages": [],
|
||||
"packages-dev": [
|
||||
{
|
||||
"name": "ergebnis/phpstan-rules",
|
||||
"version": "2.13.1",
|
||||
"version": "2.12.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/ergebnis/phpstan-rules.git",
|
||||
"reference": "f69db86b98595c34fc1f61c89fe3b380141aa519"
|
||||
"reference": "c4e0121a937b3b551f800a86e7d78794da2783ea"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/ergebnis/phpstan-rules/zipball/f69db86b98595c34fc1f61c89fe3b380141aa519",
|
||||
"reference": "f69db86b98595c34fc1f61c89fe3b380141aa519",
|
||||
"url": "https://api.github.com/repos/ergebnis/phpstan-rules/zipball/c4e0121a937b3b551f800a86e7d78794da2783ea",
|
||||
"reference": "c4e0121a937b3b551f800a86e7d78794da2783ea",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-mbstring": "*",
|
||||
"php": "~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0",
|
||||
"phpstan/phpstan": "^2.1.35"
|
||||
"phpstan/phpstan": "^2.1.8"
|
||||
},
|
||||
"require-dev": {
|
||||
"codeception/codeception": "^4.0.0 || ^5.0.0",
|
||||
"doctrine/orm": "^2.20.0 || ^3.3.0",
|
||||
"ergebnis/composer-normalize": "^2.49.0",
|
||||
"ergebnis/license": "^2.7.0",
|
||||
"ergebnis/php-cs-fixer-config": "^6.59.0",
|
||||
"ergebnis/composer-normalize": "^2.47.0",
|
||||
"ergebnis/license": "^2.6.0",
|
||||
"ergebnis/php-cs-fixer-config": "^6.54.0",
|
||||
"ergebnis/phpunit-slow-test-detector": "^2.20.0",
|
||||
"fakerphp/faker": "^1.24.1",
|
||||
"phpstan/extension-installer": "^1.4.3",
|
||||
"phpstan/phpstan-deprecation-rules": "^2.0.3",
|
||||
"phpstan/phpstan-phpunit": "^2.0.12",
|
||||
"phpstan/phpstan-strict-rules": "^2.0.8",
|
||||
"phpstan/phpstan-phpunit": "^2.0.7",
|
||||
"phpstan/phpstan-strict-rules": "^2.0.6",
|
||||
"phpunit/phpunit": "^9.6.21",
|
||||
"psr/container": "^2.0.2",
|
||||
"symfony/finder": "^5.4.45",
|
||||
|
|
@ -78,7 +78,96 @@
|
|||
"security": "https://github.com/ergebnis/phpstan-rules/blob/main/.github/SECURITY.md",
|
||||
"source": "https://github.com/ergebnis/phpstan-rules"
|
||||
},
|
||||
"time": "2026-01-27T17:13:06+00:00"
|
||||
"time": "2025-09-07T13:31:33+00:00"
|
||||
},
|
||||
{
|
||||
"name": "nette/utils",
|
||||
"version": "v4.0.8",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/nette/utils.git",
|
||||
"reference": "c930ca4e3cf4f17dcfb03037703679d2396d2ede"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/nette/utils/zipball/c930ca4e3cf4f17dcfb03037703679d2396d2ede",
|
||||
"reference": "c930ca4e3cf4f17dcfb03037703679d2396d2ede",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "8.0 - 8.5"
|
||||
},
|
||||
"conflict": {
|
||||
"nette/finder": "<3",
|
||||
"nette/schema": "<1.2.2"
|
||||
},
|
||||
"require-dev": {
|
||||
"jetbrains/phpstorm-attributes": "^1.2",
|
||||
"nette/tester": "^2.5",
|
||||
"phpstan/phpstan-nette": "^2.0@stable",
|
||||
"tracy/tracy": "^2.9"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-gd": "to use Image",
|
||||
"ext-iconv": "to use Strings::webalize(), toAscii(), chr() and reverse()",
|
||||
"ext-intl": "to use Strings::webalize(), toAscii(), normalize() and compare()",
|
||||
"ext-json": "to use Nette\\Utils\\Json",
|
||||
"ext-mbstring": "to use Strings::lower() etc...",
|
||||
"ext-tokenizer": "to use Nette\\Utils\\Reflection::getUseStatements()"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "4.0-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Nette\\": "src"
|
||||
},
|
||||
"classmap": [
|
||||
"src/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"BSD-3-Clause",
|
||||
"GPL-2.0-only",
|
||||
"GPL-3.0-only"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "David Grudl",
|
||||
"homepage": "https://davidgrudl.com"
|
||||
},
|
||||
{
|
||||
"name": "Nette Community",
|
||||
"homepage": "https://nette.org/contributors"
|
||||
}
|
||||
],
|
||||
"description": "🛠 Nette Utils: lightweight utilities for string & array manipulation, image handling, safe JSON encoding/decoding, validation, slug or strong password generating etc.",
|
||||
"homepage": "https://nette.org",
|
||||
"keywords": [
|
||||
"array",
|
||||
"core",
|
||||
"datetime",
|
||||
"images",
|
||||
"json",
|
||||
"nette",
|
||||
"paginator",
|
||||
"password",
|
||||
"slugify",
|
||||
"string",
|
||||
"unicode",
|
||||
"utf-8",
|
||||
"utility",
|
||||
"validation"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/nette/utils/issues",
|
||||
"source": "https://github.com/nette/utils/tree/v4.0.8"
|
||||
},
|
||||
"time": "2025-08-06T21:43:34+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpstan/extension-installer",
|
||||
|
|
@ -130,11 +219,16 @@
|
|||
},
|
||||
{
|
||||
"name": "phpstan/phpstan",
|
||||
"version": "2.2.7",
|
||||
"version": "2.1.28",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/phpstan/phpstan.git",
|
||||
"reference": "578fa296a166605d97b94091f724f1257185d278"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/692db47b9dddb0487934e5236e77d48594aef921",
|
||||
"reference": "692db47b9dddb0487934e5236e77d48594aef921",
|
||||
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/578fa296a166605d97b94091f724f1257185d278",
|
||||
"reference": "578fa296a166605d97b94091f724f1257185d278",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
|
@ -157,17 +251,6 @@
|
|||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Ondřej Mirtes"
|
||||
},
|
||||
{
|
||||
"name": "Markus Staab"
|
||||
},
|
||||
{
|
||||
"name": "Vincent Langlet"
|
||||
}
|
||||
],
|
||||
"description": "PHPStan - PHP Static Analysis Tool",
|
||||
"keywords": [
|
||||
"dev",
|
||||
|
|
@ -190,32 +273,31 @@
|
|||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2026-07-29T17:39:32+00:00"
|
||||
"time": "2025-09-19T08:58:49+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpstan/phpstan-strict-rules",
|
||||
"version": "2.0.12",
|
||||
"version": "2.0.6",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/phpstan/phpstan-strict-rules.git",
|
||||
"reference": "2bc5ae19ae965663b62ac907ee6342c3903ec93b"
|
||||
"reference": "f9f77efa9de31992a832ff77ea52eb42d675b094"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/2bc5ae19ae965663b62ac907ee6342c3903ec93b",
|
||||
"reference": "2bc5ae19ae965663b62ac907ee6342c3903ec93b",
|
||||
"url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/f9f77efa9de31992a832ff77ea52eb42d675b094",
|
||||
"reference": "f9f77efa9de31992a832ff77ea52eb42d675b094",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^7.4 || ^8.0",
|
||||
"phpstan/phpstan": "^2.1.52"
|
||||
"phpstan/phpstan": "^2.0.4"
|
||||
},
|
||||
"require-dev": {
|
||||
"php-parallel-lint/php-parallel-lint": "^1.2",
|
||||
"phpstan/phpstan-deprecation-rules": "^2.0",
|
||||
"phpstan/phpstan-phpunit": "^2.0",
|
||||
"phpunit/phpunit": "^9.6",
|
||||
"shipmonk/name-collision-detector": "^2.1"
|
||||
"phpunit/phpunit": "^9.6"
|
||||
},
|
||||
"type": "phpstan-extension",
|
||||
"extra": {
|
||||
|
|
@ -235,57 +317,41 @@
|
|||
"MIT"
|
||||
],
|
||||
"description": "Extra strict and opinionated rules for PHPStan",
|
||||
"keywords": [
|
||||
"static analysis"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/phpstan/phpstan-strict-rules/issues",
|
||||
"source": "https://github.com/phpstan/phpstan-strict-rules/tree/2.0.12"
|
||||
"source": "https://github.com/phpstan/phpstan-strict-rules/tree/2.0.6"
|
||||
},
|
||||
"time": "2026-07-19T07:24:06+00:00"
|
||||
"time": "2025-07-21T12:19:29+00:00"
|
||||
},
|
||||
{
|
||||
"name": "tomasvotruba/type-coverage",
|
||||
"version": "2.3.0",
|
||||
"version": "2.0.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/TomasVotruba/type-coverage.git",
|
||||
"reference": "7ba48c1bf3bbb7ffe99caacc7017bae5ee72d644"
|
||||
"reference": "d033429580f2c18bda538fa44f2939236a990e0c"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/TomasVotruba/type-coverage/zipball/7ba48c1bf3bbb7ffe99caacc7017bae5ee72d644",
|
||||
"reference": "7ba48c1bf3bbb7ffe99caacc7017bae5ee72d644",
|
||||
"url": "https://api.github.com/repos/TomasVotruba/type-coverage/zipball/d033429580f2c18bda538fa44f2939236a990e0c",
|
||||
"reference": "d033429580f2c18bda538fa44f2939236a990e0c",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^8.4",
|
||||
"phpstan/phpstan": "^2.2",
|
||||
"webmozart/assert": "^1.11 || ^2.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpstan/extension-installer": "^1.4",
|
||||
"phpunit/phpunit": "^13.2",
|
||||
"rector/jack": "^1.0",
|
||||
"rector/rector": "^2.5",
|
||||
"shipmonk/composer-dependency-analyser": "^1.8",
|
||||
"symfony/dom-crawler": "^8.1",
|
||||
"symplify/easy-coding-standard": "^13.2",
|
||||
"tomasvotruba/unused-public": "^2.2",
|
||||
"tracy/tracy": "^2.12"
|
||||
"nette/utils": "^3.2 || ^4.0",
|
||||
"php": "^7.4 || ^8.0",
|
||||
"phpstan/phpstan": "^2.0"
|
||||
},
|
||||
"type": "phpstan-extension",
|
||||
"extra": {
|
||||
"phpstan": {
|
||||
"includes": [
|
||||
"config/extension.neon",
|
||||
"packages/type-perfect/config/extension.neon"
|
||||
"config/extension.neon"
|
||||
]
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Rector\\TypePerfect\\": "packages/type-perfect/src",
|
||||
"TomasVotruba\\TypeCoverage\\": "src"
|
||||
}
|
||||
},
|
||||
|
|
@ -300,7 +366,7 @@
|
|||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/TomasVotruba/type-coverage/issues",
|
||||
"source": "https://github.com/TomasVotruba/type-coverage/tree/2.3.0"
|
||||
"source": "https://github.com/TomasVotruba/type-coverage/tree/2.0.2"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
|
|
@ -312,73 +378,7 @@
|
|||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2026-07-29T20:32:37+00:00"
|
||||
},
|
||||
{
|
||||
"name": "webmozart/assert",
|
||||
"version": "2.4.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/webmozarts/assert.git",
|
||||
"reference": "2ccb7c2e821038c03a3e6e1700c570c158c55f70"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/webmozarts/assert/zipball/2ccb7c2e821038c03a3e6e1700c570c158c55f70",
|
||||
"reference": "2ccb7c2e821038c03a3e6e1700c570c158c55f70",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-ctype": "*",
|
||||
"ext-date": "*",
|
||||
"ext-filter": "*",
|
||||
"php": "^8.2"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-intl": "",
|
||||
"ext-simplexml": "",
|
||||
"ext-spl": ""
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"psalm": {
|
||||
"pluginClass": "Webmozart\\Assert\\PsalmPlugin"
|
||||
},
|
||||
"branch-alias": {
|
||||
"dev-master": "2.0-dev",
|
||||
"dev-feature/2-0": "2.0-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Webmozart\\Assert\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Bernhard Schussek",
|
||||
"email": "bschussek@gmail.com"
|
||||
},
|
||||
{
|
||||
"name": "Woody Gilk",
|
||||
"email": "woody.gilk@gmail.com"
|
||||
}
|
||||
],
|
||||
"description": "Assertions to validate method input/output with nice error messages.",
|
||||
"keywords": [
|
||||
"assert",
|
||||
"check",
|
||||
"validate"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/webmozarts/assert/issues",
|
||||
"source": "https://github.com/webmozarts/assert/tree/2.4.1"
|
||||
},
|
||||
"time": "2026-06-15T15:31:57+00:00"
|
||||
"time": "2025-01-07T00:10:26+00:00"
|
||||
}
|
||||
],
|
||||
"aliases": [],
|
||||
|
|
@ -388,5 +388,5 @@
|
|||
"prefer-lowest": false,
|
||||
"platform": {},
|
||||
"platform-dev": {},
|
||||
"plugin-api-version": "2.9.0"
|
||||
"plugin-api-version": "2.6.0"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,9 +49,6 @@ return array(
|
|||
'Nette\\Utils\\JsonException' => $vendorDir . '/nette/utils/src/Utils/exceptions.php',
|
||||
'Nette\\Utils\\ObjectHelpers' => $vendorDir . '/nette/utils/src/Utils/ObjectHelpers.php',
|
||||
'Nette\\Utils\\Paginator' => $vendorDir . '/nette/utils/src/Utils/Paginator.php',
|
||||
'Nette\\Utils\\Process' => $vendorDir . '/nette/utils/src/Utils/Process.php',
|
||||
'Nette\\Utils\\ProcessFailedException' => $vendorDir . '/nette/utils/src/Utils/exceptions.php',
|
||||
'Nette\\Utils\\ProcessTimeoutException' => $vendorDir . '/nette/utils/src/Utils/exceptions.php',
|
||||
'Nette\\Utils\\Random' => $vendorDir . '/nette/utils/src/Utils/Random.php',
|
||||
'Nette\\Utils\\Reflection' => $vendorDir . '/nette/utils/src/Utils/Reflection.php',
|
||||
'Nette\\Utils\\ReflectionMethod' => $vendorDir . '/nette/utils/src/Utils/ReflectionMethod.php',
|
||||
|
|
|
|||
|
|
@ -11,43 +11,43 @@ class ComposerStaticInitf9e7218f71d5874b5632927df4f72bd7
|
|||
);
|
||||
|
||||
public static $prefixLengthsPsr4 = array (
|
||||
'T' =>
|
||||
'T' =>
|
||||
array (
|
||||
'TomasVotruba\\TypeCoverage\\' => 26,
|
||||
),
|
||||
'P' =>
|
||||
'P' =>
|
||||
array (
|
||||
'PHPStan\\ExtensionInstaller\\' => 27,
|
||||
'PHPStan\\' => 8,
|
||||
),
|
||||
'N' =>
|
||||
'N' =>
|
||||
array (
|
||||
'Nette\\' => 6,
|
||||
),
|
||||
'E' =>
|
||||
'E' =>
|
||||
array (
|
||||
'Ergebnis\\PHPStan\\Rules\\' => 23,
|
||||
),
|
||||
);
|
||||
|
||||
public static $prefixDirsPsr4 = array (
|
||||
'TomasVotruba\\TypeCoverage\\' =>
|
||||
'TomasVotruba\\TypeCoverage\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/tomasvotruba/type-coverage/src',
|
||||
),
|
||||
'PHPStan\\ExtensionInstaller\\' =>
|
||||
'PHPStan\\ExtensionInstaller\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/phpstan/extension-installer/src',
|
||||
),
|
||||
'PHPStan\\' =>
|
||||
'PHPStan\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/phpstan/phpstan-strict-rules/src',
|
||||
),
|
||||
'Nette\\' =>
|
||||
'Nette\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/nette/utils/src',
|
||||
),
|
||||
'Ergebnis\\PHPStan\\Rules\\' =>
|
||||
'Ergebnis\\PHPStan\\Rules\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/ergebnis/phpstan-rules/src',
|
||||
),
|
||||
|
|
@ -97,9 +97,6 @@ class ComposerStaticInitf9e7218f71d5874b5632927df4f72bd7
|
|||
'Nette\\Utils\\JsonException' => __DIR__ . '/..' . '/nette/utils/src/Utils/exceptions.php',
|
||||
'Nette\\Utils\\ObjectHelpers' => __DIR__ . '/..' . '/nette/utils/src/Utils/ObjectHelpers.php',
|
||||
'Nette\\Utils\\Paginator' => __DIR__ . '/..' . '/nette/utils/src/Utils/Paginator.php',
|
||||
'Nette\\Utils\\Process' => __DIR__ . '/..' . '/nette/utils/src/Utils/Process.php',
|
||||
'Nette\\Utils\\ProcessFailedException' => __DIR__ . '/..' . '/nette/utils/src/Utils/exceptions.php',
|
||||
'Nette\\Utils\\ProcessTimeoutException' => __DIR__ . '/..' . '/nette/utils/src/Utils/exceptions.php',
|
||||
'Nette\\Utils\\Random' => __DIR__ . '/..' . '/nette/utils/src/Utils/Random.php',
|
||||
'Nette\\Utils\\Reflection' => __DIR__ . '/..' . '/nette/utils/src/Utils/Reflection.php',
|
||||
'Nette\\Utils\\ReflectionMethod' => __DIR__ . '/..' . '/nette/utils/src/Utils/ReflectionMethod.php',
|
||||
|
|
|
|||
|
|
@ -2,42 +2,42 @@
|
|||
"packages": [
|
||||
{
|
||||
"name": "ergebnis/phpstan-rules",
|
||||
"version": "2.13.1",
|
||||
"version_normalized": "2.13.1.0",
|
||||
"version": "2.12.0",
|
||||
"version_normalized": "2.12.0.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/ergebnis/phpstan-rules.git",
|
||||
"reference": "f69db86b98595c34fc1f61c89fe3b380141aa519"
|
||||
"reference": "c4e0121a937b3b551f800a86e7d78794da2783ea"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/ergebnis/phpstan-rules/zipball/f69db86b98595c34fc1f61c89fe3b380141aa519",
|
||||
"reference": "f69db86b98595c34fc1f61c89fe3b380141aa519",
|
||||
"url": "https://api.github.com/repos/ergebnis/phpstan-rules/zipball/c4e0121a937b3b551f800a86e7d78794da2783ea",
|
||||
"reference": "c4e0121a937b3b551f800a86e7d78794da2783ea",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-mbstring": "*",
|
||||
"php": "~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0",
|
||||
"phpstan/phpstan": "^2.1.35"
|
||||
"phpstan/phpstan": "^2.1.8"
|
||||
},
|
||||
"require-dev": {
|
||||
"codeception/codeception": "^4.0.0 || ^5.0.0",
|
||||
"doctrine/orm": "^2.20.0 || ^3.3.0",
|
||||
"ergebnis/composer-normalize": "^2.49.0",
|
||||
"ergebnis/license": "^2.7.0",
|
||||
"ergebnis/php-cs-fixer-config": "^6.59.0",
|
||||
"ergebnis/composer-normalize": "^2.47.0",
|
||||
"ergebnis/license": "^2.6.0",
|
||||
"ergebnis/php-cs-fixer-config": "^6.54.0",
|
||||
"ergebnis/phpunit-slow-test-detector": "^2.20.0",
|
||||
"fakerphp/faker": "^1.24.1",
|
||||
"phpstan/extension-installer": "^1.4.3",
|
||||
"phpstan/phpstan-deprecation-rules": "^2.0.3",
|
||||
"phpstan/phpstan-phpunit": "^2.0.12",
|
||||
"phpstan/phpstan-strict-rules": "^2.0.8",
|
||||
"phpstan/phpstan-phpunit": "^2.0.7",
|
||||
"phpstan/phpstan-strict-rules": "^2.0.6",
|
||||
"phpunit/phpunit": "^9.6.21",
|
||||
"psr/container": "^2.0.2",
|
||||
"symfony/finder": "^5.4.45",
|
||||
"symfony/process": "^5.4.47"
|
||||
},
|
||||
"time": "2026-01-27T17:13:06+00:00",
|
||||
"time": "2025-09-07T13:31:33+00:00",
|
||||
"type": "phpstan-extension",
|
||||
"extra": {
|
||||
"phpstan": {
|
||||
|
|
@ -76,6 +76,98 @@
|
|||
},
|
||||
"install-path": "../ergebnis/phpstan-rules"
|
||||
},
|
||||
{
|
||||
"name": "nette/utils",
|
||||
"version": "v4.0.8",
|
||||
"version_normalized": "4.0.8.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/nette/utils.git",
|
||||
"reference": "c930ca4e3cf4f17dcfb03037703679d2396d2ede"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/nette/utils/zipball/c930ca4e3cf4f17dcfb03037703679d2396d2ede",
|
||||
"reference": "c930ca4e3cf4f17dcfb03037703679d2396d2ede",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "8.0 - 8.5"
|
||||
},
|
||||
"conflict": {
|
||||
"nette/finder": "<3",
|
||||
"nette/schema": "<1.2.2"
|
||||
},
|
||||
"require-dev": {
|
||||
"jetbrains/phpstorm-attributes": "^1.2",
|
||||
"nette/tester": "^2.5",
|
||||
"phpstan/phpstan-nette": "^2.0@stable",
|
||||
"tracy/tracy": "^2.9"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-gd": "to use Image",
|
||||
"ext-iconv": "to use Strings::webalize(), toAscii(), chr() and reverse()",
|
||||
"ext-intl": "to use Strings::webalize(), toAscii(), normalize() and compare()",
|
||||
"ext-json": "to use Nette\\Utils\\Json",
|
||||
"ext-mbstring": "to use Strings::lower() etc...",
|
||||
"ext-tokenizer": "to use Nette\\Utils\\Reflection::getUseStatements()"
|
||||
},
|
||||
"time": "2025-08-06T21:43:34+00:00",
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "4.0-dev"
|
||||
}
|
||||
},
|
||||
"installation-source": "dist",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Nette\\": "src"
|
||||
},
|
||||
"classmap": [
|
||||
"src/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"BSD-3-Clause",
|
||||
"GPL-2.0-only",
|
||||
"GPL-3.0-only"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "David Grudl",
|
||||
"homepage": "https://davidgrudl.com"
|
||||
},
|
||||
{
|
||||
"name": "Nette Community",
|
||||
"homepage": "https://nette.org/contributors"
|
||||
}
|
||||
],
|
||||
"description": "🛠 Nette Utils: lightweight utilities for string & array manipulation, image handling, safe JSON encoding/decoding, validation, slug or strong password generating etc.",
|
||||
"homepage": "https://nette.org",
|
||||
"keywords": [
|
||||
"array",
|
||||
"core",
|
||||
"datetime",
|
||||
"images",
|
||||
"json",
|
||||
"nette",
|
||||
"paginator",
|
||||
"password",
|
||||
"slugify",
|
||||
"string",
|
||||
"unicode",
|
||||
"utf-8",
|
||||
"utility",
|
||||
"validation"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/nette/utils/issues",
|
||||
"source": "https://github.com/nette/utils/tree/v4.0.8"
|
||||
},
|
||||
"install-path": "../nette/utils"
|
||||
},
|
||||
{
|
||||
"name": "phpstan/extension-installer",
|
||||
"version": "1.4.3",
|
||||
|
|
@ -129,12 +221,17 @@
|
|||
},
|
||||
{
|
||||
"name": "phpstan/phpstan",
|
||||
"version": "2.2.7",
|
||||
"version_normalized": "2.2.7.0",
|
||||
"version": "2.1.28",
|
||||
"version_normalized": "2.1.28.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/phpstan/phpstan.git",
|
||||
"reference": "578fa296a166605d97b94091f724f1257185d278"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/692db47b9dddb0487934e5236e77d48594aef921",
|
||||
"reference": "692db47b9dddb0487934e5236e77d48594aef921",
|
||||
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/578fa296a166605d97b94091f724f1257185d278",
|
||||
"reference": "578fa296a166605d97b94091f724f1257185d278",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
|
@ -143,7 +240,7 @@
|
|||
"conflict": {
|
||||
"phpstan/phpstan-shim": "*"
|
||||
},
|
||||
"time": "2026-07-29T17:39:32+00:00",
|
||||
"time": "2025-09-19T08:58:49+00:00",
|
||||
"bin": [
|
||||
"phpstan",
|
||||
"phpstan.phar"
|
||||
|
|
@ -159,17 +256,6 @@
|
|||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Ondřej Mirtes"
|
||||
},
|
||||
{
|
||||
"name": "Markus Staab"
|
||||
},
|
||||
{
|
||||
"name": "Vincent Langlet"
|
||||
}
|
||||
],
|
||||
"description": "PHPStan - PHP Static Analysis Tool",
|
||||
"keywords": [
|
||||
"dev",
|
||||
|
|
@ -196,31 +282,30 @@
|
|||
},
|
||||
{
|
||||
"name": "phpstan/phpstan-strict-rules",
|
||||
"version": "2.0.12",
|
||||
"version_normalized": "2.0.12.0",
|
||||
"version": "2.0.6",
|
||||
"version_normalized": "2.0.6.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/phpstan/phpstan-strict-rules.git",
|
||||
"reference": "2bc5ae19ae965663b62ac907ee6342c3903ec93b"
|
||||
"reference": "f9f77efa9de31992a832ff77ea52eb42d675b094"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/2bc5ae19ae965663b62ac907ee6342c3903ec93b",
|
||||
"reference": "2bc5ae19ae965663b62ac907ee6342c3903ec93b",
|
||||
"url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/f9f77efa9de31992a832ff77ea52eb42d675b094",
|
||||
"reference": "f9f77efa9de31992a832ff77ea52eb42d675b094",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^7.4 || ^8.0",
|
||||
"phpstan/phpstan": "^2.1.52"
|
||||
"phpstan/phpstan": "^2.0.4"
|
||||
},
|
||||
"require-dev": {
|
||||
"php-parallel-lint/php-parallel-lint": "^1.2",
|
||||
"phpstan/phpstan-deprecation-rules": "^2.0",
|
||||
"phpstan/phpstan-phpunit": "^2.0",
|
||||
"phpunit/phpunit": "^9.6",
|
||||
"shipmonk/name-collision-detector": "^2.1"
|
||||
"phpunit/phpunit": "^9.6"
|
||||
},
|
||||
"time": "2026-07-19T07:24:06+00:00",
|
||||
"time": "2025-07-21T12:19:29+00:00",
|
||||
"type": "phpstan-extension",
|
||||
"extra": {
|
||||
"phpstan": {
|
||||
|
|
@ -240,60 +325,44 @@
|
|||
"MIT"
|
||||
],
|
||||
"description": "Extra strict and opinionated rules for PHPStan",
|
||||
"keywords": [
|
||||
"static analysis"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/phpstan/phpstan-strict-rules/issues",
|
||||
"source": "https://github.com/phpstan/phpstan-strict-rules/tree/2.0.12"
|
||||
"source": "https://github.com/phpstan/phpstan-strict-rules/tree/2.0.6"
|
||||
},
|
||||
"install-path": "../phpstan/phpstan-strict-rules"
|
||||
},
|
||||
{
|
||||
"name": "tomasvotruba/type-coverage",
|
||||
"version": "2.3.0",
|
||||
"version_normalized": "2.3.0.0",
|
||||
"version": "2.0.2",
|
||||
"version_normalized": "2.0.2.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/TomasVotruba/type-coverage.git",
|
||||
"reference": "7ba48c1bf3bbb7ffe99caacc7017bae5ee72d644"
|
||||
"reference": "d033429580f2c18bda538fa44f2939236a990e0c"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/TomasVotruba/type-coverage/zipball/7ba48c1bf3bbb7ffe99caacc7017bae5ee72d644",
|
||||
"reference": "7ba48c1bf3bbb7ffe99caacc7017bae5ee72d644",
|
||||
"url": "https://api.github.com/repos/TomasVotruba/type-coverage/zipball/d033429580f2c18bda538fa44f2939236a990e0c",
|
||||
"reference": "d033429580f2c18bda538fa44f2939236a990e0c",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^8.4",
|
||||
"phpstan/phpstan": "^2.2",
|
||||
"webmozart/assert": "^1.11 || ^2.1"
|
||||
"nette/utils": "^3.2 || ^4.0",
|
||||
"php": "^7.4 || ^8.0",
|
||||
"phpstan/phpstan": "^2.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpstan/extension-installer": "^1.4",
|
||||
"phpunit/phpunit": "^13.2",
|
||||
"rector/jack": "^1.0",
|
||||
"rector/rector": "^2.5",
|
||||
"shipmonk/composer-dependency-analyser": "^1.8",
|
||||
"symfony/dom-crawler": "^8.1",
|
||||
"symplify/easy-coding-standard": "^13.2",
|
||||
"tomasvotruba/unused-public": "^2.2",
|
||||
"tracy/tracy": "^2.12"
|
||||
},
|
||||
"time": "2026-07-29T20:32:37+00:00",
|
||||
"time": "2025-01-07T00:10:26+00:00",
|
||||
"type": "phpstan-extension",
|
||||
"extra": {
|
||||
"phpstan": {
|
||||
"includes": [
|
||||
"config/extension.neon",
|
||||
"packages/type-perfect/config/extension.neon"
|
||||
"config/extension.neon"
|
||||
]
|
||||
}
|
||||
},
|
||||
"installation-source": "dist",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Rector\\TypePerfect\\": "packages/type-perfect/src",
|
||||
"TomasVotruba\\TypeCoverage\\": "src"
|
||||
}
|
||||
},
|
||||
|
|
@ -308,7 +377,7 @@
|
|||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/TomasVotruba/type-coverage/issues",
|
||||
"source": "https://github.com/TomasVotruba/type-coverage/tree/2.3.0"
|
||||
"source": "https://github.com/TomasVotruba/type-coverage/tree/2.0.2"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
|
|
@ -321,84 +390,15 @@
|
|||
}
|
||||
],
|
||||
"install-path": "../tomasvotruba/type-coverage"
|
||||
},
|
||||
{
|
||||
"name": "webmozart/assert",
|
||||
"version": "2.4.1",
|
||||
"version_normalized": "2.4.1.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/webmozarts/assert.git",
|
||||
"reference": "2ccb7c2e821038c03a3e6e1700c570c158c55f70"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/webmozarts/assert/zipball/2ccb7c2e821038c03a3e6e1700c570c158c55f70",
|
||||
"reference": "2ccb7c2e821038c03a3e6e1700c570c158c55f70",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-ctype": "*",
|
||||
"ext-date": "*",
|
||||
"ext-filter": "*",
|
||||
"php": "^8.2"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-intl": "",
|
||||
"ext-simplexml": "",
|
||||
"ext-spl": ""
|
||||
},
|
||||
"time": "2026-06-15T15:31:57+00:00",
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"psalm": {
|
||||
"pluginClass": "Webmozart\\Assert\\PsalmPlugin"
|
||||
},
|
||||
"branch-alias": {
|
||||
"dev-master": "2.0-dev",
|
||||
"dev-feature/2-0": "2.0-dev"
|
||||
}
|
||||
},
|
||||
"installation-source": "dist",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Webmozart\\Assert\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Bernhard Schussek",
|
||||
"email": "bschussek@gmail.com"
|
||||
},
|
||||
{
|
||||
"name": "Woody Gilk",
|
||||
"email": "woody.gilk@gmail.com"
|
||||
}
|
||||
],
|
||||
"description": "Assertions to validate method input/output with nice error messages.",
|
||||
"keywords": [
|
||||
"assert",
|
||||
"check",
|
||||
"validate"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/webmozarts/assert/issues",
|
||||
"source": "https://github.com/webmozarts/assert/tree/2.4.1"
|
||||
},
|
||||
"install-path": "../webmozart/assert"
|
||||
}
|
||||
],
|
||||
"dev": true,
|
||||
"dev-package-names": [
|
||||
"ergebnis/phpstan-rules",
|
||||
"nette/utils",
|
||||
"phpstan/extension-installer",
|
||||
"phpstan/phpstan",
|
||||
"phpstan/phpstan-strict-rules",
|
||||
"tomasvotruba/type-coverage",
|
||||
"webmozart/assert"
|
||||
"tomasvotruba/type-coverage"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
'name' => '__root__',
|
||||
'pretty_version' => 'dev-main',
|
||||
'version' => 'dev-main',
|
||||
'reference' => '4f08958e05d5bfc330b2f0b05252a7b7ddbe0558',
|
||||
'reference' => '0ef7d66753a1282b23890c2d9a8c775607e8e07f',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../../',
|
||||
'aliases' => array(),
|
||||
|
|
@ -13,21 +13,30 @@
|
|||
'__root__' => array(
|
||||
'pretty_version' => 'dev-main',
|
||||
'version' => 'dev-main',
|
||||
'reference' => '4f08958e05d5bfc330b2f0b05252a7b7ddbe0558',
|
||||
'reference' => '0ef7d66753a1282b23890c2d9a8c775607e8e07f',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../../',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'ergebnis/phpstan-rules' => array(
|
||||
'pretty_version' => '2.13.1',
|
||||
'version' => '2.13.1.0',
|
||||
'reference' => 'f69db86b98595c34fc1f61c89fe3b380141aa519',
|
||||
'pretty_version' => '2.12.0',
|
||||
'version' => '2.12.0.0',
|
||||
'reference' => 'c4e0121a937b3b551f800a86e7d78794da2783ea',
|
||||
'type' => 'phpstan-extension',
|
||||
'install_path' => __DIR__ . '/../ergebnis/phpstan-rules',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => true,
|
||||
),
|
||||
'nette/utils' => array(
|
||||
'pretty_version' => 'v4.0.8',
|
||||
'version' => '4.0.8.0',
|
||||
'reference' => 'c930ca4e3cf4f17dcfb03037703679d2396d2ede',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../nette/utils',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => true,
|
||||
),
|
||||
'phpstan/extension-installer' => array(
|
||||
'pretty_version' => '1.4.3',
|
||||
'version' => '1.4.3.0',
|
||||
|
|
@ -38,40 +47,31 @@
|
|||
'dev_requirement' => true,
|
||||
),
|
||||
'phpstan/phpstan' => array(
|
||||
'pretty_version' => '2.2.7',
|
||||
'version' => '2.2.7.0',
|
||||
'reference' => '692db47b9dddb0487934e5236e77d48594aef921',
|
||||
'pretty_version' => '2.1.28',
|
||||
'version' => '2.1.28.0',
|
||||
'reference' => '578fa296a166605d97b94091f724f1257185d278',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../phpstan/phpstan',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => true,
|
||||
),
|
||||
'phpstan/phpstan-strict-rules' => array(
|
||||
'pretty_version' => '2.0.12',
|
||||
'version' => '2.0.12.0',
|
||||
'reference' => '2bc5ae19ae965663b62ac907ee6342c3903ec93b',
|
||||
'pretty_version' => '2.0.6',
|
||||
'version' => '2.0.6.0',
|
||||
'reference' => 'f9f77efa9de31992a832ff77ea52eb42d675b094',
|
||||
'type' => 'phpstan-extension',
|
||||
'install_path' => __DIR__ . '/../phpstan/phpstan-strict-rules',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => true,
|
||||
),
|
||||
'tomasvotruba/type-coverage' => array(
|
||||
'pretty_version' => '2.3.0',
|
||||
'version' => '2.3.0.0',
|
||||
'reference' => '7ba48c1bf3bbb7ffe99caacc7017bae5ee72d644',
|
||||
'pretty_version' => '2.0.2',
|
||||
'version' => '2.0.2.0',
|
||||
'reference' => 'd033429580f2c18bda538fa44f2939236a990e0c',
|
||||
'type' => 'phpstan-extension',
|
||||
'install_path' => __DIR__ . '/../tomasvotruba/type-coverage',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => true,
|
||||
),
|
||||
'webmozart/assert' => array(
|
||||
'pretty_version' => '2.4.1',
|
||||
'version' => '2.4.1.0',
|
||||
'reference' => '2ccb7c2e821038c03a3e6e1700c570c158c55f70',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../webmozart/assert',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => true,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -6,27 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|||
|
||||
## Unreleased
|
||||
|
||||
For a full diff see [`2.13.1...main`][2.13.1...main].
|
||||
|
||||
## [`2.13.1`][2.13.1]
|
||||
|
||||
For a full diff see [`2.13.0...2.13.1`][2.13.0...2.13.1].
|
||||
|
||||
### Fixed
|
||||
|
||||
- Adjusted `Files\NoPhpstanIgnoreRule` to traverse into all nodes ([#1011]), by [@localheinz]
|
||||
|
||||
## [`2.13.0`][2.13.0]
|
||||
|
||||
For a full diff see [`2.12.0...2.13.0`][2.12.0...2.13.0].
|
||||
|
||||
### Added
|
||||
|
||||
- Added `Files\NoPhpstanIgnoreRule`, which reports an error when a `@phpstan-ignore`, `@phpstan-ignore-line`, or `@phpstan-ignore-next-line` tag is used to suppress an error ([#1009]), by [@localheinz]
|
||||
|
||||
### Changed
|
||||
|
||||
- Required `phpstan/phpstan:^2.1.35` ([#1010]), by [@localheinz]
|
||||
For a full diff see [`2.12.0...main`][2.12.0...main].
|
||||
|
||||
## [`2.12.0`][2.12.0]
|
||||
|
||||
|
|
@ -608,8 +588,6 @@ For a full diff see [`362c7ea...0.1.0`][362c7ea...0.1.0].
|
|||
[2.10.5]: https://github.com/ergebnis/phpstan-rules/releases/tag/2.10.5
|
||||
[2.11.0]: https://github.com/ergebnis/phpstan-rules/releases/tag/2.11.0
|
||||
[2.12.0]: https://github.com/ergebnis/phpstan-rules/releases/tag/2.12.0
|
||||
[2.13.0]: https://github.com/ergebnis/phpstan-rules/releases/tag/2.13.0
|
||||
[2.13.1]: https://github.com/ergebnis/phpstan-rules/releases/tag/2.13.1
|
||||
|
||||
[362c7ea...0.1.0]: https://github.com/ergebnis/phpstan-rules/compare/362c7ea...0.1.0
|
||||
[0.1.0...0.2.0]: https://github.com/ergebnis/phpstan-rules/compare/0.1.0...0.2.0
|
||||
|
|
@ -660,9 +638,7 @@ For a full diff see [`362c7ea...0.1.0`][362c7ea...0.1.0].
|
|||
[2.10.4...2.10.5]: https://github.com/ergebnis/phpstan-rules/compare/2.10.4...2.10.5
|
||||
[2.10.5...2.11.0]: https://github.com/ergebnis/phpstan-rules/compare/2.10.5...2.11.0
|
||||
[2.11.0...2.12.0]: https://github.com/ergebnis/phpstan-rules/compare/2.11.0...2.12.0
|
||||
[2.12.0...2.13.0]: https://github.com/ergebnis/phpstan-rules/compare/2.12.0...2.13.0
|
||||
[2.13.0...2.13.1]: https://github.com/ergebnis/phpstan-rules/compare/2.13.0...2.13.1
|
||||
[2.13.1...main]: https://github.com/ergebnis/phpstan-rules/compare/2.13.1...main
|
||||
[2.12.0...main]: https://github.com/ergebnis/phpstan-rules/compare/2.12.0...main
|
||||
|
||||
[#1]: https://github.com/ergebnis/phpstan-rules/pull/1
|
||||
[#4]: https://github.com/ergebnis/phpstan-rules/pull/4
|
||||
|
|
@ -760,9 +736,6 @@ For a full diff see [`362c7ea...0.1.0`][362c7ea...0.1.0].
|
|||
[#958]: https://github.com/ergebnis/phpstan-rules/pull/958
|
||||
[#972]: https://github.com/ergebnis/phpstan-rules/pull/972
|
||||
[#977]: https://github.com/ergebnis/phpstan-rules/pull/977
|
||||
[#1009]: https://github.com/ergebnis/phpstan-rules/pull/1009
|
||||
[#1010]: https://github.com/ergebnis/phpstan-rules/pull/1010
|
||||
[#1011]: https://github.com/ergebnis/phpstan-rules/pull/1011
|
||||
|
||||
[@cosmastech]: https://github.com/cosmastech
|
||||
[@enumag]: https://github.com/enumag
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2018-2026 Andreas Möller
|
||||
Copyright (c) 2018-2025 Andreas Möller
|
||||
|
||||
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
|
||||
|
|
|
|||
|
|
@ -58,7 +58,6 @@ This package provides the following rules for use with [`phpstan/phpstan`](https
|
|||
- [`Ergebnis\PHPStan\Rules\Expressions\NoEvalRule`](https://github.com/ergebnis/phpstan-rules#expressionsnoevalrule)
|
||||
- [`Ergebnis\PHPStan\Rules\Expressions\NoIssetRule`](https://github.com/ergebnis/phpstan-rules#expressionsnoissetrule)
|
||||
- [`Ergebnis\PHPStan\Rules\Files\DeclareStrictTypesRule`](https://github.com/ergebnis/phpstan-rules#filesdeclarestricttypesrule)
|
||||
- [`Ergebnis\PHPStan\Rules\Files\NoPhpstanIgnoreRule`](https://github.com/ergebnis/phpstan-rules#filesnophpstanignorerule)
|
||||
- [`Ergebnis\PHPStan\Rules\Functions\NoNullableReturnTypeDeclarationRule`](https://github.com/ergebnis/phpstan-rules#functionsnonullablereturntypedeclarationrule)
|
||||
- [`Ergebnis\PHPStan\Rules\Functions\NoParameterPassedByReferenceRule`](https://github.com/ergebnis/phpstan-rules#functionsnoparameterpassedbyreferencerule)
|
||||
- [`Ergebnis\PHPStan\Rules\Functions\NoParameterWithNullableTypeDeclarationRule`](https://github.com/ergebnis/phpstan-rules#functionsnoparameterwithnullabletypedeclarationrule)
|
||||
|
|
@ -349,21 +348,6 @@ parameters:
|
|||
enabled: false
|
||||
```
|
||||
|
||||
#### `Files\NoPhpstanIgnoreRule`
|
||||
|
||||
This rule reports an error when a `@phpstan-ignore`, `@phpstan-ignore-line`, or `@phpstan-ignore-next-line` tag is used to [ignore errors reported by `phpstan/phpstan`](https://phpstan.org/user-guide/ignoring-errors#ignoring-in-code-using-phpdocs).
|
||||
|
||||
##### Disabling the rule
|
||||
|
||||
You can set the `enabled` parameter to `false` to disable this rule.
|
||||
|
||||
```neon
|
||||
parameters:
|
||||
ergebnis:
|
||||
noPhpstanIgnore:
|
||||
enabled: false
|
||||
```
|
||||
|
||||
### Functions
|
||||
|
||||
#### `Functions\NoNullableReturnTypeDeclarationRule`
|
||||
|
|
|
|||
|
|
@ -23,20 +23,20 @@
|
|||
"require": {
|
||||
"php": "~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0",
|
||||
"ext-mbstring": "*",
|
||||
"phpstan/phpstan": "^2.1.35"
|
||||
"phpstan/phpstan": "^2.1.8"
|
||||
},
|
||||
"require-dev": {
|
||||
"codeception/codeception": "^4.0.0 || ^5.0.0",
|
||||
"doctrine/orm": "^2.20.0 || ^3.3.0",
|
||||
"ergebnis/composer-normalize": "^2.49.0",
|
||||
"ergebnis/license": "^2.7.0",
|
||||
"ergebnis/php-cs-fixer-config": "^6.59.0",
|
||||
"ergebnis/composer-normalize": "^2.47.0",
|
||||
"ergebnis/license": "^2.6.0",
|
||||
"ergebnis/php-cs-fixer-config": "^6.54.0",
|
||||
"ergebnis/phpunit-slow-test-detector": "^2.20.0",
|
||||
"fakerphp/faker": "^1.24.1",
|
||||
"phpstan/extension-installer": "^1.4.3",
|
||||
"phpstan/phpstan-deprecation-rules": "^2.0.3",
|
||||
"phpstan/phpstan-phpunit": "^2.0.12",
|
||||
"phpstan/phpstan-strict-rules": "^2.0.8",
|
||||
"phpstan/phpstan-phpunit": "^2.0.7",
|
||||
"phpstan/phpstan-strict-rules": "^2.0.6",
|
||||
"phpunit/phpunit": "^9.6.21",
|
||||
"psr/container": "^2.0.2",
|
||||
"symfony/finder": "^5.4.45",
|
||||
|
|
|
|||
|
|
@ -25,8 +25,6 @@ conditionalTags:
|
|||
phpstan.rules.rule: %ergebnis.noIsset.enabled%
|
||||
Ergebnis\PHPStan\Rules\Files\DeclareStrictTypesRule:
|
||||
phpstan.rules.rule: %ergebnis.declareStrictTypes.enabled%
|
||||
Ergebnis\PHPStan\Rules\Files\NoPhpstanIgnoreRule:
|
||||
phpstan.rules.rule: %ergebnis.noPhpstanIgnore.enabled%
|
||||
Ergebnis\PHPStan\Rules\Functions\NoNullableReturnTypeDeclarationRule:
|
||||
phpstan.rules.rule: %ergebnis.noNullableReturnTypeDeclaration.enabled%
|
||||
Ergebnis\PHPStan\Rules\Functions\NoParameterPassedByReferenceRule:
|
||||
|
|
@ -104,8 +102,6 @@ parameters:
|
|||
enabled: %ergebnis.allRules%
|
||||
noParameterWithNullDefaultValue:
|
||||
enabled: %ergebnis.allRules%
|
||||
noPhpstanIgnore:
|
||||
enabled: %ergebnis.allRules%
|
||||
noReturnByReference:
|
||||
enabled: %ergebnis.allRules%
|
||||
noSwitch:
|
||||
|
|
@ -180,9 +176,6 @@ parametersSchema:
|
|||
noParameterWithNullDefaultValue: structure([
|
||||
enabled: bool(),
|
||||
])
|
||||
noPhpstanIgnore: structure([
|
||||
enabled: bool(),
|
||||
])
|
||||
noReturnByReference: structure([
|
||||
enabled: bool(),
|
||||
])
|
||||
|
|
@ -245,9 +238,6 @@ services:
|
|||
-
|
||||
class: Ergebnis\PHPStan\Rules\Files\DeclareStrictTypesRule
|
||||
|
||||
-
|
||||
class: Ergebnis\PHPStan\Rules\Files\NoPhpstanIgnoreRule
|
||||
|
||||
-
|
||||
class: Ergebnis\PHPStan\Rules\Functions\NoNullableReturnTypeDeclarationRule
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Copyright (c) 2018-2026 Andreas Möller
|
||||
* Copyright (c) 2018-2025 Andreas Möller
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE.md file that was distributed with this source code.
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Copyright (c) 2018-2026 Andreas Möller
|
||||
* Copyright (c) 2018-2025 Andreas Möller
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE.md file that was distributed with this source code.
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Copyright (c) 2018-2026 Andreas Möller
|
||||
* Copyright (c) 2018-2025 Andreas Möller
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE.md file that was distributed with this source code.
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Copyright (c) 2018-2026 Andreas Möller
|
||||
* Copyright (c) 2018-2025 Andreas Möller
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE.md file that was distributed with this source code.
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Copyright (c) 2018-2026 Andreas Möller
|
||||
* Copyright (c) 2018-2025 Andreas Möller
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE.md file that was distributed with this source code.
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Copyright (c) 2018-2026 Andreas Möller
|
||||
* Copyright (c) 2018-2025 Andreas Möller
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE.md file that was distributed with this source code.
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Copyright (c) 2018-2026 Andreas Möller
|
||||
* Copyright (c) 2018-2025 Andreas Möller
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE.md file that was distributed with this source code.
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Copyright (c) 2018-2026 Andreas Möller
|
||||
* Copyright (c) 2018-2025 Andreas Möller
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE.md file that was distributed with this source code.
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Copyright (c) 2018-2026 Andreas Möller
|
||||
* Copyright (c) 2018-2025 Andreas Möller
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE.md file that was distributed with this source code.
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Copyright (c) 2018-2026 Andreas Möller
|
||||
* Copyright (c) 2018-2025 Andreas Möller
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE.md file that was distributed with this source code.
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Copyright (c) 2018-2026 Andreas Möller
|
||||
* Copyright (c) 2018-2025 Andreas Möller
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE.md file that was distributed with this source code.
|
||||
|
|
@ -110,11 +110,6 @@ final class ErrorIdentifier
|
|||
return new self('noNullableReturnTypeDeclaration');
|
||||
}
|
||||
|
||||
public static function noPhpstanIgnore(): self
|
||||
{
|
||||
return new self('noPhpstanIgnore');
|
||||
}
|
||||
|
||||
public static function noReturnByReference(): self
|
||||
{
|
||||
return new self('noReturnByReference');
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Copyright (c) 2018-2026 Andreas Möller
|
||||
* Copyright (c) 2018-2025 Andreas Möller
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE.md file that was distributed with this source code.
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Copyright (c) 2018-2026 Andreas Möller
|
||||
* Copyright (c) 2018-2025 Andreas Möller
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE.md file that was distributed with this source code.
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Copyright (c) 2018-2026 Andreas Möller
|
||||
* Copyright (c) 2018-2025 Andreas Möller
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE.md file that was distributed with this source code.
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Copyright (c) 2018-2026 Andreas Möller
|
||||
* Copyright (c) 2018-2025 Andreas Möller
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE.md file that was distributed with this source code.
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Copyright (c) 2018-2026 Andreas Möller
|
||||
* Copyright (c) 2018-2025 Andreas Möller
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE.md file that was distributed with this source code.
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Copyright (c) 2018-2026 Andreas Möller
|
||||
* Copyright (c) 2018-2025 Andreas Möller
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE.md file that was distributed with this source code.
|
||||
|
|
|
|||
|
|
@ -1,70 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Copyright (c) 2018-2026 Andreas Möller
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE.md file that was distributed with this source code.
|
||||
*
|
||||
* @see https://github.com/ergebnis/phpstan-rules
|
||||
*/
|
||||
|
||||
namespace Ergebnis\PHPStan\Rules\Files;
|
||||
|
||||
use Ergebnis\PHPStan\Rules\ErrorIdentifier;
|
||||
use PhpParser\Node;
|
||||
use PhpParser\NodeFinder;
|
||||
use PHPStan\Analyser;
|
||||
use PHPStan\Node\FileNode;
|
||||
use PHPStan\Rules;
|
||||
|
||||
/**
|
||||
* @implements Rules\Rule<FileNode>
|
||||
*/
|
||||
final class NoPhpstanIgnoreRule implements Rules\Rule
|
||||
{
|
||||
public function getNodeType(): string
|
||||
{
|
||||
return FileNode::class;
|
||||
}
|
||||
|
||||
public function processNode(
|
||||
Node $node,
|
||||
Analyser\Scope $scope
|
||||
): array {
|
||||
$nodeFinder = new NodeFinder();
|
||||
|
||||
$foundNodes = $nodeFinder->find($node->getNodes(), static function (Node $node): bool {
|
||||
return true;
|
||||
});
|
||||
|
||||
$errors = [];
|
||||
|
||||
foreach ($foundNodes as $foundNode) {
|
||||
foreach ($foundNode->getComments() as $comment) {
|
||||
foreach (\explode("\n", $comment->getText()) as $index => $line) {
|
||||
if (0 === \preg_match('/@phpstan-ignore(-line|-next-line)?(?=\s|$|,)/', $line, $matches)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$tag = $matches[0];
|
||||
|
||||
$message = \sprintf(
|
||||
'Errors reported by phpstan/phpstan should not be ignored via "%s", fix the error or use the baseline instead.',
|
||||
$tag,
|
||||
);
|
||||
|
||||
$errors[] = Rules\RuleErrorBuilder::message($message)
|
||||
->identifier(ErrorIdentifier::noPhpstanIgnore()->toString())
|
||||
->line($comment->getStartLine() + $index)
|
||||
->nonIgnorable()
|
||||
->build();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $errors;
|
||||
}
|
||||
}
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Copyright (c) 2018-2026 Andreas Möller
|
||||
* Copyright (c) 2018-2025 Andreas Möller
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE.md file that was distributed with this source code.
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Copyright (c) 2018-2026 Andreas Möller
|
||||
* Copyright (c) 2018-2025 Andreas Möller
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE.md file that was distributed with this source code.
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Copyright (c) 2018-2026 Andreas Möller
|
||||
* Copyright (c) 2018-2025 Andreas Möller
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE.md file that was distributed with this source code.
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Copyright (c) 2018-2026 Andreas Möller
|
||||
* Copyright (c) 2018-2025 Andreas Möller
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE.md file that was distributed with this source code.
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Copyright (c) 2018-2026 Andreas Möller
|
||||
* Copyright (c) 2018-2025 Andreas Möller
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE.md file that was distributed with this source code.
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Copyright (c) 2018-2026 Andreas Möller
|
||||
* Copyright (c) 2018-2025 Andreas Möller
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE.md file that was distributed with this source code.
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Copyright (c) 2018-2026 Andreas Möller
|
||||
* Copyright (c) 2018-2025 Andreas Möller
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE.md file that was distributed with this source code.
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Copyright (c) 2018-2026 Andreas Möller
|
||||
* Copyright (c) 2018-2025 Andreas Möller
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE.md file that was distributed with this source code.
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Copyright (c) 2018-2026 Andreas Möller
|
||||
* Copyright (c) 2018-2025 Andreas Möller
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE.md file that was distributed with this source code.
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Copyright (c) 2018-2026 Andreas Möller
|
||||
* Copyright (c) 2018-2025 Andreas Möller
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE.md file that was distributed with this source code.
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Copyright (c) 2018-2026 Andreas Möller
|
||||
* Copyright (c) 2018-2025 Andreas Möller
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE.md file that was distributed with this source code.
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Copyright (c) 2018-2026 Andreas Möller
|
||||
* Copyright (c) 2018-2025 Andreas Möller
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE.md file that was distributed with this source code.
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Copyright (c) 2018-2026 Andreas Möller
|
||||
* Copyright (c) 2018-2025 Andreas Möller
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE.md file that was distributed with this source code.
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Copyright (c) 2018-2026 Andreas Möller
|
||||
* Copyright (c) 2018-2025 Andreas Möller
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE.md file that was distributed with this source code.
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Copyright (c) 2018-2026 Andreas Möller
|
||||
* Copyright (c) 2018-2025 Andreas Möller
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE.md file that was distributed with this source code.
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Copyright (c) 2018-2026 Andreas Möller
|
||||
* Copyright (c) 2018-2025 Andreas Möller
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE.md file that was distributed with this source code.
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Copyright (c) 2018-2026 Andreas Möller
|
||||
* Copyright (c) 2018-2025 Andreas Möller
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE.md file that was distributed with this source code.
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Copyright (c) 2018-2026 Andreas Möller
|
||||
* Copyright (c) 2018-2025 Andreas Möller
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE.md file that was distributed with this source code.
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Copyright (c) 2018-2026 Andreas Möller
|
||||
* Copyright (c) 2018-2025 Andreas Möller
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE.md file that was distributed with this source code.
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Copyright (c) 2018-2026 Andreas Möller
|
||||
* Copyright (c) 2018-2025 Andreas Möller
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE.md file that was distributed with this source code.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace PHPSTORM_META;
|
||||
|
||||
override(\Nette\Utils\Arrays::get(0), elementType(0));
|
||||
override(\Nette\Utils\Arrays::getRef(0), elementType(0));
|
||||
override(\Nette\Utils\Arrays::grep(0), type(0));
|
||||
override(\Nette\Utils\Arrays::toObject(0), type(1));
|
||||
|
||||
expectedArguments(\Nette\Utils\Image::resize(), 2, \Nette\Utils\Image::ShrinkOnly, \Nette\Utils\Image::Stretch, \Nette\Utils\Image::OrSmaller, \Nette\Utils\Image::OrBigger, \Nette\Utils\Image::Cover);
|
||||
expectedArguments(\Nette\Utils\Image::calculateSize(), 4, \Nette\Utils\Image::ShrinkOnly, \Nette\Utils\Image::Stretch, \Nette\Utils\Image::OrSmaller, \Nette\Utils\Image::OrBigger, \Nette\Utils\Image::Cover);
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
{
|
||||
"name": "nette/utils",
|
||||
"description": "🛠 Nette Utils: lightweight utilities for string & array manipulation, image handling, safe JSON encoding/decoding, validation, slug or strong password generating etc.",
|
||||
"keywords": ["nette", "images", "json", "password", "validation", "utility", "string", "array", "core", "slugify", "utf-8", "unicode", "paginator", "datetime"],
|
||||
"homepage": "https://nette.org",
|
||||
"license": ["BSD-3-Clause", "GPL-2.0-only", "GPL-3.0-only"],
|
||||
"authors": [
|
||||
{
|
||||
"name": "David Grudl",
|
||||
"homepage": "https://davidgrudl.com"
|
||||
},
|
||||
{
|
||||
"name": "Nette Community",
|
||||
"homepage": "https://nette.org/contributors"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": "8.0 - 8.5"
|
||||
},
|
||||
"require-dev": {
|
||||
"nette/tester": "^2.5",
|
||||
"tracy/tracy": "^2.9",
|
||||
"phpstan/phpstan-nette": "^2.0@stable",
|
||||
"jetbrains/phpstorm-attributes": "^1.2"
|
||||
},
|
||||
"conflict": {
|
||||
"nette/finder": "<3",
|
||||
"nette/schema": "<1.2.2"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-iconv": "to use Strings::webalize(), toAscii(), chr() and reverse()",
|
||||
"ext-json": "to use Nette\\Utils\\Json",
|
||||
"ext-intl": "to use Strings::webalize(), toAscii(), normalize() and compare()",
|
||||
"ext-mbstring": "to use Strings::lower() etc...",
|
||||
"ext-gd": "to use Image",
|
||||
"ext-tokenizer": "to use Nette\\Utils\\Reflection::getUseStatements()"
|
||||
},
|
||||
"autoload": {
|
||||
"classmap": ["src/"],
|
||||
"psr-4": {
|
||||
"Nette\\": "src"
|
||||
}
|
||||
},
|
||||
"minimum-stability": "dev",
|
||||
"scripts": {
|
||||
"phpstan": "phpstan analyse",
|
||||
"tester": "tester tests -s"
|
||||
},
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "4.0-dev"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
Licenses
|
||||
========
|
||||
|
||||
Good news! You may use Nette Framework under the terms of either
|
||||
the New BSD License or the GNU General Public License (GPL) version 2 or 3.
|
||||
|
||||
The BSD License is recommended for most projects. It is easy to understand and it
|
||||
places almost no restrictions on what you can do with the framework. If the GPL
|
||||
fits better to your project, you can use the framework under this license.
|
||||
|
||||
You don't have to notify anyone which license you are using. You can freely
|
||||
use Nette Framework in commercial projects as long as the copyright header
|
||||
remains intact.
|
||||
|
||||
Please be advised that the name "Nette Framework" is a protected trademark and its
|
||||
usage has some limitations. So please do not use word "Nette" in the name of your
|
||||
project or top-level domain, and choose a name that stands on its own merits.
|
||||
If your stuff is good, it will not take long to establish a reputation for yourselves.
|
||||
|
||||
|
||||
New BSD License
|
||||
---------------
|
||||
|
||||
Copyright (c) 2004, 2014 David Grudl (https://davidgrudl.com)
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice,
|
||||
this list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
* Neither the name of "Nette Framework" nor the names of its contributors
|
||||
may be used to endorse or promote products derived from this software
|
||||
without specific prior written permission.
|
||||
|
||||
This software is provided by the copyright holders and contributors "as is" and
|
||||
any express or implied warranties, including, but not limited to, the implied
|
||||
warranties of merchantability and fitness for a particular purpose are
|
||||
disclaimed. In no event shall the copyright owner or contributors be liable for
|
||||
any direct, indirect, incidental, special, exemplary, or consequential damages
|
||||
(including, but not limited to, procurement of substitute goods or services;
|
||||
loss of use, data, or profits; or business interruption) however caused and on
|
||||
any theory of liability, whether in contract, strict liability, or tort
|
||||
(including negligence or otherwise) arising in any way out of the use of this
|
||||
software, even if advised of the possibility of such damage.
|
||||
|
||||
|
||||
GNU General Public License
|
||||
--------------------------
|
||||
|
||||
GPL licenses are very very long, so instead of including them here we offer
|
||||
you URLs with full text:
|
||||
|
||||
- [GPL version 2](http://www.gnu.org/licenses/gpl-2.0.html)
|
||||
- [GPL version 3](http://www.gnu.org/licenses/gpl-3.0.html)
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
[](https://doc.nette.org/en/utils)
|
||||
|
||||
[](https://packagist.org/packages/nette/utils)
|
||||
[](https://github.com/nette/utils/actions)
|
||||
[](https://coveralls.io/github/nette/utils?branch=master)
|
||||
[](https://github.com/nette/utils/releases)
|
||||
[](https://github.com/nette/utils/blob/master/license.md)
|
||||
|
||||
|
||||
Introduction
|
||||
------------
|
||||
|
||||
In package nette/utils you will find a set of useful classes for everyday use:
|
||||
|
||||
✅ [Arrays](https://doc.nette.org/utils/arrays)<br>
|
||||
✅ [Callback](https://doc.nette.org/utils/callback) - PHP callbacks<br>
|
||||
✅ [Filesystem](https://doc.nette.org/utils/filesystem) - copying, renaming, …<br>
|
||||
✅ [Finder](https://doc.nette.org/utils/finder) - finds files and directories<br>
|
||||
✅ [Floats](https://doc.nette.org/utils/floats) - floating point numbers<br>
|
||||
✅ [Helper Functions](https://doc.nette.org/utils/helpers)<br>
|
||||
✅ [HTML elements](https://doc.nette.org/utils/html-elements) - generate HTML<br>
|
||||
✅ [Images](https://doc.nette.org/utils/images) - crop, resize, rotate images<br>
|
||||
✅ [Iterables](https://doc.nette.org/utils/iterables) <br>
|
||||
✅ [JSON](https://doc.nette.org/utils/json) - encoding and decoding<br>
|
||||
✅ [Generating Random Strings](https://doc.nette.org/utils/random)<br>
|
||||
✅ [Paginator](https://doc.nette.org/utils/paginator) - pagination math<br>
|
||||
✅ [PHP Reflection](https://doc.nette.org/utils/reflection)<br>
|
||||
✅ [Strings](https://doc.nette.org/utils/strings) - useful text functions<br>
|
||||
✅ [SmartObject](https://doc.nette.org/utils/smartobject) - PHP object enhancements<br>
|
||||
✅ [Type](https://doc.nette.org/utils/type) - PHP data type<br>
|
||||
✅ [Validation](https://doc.nette.org/utils/validators) - validate inputs<br>
|
||||
|
||||
<!---->
|
||||
|
||||
Installation
|
||||
------------
|
||||
|
||||
The recommended way to install is via Composer:
|
||||
|
||||
```
|
||||
composer require nette/utils
|
||||
```
|
||||
|
||||
Nette Utils 4.0 is compatible with PHP 8.0 to 8.5.
|
||||
|
||||
<!---->
|
||||
|
||||
[Support Me](https://github.com/sponsors/dg)
|
||||
--------------------------------------------
|
||||
|
||||
Do you like Nette Utils? Are you looking forward to the new features?
|
||||
|
||||
[](https://github.com/sponsors/dg)
|
||||
|
||||
Thank you!
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Nette Framework (https://nette.org)
|
||||
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Nette;
|
||||
|
||||
|
||||
interface HtmlStringable
|
||||
{
|
||||
/**
|
||||
* Returns string in HTML format
|
||||
*/
|
||||
function __toString(): string;
|
||||
}
|
||||
|
||||
|
||||
interface_exists(Utils\IHtmlString::class);
|
||||
|
|
@ -0,0 +1,150 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Nette Framework (https://nette.org)
|
||||
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Nette\Iterators;
|
||||
|
||||
use Nette;
|
||||
|
||||
|
||||
/**
|
||||
* Smarter caching iterator.
|
||||
*
|
||||
* @property-read bool $first
|
||||
* @property-read bool $last
|
||||
* @property-read bool $empty
|
||||
* @property-read bool $odd
|
||||
* @property-read bool $even
|
||||
* @property-read int $counter
|
||||
* @property-read mixed $nextKey
|
||||
* @property-read mixed $nextValue
|
||||
*/
|
||||
class CachingIterator extends \CachingIterator implements \Countable
|
||||
{
|
||||
use Nette\SmartObject;
|
||||
|
||||
private int $counter = 0;
|
||||
|
||||
|
||||
public function __construct(iterable|\stdClass $iterable)
|
||||
{
|
||||
$iterable = $iterable instanceof \stdClass
|
||||
? new \ArrayIterator($iterable)
|
||||
: Nette\Utils\Iterables::toIterator($iterable);
|
||||
parent::__construct($iterable, 0);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Is the current element the first one?
|
||||
*/
|
||||
public function isFirst(?int $gridWidth = null): bool
|
||||
{
|
||||
return $this->counter === 1 || ($gridWidth && $this->counter !== 0 && (($this->counter - 1) % $gridWidth) === 0);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Is the current element the last one?
|
||||
*/
|
||||
public function isLast(?int $gridWidth = null): bool
|
||||
{
|
||||
return !$this->hasNext() || ($gridWidth && ($this->counter % $gridWidth) === 0);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Is the iterator empty?
|
||||
*/
|
||||
public function isEmpty(): bool
|
||||
{
|
||||
return $this->counter === 0;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Is the counter odd?
|
||||
*/
|
||||
public function isOdd(): bool
|
||||
{
|
||||
return $this->counter % 2 === 1;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Is the counter even?
|
||||
*/
|
||||
public function isEven(): bool
|
||||
{
|
||||
return $this->counter % 2 === 0;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the counter.
|
||||
*/
|
||||
public function getCounter(): int
|
||||
{
|
||||
return $this->counter;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the count of elements.
|
||||
*/
|
||||
public function count(): int
|
||||
{
|
||||
$inner = $this->getInnerIterator();
|
||||
if ($inner instanceof \Countable) {
|
||||
return $inner->count();
|
||||
|
||||
} else {
|
||||
throw new Nette\NotSupportedException('Iterator is not countable.');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Forwards to the next element.
|
||||
*/
|
||||
public function next(): void
|
||||
{
|
||||
parent::next();
|
||||
if (parent::valid()) {
|
||||
$this->counter++;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Rewinds the Iterator.
|
||||
*/
|
||||
public function rewind(): void
|
||||
{
|
||||
parent::rewind();
|
||||
$this->counter = parent::valid() ? 1 : 0;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the next key.
|
||||
*/
|
||||
public function getNextKey(): mixed
|
||||
{
|
||||
return $this->getInnerIterator()->key();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the next element.
|
||||
*/
|
||||
public function getNextValue(): mixed
|
||||
{
|
||||
return $this->getInnerIterator()->current();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Nette Framework (https://nette.org)
|
||||
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Nette\Iterators;
|
||||
|
||||
|
||||
/**
|
||||
* @deprecated use Nette\Utils\Iterables::map()
|
||||
*/
|
||||
class Mapper extends \IteratorIterator
|
||||
{
|
||||
/** @var callable */
|
||||
private $callback;
|
||||
|
||||
|
||||
public function __construct(\Traversable $iterator, callable $callback)
|
||||
{
|
||||
parent::__construct($iterator);
|
||||
$this->callback = $callback;
|
||||
}
|
||||
|
||||
|
||||
public function current(): mixed
|
||||
{
|
||||
return ($this->callback)(parent::current(), parent::key());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,140 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Nette Framework (https://nette.org)
|
||||
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Nette;
|
||||
|
||||
use Nette\Utils\ObjectHelpers;
|
||||
|
||||
|
||||
/**
|
||||
* Strict class for better experience.
|
||||
* - 'did you mean' hints
|
||||
* - access to undeclared members throws exceptions
|
||||
* - support for @property annotations
|
||||
* - support for calling event handlers stored in $onEvent via onEvent()
|
||||
*/
|
||||
trait SmartObject
|
||||
{
|
||||
/**
|
||||
* @return mixed
|
||||
* @throws MemberAccessException
|
||||
*/
|
||||
public function __call(string $name, array $args)
|
||||
{
|
||||
$class = static::class;
|
||||
|
||||
if (ObjectHelpers::hasProperty($class, $name) === 'event') { // calling event handlers
|
||||
$handlers = $this->$name ?? null;
|
||||
if (is_iterable($handlers)) {
|
||||
foreach ($handlers as $handler) {
|
||||
$handler(...$args);
|
||||
}
|
||||
} elseif ($handlers !== null) {
|
||||
throw new UnexpectedValueException("Property $class::$$name must be iterable or null, " . get_debug_type($handlers) . ' given.');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
ObjectHelpers::strictCall($class, $name);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @throws MemberAccessException
|
||||
*/
|
||||
public static function __callStatic(string $name, array $args)
|
||||
{
|
||||
ObjectHelpers::strictStaticCall(static::class, $name);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
* @throws MemberAccessException if the property is not defined.
|
||||
*/
|
||||
public function &__get(string $name)
|
||||
{
|
||||
$class = static::class;
|
||||
|
||||
if ($prop = ObjectHelpers::getMagicProperties($class)[$name] ?? null) { // property getter
|
||||
if (!($prop & 0b0001)) {
|
||||
throw new MemberAccessException("Cannot read a write-only property $class::\$$name.");
|
||||
}
|
||||
|
||||
$m = ($prop & 0b0010 ? 'get' : 'is') . ucfirst($name);
|
||||
if ($prop & 0b10000) {
|
||||
$trace = debug_backtrace(0, 1)[0]; // suppose this method is called from __call()
|
||||
$loc = isset($trace['file'], $trace['line'])
|
||||
? " in $trace[file] on line $trace[line]"
|
||||
: '';
|
||||
trigger_error("Property $class::\$$name is deprecated, use $class::$m() method$loc.", E_USER_DEPRECATED);
|
||||
}
|
||||
|
||||
if ($prop & 0b0100) { // return by reference
|
||||
return $this->$m();
|
||||
} else {
|
||||
$val = $this->$m();
|
||||
return $val;
|
||||
}
|
||||
} else {
|
||||
ObjectHelpers::strictGet($class, $name);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @throws MemberAccessException if the property is not defined or is read-only
|
||||
*/
|
||||
public function __set(string $name, mixed $value): void
|
||||
{
|
||||
$class = static::class;
|
||||
|
||||
if (ObjectHelpers::hasProperty($class, $name)) { // unsetted property
|
||||
$this->$name = $value;
|
||||
|
||||
} elseif ($prop = ObjectHelpers::getMagicProperties($class)[$name] ?? null) { // property setter
|
||||
if (!($prop & 0b1000)) {
|
||||
throw new MemberAccessException("Cannot write to a read-only property $class::\$$name.");
|
||||
}
|
||||
|
||||
$m = 'set' . ucfirst($name);
|
||||
if ($prop & 0b10000) {
|
||||
$trace = debug_backtrace(0, 1)[0]; // suppose this method is called from __call()
|
||||
$loc = isset($trace['file'], $trace['line'])
|
||||
? " in $trace[file] on line $trace[line]"
|
||||
: '';
|
||||
trigger_error("Property $class::\$$name is deprecated, use $class::$m() method$loc.", E_USER_DEPRECATED);
|
||||
}
|
||||
|
||||
$this->$m($value);
|
||||
|
||||
} else {
|
||||
ObjectHelpers::strictSet($class, $name);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @throws MemberAccessException
|
||||
*/
|
||||
public function __unset(string $name): void
|
||||
{
|
||||
$class = static::class;
|
||||
if (!ObjectHelpers::hasProperty($class, $name)) {
|
||||
throw new MemberAccessException("Cannot unset the property $class::\$$name.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function __isset(string $name): bool
|
||||
{
|
||||
return isset(ObjectHelpers::getMagicProperties(static::class)[$name]);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Nette Framework (https://nette.org)
|
||||
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Nette;
|
||||
|
||||
|
||||
/**
|
||||
* Static class.
|
||||
*/
|
||||
trait StaticClass
|
||||
{
|
||||
/**
|
||||
* Class is static and cannot be instantiated.
|
||||
*/
|
||||
private function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Call to undefined static method.
|
||||
* @throws MemberAccessException
|
||||
*/
|
||||
public static function __callStatic(string $name, array $args): mixed
|
||||
{
|
||||
Utils\ObjectHelpers::strictStaticCall(static::class, $name);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Nette Framework (https://nette.org)
|
||||
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Nette\Localization;
|
||||
|
||||
|
||||
/**
|
||||
* Translator adapter.
|
||||
*/
|
||||
interface Translator
|
||||
{
|
||||
/**
|
||||
* Translates the given string.
|
||||
*/
|
||||
function translate(string|\Stringable $message, mixed ...$parameters): string|\Stringable;
|
||||
}
|
||||
|
||||
|
||||
interface_exists(ITranslator::class);
|
||||
|
|
@ -0,0 +1,107 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Nette Framework (https://nette.org)
|
||||
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Nette\Utils;
|
||||
|
||||
use Nette;
|
||||
use function count, is_array, is_scalar, sprintf;
|
||||
|
||||
|
||||
/**
|
||||
* Provides objects to work as array.
|
||||
* @template T
|
||||
* @implements \IteratorAggregate<array-key, T>
|
||||
* @implements \ArrayAccess<array-key, T>
|
||||
*/
|
||||
class ArrayHash extends \stdClass implements \ArrayAccess, \Countable, \IteratorAggregate
|
||||
{
|
||||
/**
|
||||
* Transforms array to ArrayHash.
|
||||
* @param array<T> $array
|
||||
*/
|
||||
public static function from(array $array, bool $recursive = true): static
|
||||
{
|
||||
$obj = new static;
|
||||
foreach ($array as $key => $value) {
|
||||
$obj->$key = $recursive && is_array($value)
|
||||
? static::from($value)
|
||||
: $value;
|
||||
}
|
||||
|
||||
return $obj;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns an iterator over all items.
|
||||
* @return \Iterator<array-key, T>
|
||||
*/
|
||||
public function &getIterator(): \Iterator
|
||||
{
|
||||
foreach ((array) $this as $key => $foo) {
|
||||
yield $key => $this->$key;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns items count.
|
||||
*/
|
||||
public function count(): int
|
||||
{
|
||||
return count((array) $this);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Replaces or appends an item.
|
||||
* @param array-key $key
|
||||
* @param T $value
|
||||
*/
|
||||
public function offsetSet($key, $value): void
|
||||
{
|
||||
if (!is_scalar($key)) { // prevents null
|
||||
throw new Nette\InvalidArgumentException(sprintf('Key must be either a string or an integer, %s given.', get_debug_type($key)));
|
||||
}
|
||||
|
||||
$this->$key = $value;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns an item.
|
||||
* @param array-key $key
|
||||
* @return T
|
||||
*/
|
||||
#[\ReturnTypeWillChange]
|
||||
public function offsetGet($key)
|
||||
{
|
||||
return $this->$key;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Determines whether an item exists.
|
||||
* @param array-key $key
|
||||
*/
|
||||
public function offsetExists($key): bool
|
||||
{
|
||||
return isset($this->$key);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Removes the element from this list.
|
||||
* @param array-key $key
|
||||
*/
|
||||
public function offsetUnset($key): void
|
||||
{
|
||||
unset($this->$key);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,137 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Nette Framework (https://nette.org)
|
||||
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Nette\Utils;
|
||||
|
||||
use Nette;
|
||||
use function array_slice, array_splice, count, is_int;
|
||||
|
||||
|
||||
/**
|
||||
* Provides the base class for a generic list (items can be accessed by index).
|
||||
* @template T
|
||||
* @implements \IteratorAggregate<int, T>
|
||||
* @implements \ArrayAccess<int, T>
|
||||
*/
|
||||
class ArrayList implements \ArrayAccess, \Countable, \IteratorAggregate
|
||||
{
|
||||
use Nette\SmartObject;
|
||||
|
||||
private array $list = [];
|
||||
|
||||
|
||||
/**
|
||||
* Transforms array to ArrayList.
|
||||
* @param list<T> $array
|
||||
*/
|
||||
public static function from(array $array): static
|
||||
{
|
||||
if (!Arrays::isList($array)) {
|
||||
throw new Nette\InvalidArgumentException('Array is not valid list.');
|
||||
}
|
||||
|
||||
$obj = new static;
|
||||
$obj->list = $array;
|
||||
return $obj;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns an iterator over all items.
|
||||
* @return \Iterator<int, T>
|
||||
*/
|
||||
public function &getIterator(): \Iterator
|
||||
{
|
||||
foreach ($this->list as &$item) {
|
||||
yield $item;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns items count.
|
||||
*/
|
||||
public function count(): int
|
||||
{
|
||||
return count($this->list);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Replaces or appends an item.
|
||||
* @param int|null $index
|
||||
* @param T $value
|
||||
* @throws Nette\OutOfRangeException
|
||||
*/
|
||||
public function offsetSet($index, $value): void
|
||||
{
|
||||
if ($index === null) {
|
||||
$this->list[] = $value;
|
||||
|
||||
} elseif (!is_int($index) || $index < 0 || $index >= count($this->list)) {
|
||||
throw new Nette\OutOfRangeException('Offset invalid or out of range');
|
||||
|
||||
} else {
|
||||
$this->list[$index] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns an item.
|
||||
* @param int $index
|
||||
* @return T
|
||||
* @throws Nette\OutOfRangeException
|
||||
*/
|
||||
public function offsetGet($index): mixed
|
||||
{
|
||||
if (!is_int($index) || $index < 0 || $index >= count($this->list)) {
|
||||
throw new Nette\OutOfRangeException('Offset invalid or out of range');
|
||||
}
|
||||
|
||||
return $this->list[$index];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Determines whether an item exists.
|
||||
* @param int $index
|
||||
*/
|
||||
public function offsetExists($index): bool
|
||||
{
|
||||
return is_int($index) && $index >= 0 && $index < count($this->list);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Removes the element at the specified position in this list.
|
||||
* @param int $index
|
||||
* @throws Nette\OutOfRangeException
|
||||
*/
|
||||
public function offsetUnset($index): void
|
||||
{
|
||||
if (!is_int($index) || $index < 0 || $index >= count($this->list)) {
|
||||
throw new Nette\OutOfRangeException('Offset invalid or out of range');
|
||||
}
|
||||
|
||||
array_splice($this->list, $index, 1);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Prepends an item.
|
||||
* @param T $value
|
||||
*/
|
||||
public function prepend(mixed $value): void
|
||||
{
|
||||
$first = array_slice($this->list, 0, 1);
|
||||
$this->offsetSet(0, $value);
|
||||
array_splice($this->list, 1, 0, $first);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,555 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Nette Framework (https://nette.org)
|
||||
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Nette\Utils;
|
||||
|
||||
use JetBrains\PhpStorm\Language;
|
||||
use Nette;
|
||||
use function array_combine, array_intersect_key, array_is_list, array_key_exists, array_key_first, array_key_last, array_keys, array_reverse, array_search, array_slice, array_walk_recursive, count, func_num_args, in_array, is_array, is_int, is_object, key, preg_split, range;
|
||||
use const PHP_VERSION_ID, PREG_GREP_INVERT, PREG_SPLIT_DELIM_CAPTURE, PREG_SPLIT_NO_EMPTY;
|
||||
|
||||
|
||||
/**
|
||||
* Array tools library.
|
||||
*/
|
||||
class Arrays
|
||||
{
|
||||
use Nette\StaticClass;
|
||||
|
||||
/**
|
||||
* Returns item from array. If it does not exist, it throws an exception, unless a default value is set.
|
||||
* @template T
|
||||
* @param array<T> $array
|
||||
* @param array-key|array-key[] $key
|
||||
* @param ?T $default
|
||||
* @return ?T
|
||||
* @throws Nette\InvalidArgumentException if item does not exist and default value is not provided
|
||||
*/
|
||||
public static function get(array $array, string|int|array $key, mixed $default = null): mixed
|
||||
{
|
||||
foreach (is_array($key) ? $key : [$key] as $k) {
|
||||
if (is_array($array) && array_key_exists($k, $array)) {
|
||||
$array = $array[$k];
|
||||
} else {
|
||||
if (func_num_args() < 3) {
|
||||
throw new Nette\InvalidArgumentException("Missing item '$k'.");
|
||||
}
|
||||
|
||||
return $default;
|
||||
}
|
||||
}
|
||||
|
||||
return $array;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns reference to array item. If the index does not exist, new one is created with value null.
|
||||
* @template T
|
||||
* @param array<T> $array
|
||||
* @param array-key|array-key[] $key
|
||||
* @return ?T
|
||||
* @throws Nette\InvalidArgumentException if traversed item is not an array
|
||||
*/
|
||||
public static function &getRef(array &$array, string|int|array $key): mixed
|
||||
{
|
||||
foreach (is_array($key) ? $key : [$key] as $k) {
|
||||
if (is_array($array) || $array === null) {
|
||||
$array = &$array[$k];
|
||||
} else {
|
||||
throw new Nette\InvalidArgumentException('Traversed item is not an array.');
|
||||
}
|
||||
}
|
||||
|
||||
return $array;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Recursively merges two fields. It is useful, for example, for merging tree structures. It behaves as
|
||||
* the + operator for array, ie. it adds a key/value pair from the second array to the first one and retains
|
||||
* the value from the first array in the case of a key collision.
|
||||
* @template T1
|
||||
* @template T2
|
||||
* @param array<T1> $array1
|
||||
* @param array<T2> $array2
|
||||
* @return array<T1|T2>
|
||||
*/
|
||||
public static function mergeTree(array $array1, array $array2): array
|
||||
{
|
||||
$res = $array1 + $array2;
|
||||
foreach (array_intersect_key($array1, $array2) as $k => $v) {
|
||||
if (is_array($v) && is_array($array2[$k])) {
|
||||
$res[$k] = self::mergeTree($v, $array2[$k]);
|
||||
}
|
||||
}
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns zero-indexed position of given array key. Returns null if key is not found.
|
||||
*/
|
||||
public static function getKeyOffset(array $array, string|int $key): ?int
|
||||
{
|
||||
return Helpers::falseToNull(array_search(self::toKey($key), array_keys($array), strict: true));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @deprecated use getKeyOffset()
|
||||
*/
|
||||
public static function searchKey(array $array, $key): ?int
|
||||
{
|
||||
return self::getKeyOffset($array, $key);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Tests an array for the presence of value.
|
||||
*/
|
||||
public static function contains(array $array, mixed $value): bool
|
||||
{
|
||||
return in_array($value, $array, true);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the first item (matching the specified predicate if given). If there is no such item, it returns result of invoking $else or null.
|
||||
* @template K of int|string
|
||||
* @template V
|
||||
* @param array<K, V> $array
|
||||
* @param ?callable(V, K, array<K, V>): bool $predicate
|
||||
* @return ?V
|
||||
*/
|
||||
public static function first(array $array, ?callable $predicate = null, ?callable $else = null): mixed
|
||||
{
|
||||
$key = self::firstKey($array, $predicate);
|
||||
return $key === null
|
||||
? ($else ? $else() : null)
|
||||
: $array[$key];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the last item (matching the specified predicate if given). If there is no such item, it returns result of invoking $else or null.
|
||||
* @template K of int|string
|
||||
* @template V
|
||||
* @param array<K, V> $array
|
||||
* @param ?callable(V, K, array<K, V>): bool $predicate
|
||||
* @return ?V
|
||||
*/
|
||||
public static function last(array $array, ?callable $predicate = null, ?callable $else = null): mixed
|
||||
{
|
||||
$key = self::lastKey($array, $predicate);
|
||||
return $key === null
|
||||
? ($else ? $else() : null)
|
||||
: $array[$key];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the key of first item (matching the specified predicate if given) or null if there is no such item.
|
||||
* @template K of int|string
|
||||
* @template V
|
||||
* @param array<K, V> $array
|
||||
* @param ?callable(V, K, array<K, V>): bool $predicate
|
||||
* @return ?K
|
||||
*/
|
||||
public static function firstKey(array $array, ?callable $predicate = null): int|string|null
|
||||
{
|
||||
if (!$predicate) {
|
||||
return array_key_first($array);
|
||||
}
|
||||
foreach ($array as $k => $v) {
|
||||
if ($predicate($v, $k, $array)) {
|
||||
return $k;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the key of last item (matching the specified predicate if given) or null if there is no such item.
|
||||
* @template K of int|string
|
||||
* @template V
|
||||
* @param array<K, V> $array
|
||||
* @param ?callable(V, K, array<K, V>): bool $predicate
|
||||
* @return ?K
|
||||
*/
|
||||
public static function lastKey(array $array, ?callable $predicate = null): int|string|null
|
||||
{
|
||||
return $predicate
|
||||
? self::firstKey(array_reverse($array, preserve_keys: true), $predicate)
|
||||
: array_key_last($array);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Inserts the contents of the $inserted array into the $array immediately after the $key.
|
||||
* If $key is null (or does not exist), it is inserted at the beginning.
|
||||
*/
|
||||
public static function insertBefore(array &$array, string|int|null $key, array $inserted): void
|
||||
{
|
||||
$offset = $key === null ? 0 : (int) self::getKeyOffset($array, $key);
|
||||
$array = array_slice($array, 0, $offset, preserve_keys: true)
|
||||
+ $inserted
|
||||
+ array_slice($array, $offset, count($array), preserve_keys: true);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Inserts the contents of the $inserted array into the $array before the $key.
|
||||
* If $key is null (or does not exist), it is inserted at the end.
|
||||
*/
|
||||
public static function insertAfter(array &$array, string|int|null $key, array $inserted): void
|
||||
{
|
||||
if ($key === null || ($offset = self::getKeyOffset($array, $key)) === null) {
|
||||
$offset = count($array) - 1;
|
||||
}
|
||||
|
||||
$array = array_slice($array, 0, $offset + 1, preserve_keys: true)
|
||||
+ $inserted
|
||||
+ array_slice($array, $offset + 1, count($array), preserve_keys: true);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Renames key in array.
|
||||
*/
|
||||
public static function renameKey(array &$array, string|int $oldKey, string|int $newKey): bool
|
||||
{
|
||||
$offset = self::getKeyOffset($array, $oldKey);
|
||||
if ($offset === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$val = &$array[$oldKey];
|
||||
$keys = array_keys($array);
|
||||
$keys[$offset] = $newKey;
|
||||
$array = array_combine($keys, $array);
|
||||
$array[$newKey] = &$val;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns only those array items, which matches a regular expression $pattern.
|
||||
* @param string[] $array
|
||||
* @return string[]
|
||||
*/
|
||||
public static function grep(
|
||||
array $array,
|
||||
#[Language('RegExp')]
|
||||
string $pattern,
|
||||
bool|int $invert = false,
|
||||
): array
|
||||
{
|
||||
$flags = $invert ? PREG_GREP_INVERT : 0;
|
||||
return Strings::pcre('preg_grep', [$pattern, $array, $flags]);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Transforms multidimensional array to flat array.
|
||||
*/
|
||||
public static function flatten(array $array, bool $preserveKeys = false): array
|
||||
{
|
||||
$res = [];
|
||||
$cb = $preserveKeys
|
||||
? function ($v, $k) use (&$res): void { $res[$k] = $v; }
|
||||
: function ($v) use (&$res): void { $res[] = $v; };
|
||||
array_walk_recursive($array, $cb);
|
||||
return $res;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Checks if the array is indexed in ascending order of numeric keys from zero, a.k.a list.
|
||||
* @return ($value is list ? true : false)
|
||||
*/
|
||||
public static function isList(mixed $value): bool
|
||||
{
|
||||
return is_array($value) && (
|
||||
PHP_VERSION_ID < 80100
|
||||
? !$value || array_keys($value) === range(0, count($value) - 1)
|
||||
: array_is_list($value)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Reformats table to associative tree. Path looks like 'field|field[]field->field=field'.
|
||||
* @param string|string[] $path
|
||||
*/
|
||||
public static function associate(array $array, $path): array|\stdClass
|
||||
{
|
||||
$parts = is_array($path)
|
||||
? $path
|
||||
: preg_split('#(\[\]|->|=|\|)#', $path, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
|
||||
|
||||
if (!$parts || $parts === ['->'] || $parts[0] === '=' || $parts[0] === '|') {
|
||||
throw new Nette\InvalidArgumentException("Invalid path '$path'.");
|
||||
}
|
||||
|
||||
$res = $parts[0] === '->' ? new \stdClass : [];
|
||||
|
||||
foreach ($array as $rowOrig) {
|
||||
$row = (array) $rowOrig;
|
||||
$x = &$res;
|
||||
|
||||
for ($i = 0; $i < count($parts); $i++) {
|
||||
$part = $parts[$i];
|
||||
if ($part === '[]') {
|
||||
$x = &$x[];
|
||||
|
||||
} elseif ($part === '=') {
|
||||
if (isset($parts[++$i])) {
|
||||
$x = $row[$parts[$i]];
|
||||
$row = null;
|
||||
}
|
||||
} elseif ($part === '->') {
|
||||
if (isset($parts[++$i])) {
|
||||
if ($x === null) {
|
||||
$x = new \stdClass;
|
||||
}
|
||||
|
||||
$x = &$x->{$row[$parts[$i]]};
|
||||
} else {
|
||||
$row = is_object($rowOrig) ? $rowOrig : (object) $row;
|
||||
}
|
||||
} elseif ($part !== '|') {
|
||||
$x = &$x[(string) $row[$part]];
|
||||
}
|
||||
}
|
||||
|
||||
if ($x === null) {
|
||||
$x = $row;
|
||||
}
|
||||
}
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Normalizes array to associative array. Replace numeric keys with their values, the new value will be $filling.
|
||||
*/
|
||||
public static function normalize(array $array, mixed $filling = null): array
|
||||
{
|
||||
$res = [];
|
||||
foreach ($array as $k => $v) {
|
||||
$res[is_int($k) ? $v : $k] = is_int($k) ? $filling : $v;
|
||||
}
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns and removes the value of an item from an array. If it does not exist, it throws an exception,
|
||||
* or returns $default, if provided.
|
||||
* @template T
|
||||
* @param array<T> $array
|
||||
* @param ?T $default
|
||||
* @return ?T
|
||||
* @throws Nette\InvalidArgumentException if item does not exist and default value is not provided
|
||||
*/
|
||||
public static function pick(array &$array, string|int $key, mixed $default = null): mixed
|
||||
{
|
||||
if (array_key_exists($key, $array)) {
|
||||
$value = $array[$key];
|
||||
unset($array[$key]);
|
||||
return $value;
|
||||
|
||||
} elseif (func_num_args() < 3) {
|
||||
throw new Nette\InvalidArgumentException("Missing item '$key'.");
|
||||
|
||||
} else {
|
||||
return $default;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Tests whether at least one element in the array passes the test implemented by the provided function.
|
||||
* @template K of int|string
|
||||
* @template V
|
||||
* @param array<K, V> $array
|
||||
* @param callable(V, K, array<K, V>): bool $predicate
|
||||
*/
|
||||
public static function some(iterable $array, callable $predicate): bool
|
||||
{
|
||||
foreach ($array as $k => $v) {
|
||||
if ($predicate($v, $k, $array)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Tests whether all elements in the array pass the test implemented by the provided function.
|
||||
* @template K of int|string
|
||||
* @template V
|
||||
* @param array<K, V> $array
|
||||
* @param callable(V, K, array<K, V>): bool $predicate
|
||||
*/
|
||||
public static function every(iterable $array, callable $predicate): bool
|
||||
{
|
||||
foreach ($array as $k => $v) {
|
||||
if (!$predicate($v, $k, $array)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns a new array containing all key-value pairs matching the given $predicate.
|
||||
* @template K of int|string
|
||||
* @template V
|
||||
* @param array<K, V> $array
|
||||
* @param callable(V, K, array<K, V>): bool $predicate
|
||||
* @return array<K, V>
|
||||
*/
|
||||
public static function filter(array $array, callable $predicate): array
|
||||
{
|
||||
$res = [];
|
||||
foreach ($array as $k => $v) {
|
||||
if ($predicate($v, $k, $array)) {
|
||||
$res[$k] = $v;
|
||||
}
|
||||
}
|
||||
return $res;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns an array containing the original keys and results of applying the given transform function to each element.
|
||||
* @template K of int|string
|
||||
* @template V
|
||||
* @template R
|
||||
* @param array<K, V> $array
|
||||
* @param callable(V, K, array<K, V>): R $transformer
|
||||
* @return array<K, R>
|
||||
*/
|
||||
public static function map(iterable $array, callable $transformer): array
|
||||
{
|
||||
$res = [];
|
||||
foreach ($array as $k => $v) {
|
||||
$res[$k] = $transformer($v, $k, $array);
|
||||
}
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns an array containing new keys and values generated by applying the given transform function to each element.
|
||||
* If the function returns null, the element is skipped.
|
||||
* @template K of int|string
|
||||
* @template V
|
||||
* @template ResK of int|string
|
||||
* @template ResV
|
||||
* @param array<K, V> $array
|
||||
* @param callable(V, K, array<K, V>): ?array{ResK, ResV} $transformer
|
||||
* @return array<ResK, ResV>
|
||||
*/
|
||||
public static function mapWithKeys(array $array, callable $transformer): array
|
||||
{
|
||||
$res = [];
|
||||
foreach ($array as $k => $v) {
|
||||
$pair = $transformer($v, $k, $array);
|
||||
if ($pair) {
|
||||
$res[$pair[0]] = $pair[1];
|
||||
}
|
||||
}
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Invokes all callbacks and returns array of results.
|
||||
* @param callable[] $callbacks
|
||||
*/
|
||||
public static function invoke(iterable $callbacks, ...$args): array
|
||||
{
|
||||
$res = [];
|
||||
foreach ($callbacks as $k => $cb) {
|
||||
$res[$k] = $cb(...$args);
|
||||
}
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Invokes method on every object in an array and returns array of results.
|
||||
* @param object[] $objects
|
||||
*/
|
||||
public static function invokeMethod(iterable $objects, string $method, ...$args): array
|
||||
{
|
||||
$res = [];
|
||||
foreach ($objects as $k => $obj) {
|
||||
$res[$k] = $obj->$method(...$args);
|
||||
}
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Copies the elements of the $array array to the $object object and then returns it.
|
||||
* @template T of object
|
||||
* @param T $object
|
||||
* @return T
|
||||
*/
|
||||
public static function toObject(iterable $array, object $object): object
|
||||
{
|
||||
foreach ($array as $k => $v) {
|
||||
$object->$k = $v;
|
||||
}
|
||||
|
||||
return $object;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Converts value to array key.
|
||||
*/
|
||||
public static function toKey(mixed $value): int|string
|
||||
{
|
||||
return key([$value => null]);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns copy of the $array where every item is converted to string
|
||||
* and prefixed by $prefix and suffixed by $suffix.
|
||||
* @param string[] $array
|
||||
* @return string[]
|
||||
*/
|
||||
public static function wrap(array $array, string $prefix = '', string $suffix = ''): array
|
||||
{
|
||||
$res = [];
|
||||
foreach ($array as $k => $v) {
|
||||
$res[$k] = $prefix . $v . $suffix;
|
||||
}
|
||||
|
||||
return $res;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,137 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Nette Framework (https://nette.org)
|
||||
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Nette\Utils;
|
||||
|
||||
use Nette;
|
||||
use function explode, func_get_args, ini_get, is_array, is_callable, is_object, is_string, preg_replace, restore_error_handler, set_error_handler, sprintf, str_contains, str_ends_with;
|
||||
|
||||
|
||||
/**
|
||||
* PHP callable tools.
|
||||
*/
|
||||
final class Callback
|
||||
{
|
||||
use Nette\StaticClass;
|
||||
|
||||
/**
|
||||
* Invokes internal PHP function with own error handler.
|
||||
*/
|
||||
public static function invokeSafe(string $function, array $args, callable $onError): mixed
|
||||
{
|
||||
$prev = set_error_handler(function ($severity, $message, $file) use ($onError, &$prev, $function): ?bool {
|
||||
if ($file === __FILE__) {
|
||||
$msg = ini_get('html_errors')
|
||||
? Html::htmlToText($message)
|
||||
: $message;
|
||||
$msg = preg_replace("#^$function\\(.*?\\): #", '', $msg);
|
||||
if ($onError($msg, $severity) !== false) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return $prev ? $prev(...func_get_args()) : false;
|
||||
});
|
||||
|
||||
try {
|
||||
return $function(...$args);
|
||||
} finally {
|
||||
restore_error_handler();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Checks that $callable is valid PHP callback. Otherwise throws exception. If the $syntax is set to true, only verifies
|
||||
* that $callable has a valid structure to be used as a callback, but does not verify if the class or method actually exists.
|
||||
* @return callable
|
||||
* @throws Nette\InvalidArgumentException
|
||||
*/
|
||||
public static function check(mixed $callable, bool $syntax = false)
|
||||
{
|
||||
if (!is_callable($callable, $syntax)) {
|
||||
throw new Nette\InvalidArgumentException(
|
||||
$syntax
|
||||
? 'Given value is not a callable type.'
|
||||
: sprintf("Callback '%s' is not callable.", self::toString($callable)),
|
||||
);
|
||||
}
|
||||
|
||||
return $callable;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Converts PHP callback to textual form. Class or method may not exists.
|
||||
*/
|
||||
public static function toString(mixed $callable): string
|
||||
{
|
||||
if ($callable instanceof \Closure) {
|
||||
$inner = self::unwrap($callable);
|
||||
return '{closure' . ($inner instanceof \Closure ? '}' : ' ' . self::toString($inner) . '}');
|
||||
} else {
|
||||
is_callable(is_object($callable) ? [$callable, '__invoke'] : $callable, true, $textual);
|
||||
return $textual;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns reflection for method or function used in PHP callback.
|
||||
* @param callable $callable type check is escalated to ReflectionException
|
||||
* @throws \ReflectionException if callback is not valid
|
||||
*/
|
||||
public static function toReflection($callable): \ReflectionMethod|\ReflectionFunction
|
||||
{
|
||||
if ($callable instanceof \Closure) {
|
||||
$callable = self::unwrap($callable);
|
||||
}
|
||||
|
||||
if (is_string($callable) && str_contains($callable, '::')) {
|
||||
return new ReflectionMethod(...explode('::', $callable, 2));
|
||||
} elseif (is_array($callable)) {
|
||||
return new ReflectionMethod($callable[0], $callable[1]);
|
||||
} elseif (is_object($callable) && !$callable instanceof \Closure) {
|
||||
return new ReflectionMethod($callable, '__invoke');
|
||||
} else {
|
||||
return new \ReflectionFunction($callable);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Checks whether PHP callback is function or static method.
|
||||
*/
|
||||
public static function isStatic(callable $callable): bool
|
||||
{
|
||||
return is_string(is_array($callable) ? $callable[0] : $callable);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Unwraps closure created by Closure::fromCallable().
|
||||
*/
|
||||
public static function unwrap(\Closure $closure): callable|array
|
||||
{
|
||||
$r = new \ReflectionFunction($closure);
|
||||
$class = $r->getClosureScopeClass()?->name;
|
||||
if (str_ends_with($r->name, '}')) {
|
||||
return $closure;
|
||||
|
||||
} elseif (($obj = $r->getClosureThis()) && $obj::class === $class) {
|
||||
return [$obj, $r->name];
|
||||
|
||||
} elseif ($class) {
|
||||
return [$class, $r->name];
|
||||
|
||||
} else {
|
||||
return $r->name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,219 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Nette Framework (https://nette.org)
|
||||
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Nette\Utils;
|
||||
|
||||
use Nette;
|
||||
use function array_merge, checkdate, implode, is_numeric, is_string, preg_replace_callback, sprintf, time, trim;
|
||||
|
||||
|
||||
/**
|
||||
* DateTime.
|
||||
*/
|
||||
class DateTime extends \DateTime implements \JsonSerializable
|
||||
{
|
||||
use Nette\SmartObject;
|
||||
|
||||
/** minute in seconds */
|
||||
public const MINUTE = 60;
|
||||
|
||||
/** hour in seconds */
|
||||
public const HOUR = 60 * self::MINUTE;
|
||||
|
||||
/** day in seconds */
|
||||
public const DAY = 24 * self::HOUR;
|
||||
|
||||
/** week in seconds */
|
||||
public const WEEK = 7 * self::DAY;
|
||||
|
||||
/** average month in seconds */
|
||||
public const MONTH = 2_629_800;
|
||||
|
||||
/** average year in seconds */
|
||||
public const YEAR = 31_557_600;
|
||||
|
||||
|
||||
/**
|
||||
* Creates a DateTime object from a string, UNIX timestamp, or other DateTimeInterface object.
|
||||
* @throws \Exception if the date and time are not valid.
|
||||
*/
|
||||
public static function from(string|int|\DateTimeInterface|null $time): static
|
||||
{
|
||||
if ($time instanceof \DateTimeInterface) {
|
||||
return static::createFromInterface($time);
|
||||
|
||||
} elseif (is_numeric($time)) {
|
||||
if ($time <= self::YEAR) {
|
||||
$time += time();
|
||||
}
|
||||
|
||||
return (new static)->setTimestamp((int) $time);
|
||||
|
||||
} else { // textual or null
|
||||
return new static((string) $time);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Creates DateTime object.
|
||||
* @throws Nette\InvalidArgumentException if the date and time are not valid.
|
||||
*/
|
||||
public static function fromParts(
|
||||
int $year,
|
||||
int $month,
|
||||
int $day,
|
||||
int $hour = 0,
|
||||
int $minute = 0,
|
||||
float $second = 0.0,
|
||||
): static
|
||||
{
|
||||
$s = sprintf('%04d-%02d-%02d %02d:%02d:%02.5F', $year, $month, $day, $hour, $minute, $second);
|
||||
if (
|
||||
!checkdate($month, $day, $year)
|
||||
|| $hour < 0 || $hour > 23
|
||||
|| $minute < 0 || $minute > 59
|
||||
|| $second < 0 || $second >= 60
|
||||
) {
|
||||
throw new Nette\InvalidArgumentException("Invalid date '$s'");
|
||||
}
|
||||
|
||||
return new static($s);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns a new DateTime object formatted according to the specified format.
|
||||
*/
|
||||
public static function createFromFormat(
|
||||
string $format,
|
||||
string $datetime,
|
||||
string|\DateTimeZone|null $timezone = null,
|
||||
): static|false
|
||||
{
|
||||
if (is_string($timezone)) {
|
||||
$timezone = new \DateTimeZone($timezone);
|
||||
}
|
||||
|
||||
$date = parent::createFromFormat($format, $datetime, $timezone);
|
||||
return $date ? static::from($date) : false;
|
||||
}
|
||||
|
||||
|
||||
public function __construct(string $datetime = 'now', ?\DateTimeZone $timezone = null)
|
||||
{
|
||||
$this->apply($datetime, $timezone, true);
|
||||
}
|
||||
|
||||
|
||||
public function modify(string $modifier): static
|
||||
{
|
||||
$this->apply($modifier);
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
public function setDate(int $year, int $month, int $day): static
|
||||
{
|
||||
if (!checkdate($month, $day, $year)) {
|
||||
trigger_error(sprintf(self::class . ': The date %04d-%02d-%02d is not valid.', $year, $month, $day), E_USER_WARNING);
|
||||
}
|
||||
return parent::setDate($year, $month, $day);
|
||||
}
|
||||
|
||||
|
||||
public function setTime(int $hour, int $minute, int $second = 0, int $microsecond = 0): static
|
||||
{
|
||||
if (
|
||||
$hour < 0 || $hour > 23
|
||||
|| $minute < 0 || $minute > 59
|
||||
|| $second < 0 || $second >= 60
|
||||
|| $microsecond < 0 || $microsecond >= 1_000_000
|
||||
) {
|
||||
trigger_error(sprintf(self::class . ': The time %02d:%02d:%08.5F is not valid.', $hour, $minute, $second + $microsecond / 1_000_000), E_USER_WARNING);
|
||||
}
|
||||
return parent::setTime($hour, $minute, $second, $microsecond);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Converts a relative time string (e.g. '10 minut') to seconds.
|
||||
*/
|
||||
public static function relativeToSeconds(string $relativeTime): int
|
||||
{
|
||||
return (new self('@0 ' . $relativeTime))
|
||||
->getTimestamp();
|
||||
}
|
||||
|
||||
|
||||
private function apply(string $datetime, $timezone = null, bool $ctr = false): void
|
||||
{
|
||||
$relPart = '';
|
||||
$absPart = preg_replace_callback(
|
||||
'/[+-]?\s*\d+\s+((microsecond|millisecond|[mµu]sec)s?|[mµ]s|sec(ond)?s?|min(ute)?s?|hours?)(\s+ago)?\b/iu',
|
||||
function ($m) use (&$relPart) {
|
||||
$relPart .= $m[0] . ' ';
|
||||
return '';
|
||||
},
|
||||
$datetime,
|
||||
);
|
||||
|
||||
if ($ctr) {
|
||||
parent::__construct($absPart, $timezone);
|
||||
$this->handleErrors($datetime);
|
||||
} elseif (trim($absPart)) {
|
||||
parent::modify($absPart) && $this->handleErrors($datetime);
|
||||
}
|
||||
|
||||
if ($relPart) {
|
||||
$timezone ??= $this->getTimezone();
|
||||
$this->setTimezone(new \DateTimeZone('UTC'));
|
||||
parent::modify($relPart) && $this->handleErrors($datetime);
|
||||
$this->setTimezone($timezone);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns JSON representation in ISO 8601 (used by JavaScript).
|
||||
*/
|
||||
public function jsonSerialize(): string
|
||||
{
|
||||
return $this->format('c');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the date and time in the format 'Y-m-d H:i:s'.
|
||||
*/
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->format('Y-m-d H:i:s');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* You'd better use: (clone $dt)->modify(...)
|
||||
*/
|
||||
public function modifyClone(string $modify = ''): static
|
||||
{
|
||||
$dolly = clone $this;
|
||||
return $modify ? $dolly->modify($modify) : $dolly;
|
||||
}
|
||||
|
||||
|
||||
private function handleErrors(string $value): void
|
||||
{
|
||||
$errors = self::getLastErrors();
|
||||
$errors = array_merge($errors['errors'] ?? [], $errors['warnings'] ?? []);
|
||||
if ($errors) {
|
||||
trigger_error(self::class . ': ' . implode(', ', $errors) . " '$value'", E_USER_WARNING);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Nette Framework (https://nette.org)
|
||||
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Nette\Utils;
|
||||
|
||||
use Nette;
|
||||
use const DIRECTORY_SEPARATOR;
|
||||
|
||||
|
||||
/**
|
||||
* Represents the file or directory returned by the Finder.
|
||||
* @internal do not create instances directly
|
||||
*/
|
||||
final class FileInfo extends \SplFileInfo
|
||||
{
|
||||
private string $relativePath;
|
||||
|
||||
|
||||
public function __construct(string $file, string $relativePath = '')
|
||||
{
|
||||
parent::__construct($file);
|
||||
$this->setInfoClass(static::class);
|
||||
$this->relativePath = $relativePath;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the relative directory path.
|
||||
*/
|
||||
public function getRelativePath(): string
|
||||
{
|
||||
return $this->relativePath;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the relative path including file name.
|
||||
*/
|
||||
public function getRelativePathname(): string
|
||||
{
|
||||
return ($this->relativePath === '' ? '' : $this->relativePath . DIRECTORY_SEPARATOR)
|
||||
. $this->getBasename();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the contents of the file.
|
||||
* @throws Nette\IOException
|
||||
*/
|
||||
public function read(): string
|
||||
{
|
||||
return FileSystem::read($this->getPathname());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Writes the contents to the file.
|
||||
* @throws Nette\IOException
|
||||
*/
|
||||
public function write(string $content): void
|
||||
{
|
||||
FileSystem::write($this->getPathname(), $content);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,341 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Nette Framework (https://nette.org)
|
||||
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Nette\Utils;
|
||||
|
||||
use Nette;
|
||||
use function array_pop, chmod, decoct, dirname, end, fclose, file_exists, file_get_contents, file_put_contents, fopen, implode, is_dir, is_file, is_link, mkdir, preg_match, preg_split, realpath, rename, rmdir, rtrim, sprintf, str_replace, stream_copy_to_stream, stream_is_local, strtr;
|
||||
use const DIRECTORY_SEPARATOR;
|
||||
|
||||
|
||||
/**
|
||||
* File system tool.
|
||||
*/
|
||||
final class FileSystem
|
||||
{
|
||||
/**
|
||||
* Creates a directory if it does not exist, including parent directories.
|
||||
* @throws Nette\IOException on error occurred
|
||||
*/
|
||||
public static function createDir(string $dir, int $mode = 0777): void
|
||||
{
|
||||
if (!is_dir($dir) && !@mkdir($dir, $mode, recursive: true) && !is_dir($dir)) { // @ - dir may already exist
|
||||
throw new Nette\IOException(sprintf(
|
||||
"Unable to create directory '%s' with mode %s. %s",
|
||||
self::normalizePath($dir),
|
||||
decoct($mode),
|
||||
Helpers::getLastError(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Copies a file or an entire directory. Overwrites existing files and directories by default.
|
||||
* @throws Nette\IOException on error occurred
|
||||
* @throws Nette\InvalidStateException if $overwrite is set to false and destination already exists
|
||||
*/
|
||||
public static function copy(string $origin, string $target, bool $overwrite = true): void
|
||||
{
|
||||
if (stream_is_local($origin) && !file_exists($origin)) {
|
||||
throw new Nette\IOException(sprintf("File or directory '%s' not found.", self::normalizePath($origin)));
|
||||
|
||||
} elseif (!$overwrite && file_exists($target)) {
|
||||
throw new Nette\InvalidStateException(sprintf("File or directory '%s' already exists.", self::normalizePath($target)));
|
||||
|
||||
} elseif (is_dir($origin)) {
|
||||
static::createDir($target);
|
||||
foreach (new \FilesystemIterator($target) as $item) {
|
||||
static::delete($item->getPathname());
|
||||
}
|
||||
|
||||
foreach ($iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($origin, \RecursiveDirectoryIterator::SKIP_DOTS), \RecursiveIteratorIterator::SELF_FIRST) as $item) {
|
||||
if ($item->isDir()) {
|
||||
static::createDir($target . '/' . $iterator->getSubPathName());
|
||||
} else {
|
||||
static::copy($item->getPathname(), $target . '/' . $iterator->getSubPathName());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
static::createDir(dirname($target));
|
||||
if (@stream_copy_to_stream(static::open($origin, 'rb'), static::open($target, 'wb')) === false) { // @ is escalated to exception
|
||||
throw new Nette\IOException(sprintf(
|
||||
"Unable to copy file '%s' to '%s'. %s",
|
||||
self::normalizePath($origin),
|
||||
self::normalizePath($target),
|
||||
Helpers::getLastError(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Opens file and returns resource.
|
||||
* @return resource
|
||||
* @throws Nette\IOException on error occurred
|
||||
*/
|
||||
public static function open(string $path, string $mode)
|
||||
{
|
||||
$f = @fopen($path, $mode); // @ is escalated to exception
|
||||
if (!$f) {
|
||||
throw new Nette\IOException(sprintf(
|
||||
"Unable to open file '%s'. %s",
|
||||
self::normalizePath($path),
|
||||
Helpers::getLastError(),
|
||||
));
|
||||
}
|
||||
return $f;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Deletes a file or an entire directory if exists. If the directory is not empty, it deletes its contents first.
|
||||
* @throws Nette\IOException on error occurred
|
||||
*/
|
||||
public static function delete(string $path): void
|
||||
{
|
||||
if (is_file($path) || is_link($path)) {
|
||||
$func = DIRECTORY_SEPARATOR === '\\' && is_dir($path) ? 'rmdir' : 'unlink';
|
||||
if (!@$func($path)) { // @ is escalated to exception
|
||||
throw new Nette\IOException(sprintf(
|
||||
"Unable to delete '%s'. %s",
|
||||
self::normalizePath($path),
|
||||
Helpers::getLastError(),
|
||||
));
|
||||
}
|
||||
} elseif (is_dir($path)) {
|
||||
foreach (new \FilesystemIterator($path) as $item) {
|
||||
static::delete($item->getPathname());
|
||||
}
|
||||
|
||||
if (!@rmdir($path)) { // @ is escalated to exception
|
||||
throw new Nette\IOException(sprintf(
|
||||
"Unable to delete directory '%s'. %s",
|
||||
self::normalizePath($path),
|
||||
Helpers::getLastError(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Renames or moves a file or a directory. Overwrites existing files and directories by default.
|
||||
* @throws Nette\IOException on error occurred
|
||||
* @throws Nette\InvalidStateException if $overwrite is set to false and destination already exists
|
||||
*/
|
||||
public static function rename(string $origin, string $target, bool $overwrite = true): void
|
||||
{
|
||||
if (!$overwrite && file_exists($target)) {
|
||||
throw new Nette\InvalidStateException(sprintf("File or directory '%s' already exists.", self::normalizePath($target)));
|
||||
|
||||
} elseif (!file_exists($origin)) {
|
||||
throw new Nette\IOException(sprintf("File or directory '%s' not found.", self::normalizePath($origin)));
|
||||
|
||||
} else {
|
||||
static::createDir(dirname($target));
|
||||
if (realpath($origin) !== realpath($target)) {
|
||||
static::delete($target);
|
||||
}
|
||||
|
||||
if (!@rename($origin, $target)) { // @ is escalated to exception
|
||||
throw new Nette\IOException(sprintf(
|
||||
"Unable to rename file or directory '%s' to '%s'. %s",
|
||||
self::normalizePath($origin),
|
||||
self::normalizePath($target),
|
||||
Helpers::getLastError(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Reads the content of a file.
|
||||
* @throws Nette\IOException on error occurred
|
||||
*/
|
||||
public static function read(string $file): string
|
||||
{
|
||||
$content = @file_get_contents($file); // @ is escalated to exception
|
||||
if ($content === false) {
|
||||
throw new Nette\IOException(sprintf(
|
||||
"Unable to read file '%s'. %s",
|
||||
self::normalizePath($file),
|
||||
Helpers::getLastError(),
|
||||
));
|
||||
}
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Reads the file content line by line. Because it reads continuously as we iterate over the lines,
|
||||
* it is possible to read files larger than the available memory.
|
||||
* @return \Generator<int, string>
|
||||
* @throws Nette\IOException on error occurred
|
||||
*/
|
||||
public static function readLines(string $file, bool $stripNewLines = true): \Generator
|
||||
{
|
||||
return (function ($f) use ($file, $stripNewLines) {
|
||||
$counter = 0;
|
||||
do {
|
||||
$line = Callback::invokeSafe('fgets', [$f], fn($error) => throw new Nette\IOException(sprintf(
|
||||
"Unable to read file '%s'. %s",
|
||||
self::normalizePath($file),
|
||||
$error,
|
||||
)));
|
||||
if ($line === false) {
|
||||
fclose($f);
|
||||
break;
|
||||
}
|
||||
if ($stripNewLines) {
|
||||
$line = rtrim($line, "\r\n");
|
||||
}
|
||||
|
||||
yield $counter++ => $line;
|
||||
|
||||
} while (true);
|
||||
})(static::open($file, 'r'));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Writes the string to a file.
|
||||
* @throws Nette\IOException on error occurred
|
||||
*/
|
||||
public static function write(string $file, string $content, ?int $mode = 0666): void
|
||||
{
|
||||
static::createDir(dirname($file));
|
||||
if (@file_put_contents($file, $content) === false) { // @ is escalated to exception
|
||||
throw new Nette\IOException(sprintf(
|
||||
"Unable to write file '%s'. %s",
|
||||
self::normalizePath($file),
|
||||
Helpers::getLastError(),
|
||||
));
|
||||
}
|
||||
|
||||
if ($mode !== null && !@chmod($file, $mode)) { // @ is escalated to exception
|
||||
throw new Nette\IOException(sprintf(
|
||||
"Unable to chmod file '%s' to mode %s. %s",
|
||||
self::normalizePath($file),
|
||||
decoct($mode),
|
||||
Helpers::getLastError(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sets file permissions to `$fileMode` or directory permissions to `$dirMode`.
|
||||
* Recursively traverses and sets permissions on the entire contents of the directory as well.
|
||||
* @throws Nette\IOException on error occurred
|
||||
*/
|
||||
public static function makeWritable(string $path, int $dirMode = 0777, int $fileMode = 0666): void
|
||||
{
|
||||
if (is_file($path)) {
|
||||
if (!@chmod($path, $fileMode)) { // @ is escalated to exception
|
||||
throw new Nette\IOException(sprintf(
|
||||
"Unable to chmod file '%s' to mode %s. %s",
|
||||
self::normalizePath($path),
|
||||
decoct($fileMode),
|
||||
Helpers::getLastError(),
|
||||
));
|
||||
}
|
||||
} elseif (is_dir($path)) {
|
||||
foreach (new \FilesystemIterator($path) as $item) {
|
||||
static::makeWritable($item->getPathname(), $dirMode, $fileMode);
|
||||
}
|
||||
|
||||
if (!@chmod($path, $dirMode)) { // @ is escalated to exception
|
||||
throw new Nette\IOException(sprintf(
|
||||
"Unable to chmod directory '%s' to mode %s. %s",
|
||||
self::normalizePath($path),
|
||||
decoct($dirMode),
|
||||
Helpers::getLastError(),
|
||||
));
|
||||
}
|
||||
} else {
|
||||
throw new Nette\IOException(sprintf("File or directory '%s' not found.", self::normalizePath($path)));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Determines if the path is absolute.
|
||||
*/
|
||||
public static function isAbsolute(string $path): bool
|
||||
{
|
||||
return (bool) preg_match('#([a-z]:)?[/\\\]|[a-z][a-z0-9+.-]*://#Ai', $path);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Normalizes `..` and `.` and directory separators in path.
|
||||
*/
|
||||
public static function normalizePath(string $path): string
|
||||
{
|
||||
$parts = $path === '' ? [] : preg_split('~[/\\\]+~', $path);
|
||||
$res = [];
|
||||
foreach ($parts as $part) {
|
||||
if ($part === '..' && $res && end($res) !== '..' && end($res) !== '') {
|
||||
array_pop($res);
|
||||
} elseif ($part !== '.') {
|
||||
$res[] = $part;
|
||||
}
|
||||
}
|
||||
|
||||
return $res === ['']
|
||||
? DIRECTORY_SEPARATOR
|
||||
: implode(DIRECTORY_SEPARATOR, $res);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Joins all segments of the path and normalizes the result.
|
||||
*/
|
||||
public static function joinPaths(string ...$paths): string
|
||||
{
|
||||
return self::normalizePath(implode('/', $paths));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Resolves a path against a base path. If the path is absolute, returns it directly, if it's relative, joins it with the base path.
|
||||
*/
|
||||
public static function resolvePath(string $basePath, string $path): string
|
||||
{
|
||||
return match (true) {
|
||||
self::isAbsolute($path) => self::platformSlashes($path),
|
||||
$path === '' => self::platformSlashes($basePath),
|
||||
default => self::joinPaths($basePath, $path),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Converts backslashes to slashes.
|
||||
*/
|
||||
public static function unixSlashes(string $path): string
|
||||
{
|
||||
return strtr($path, '\\', '/');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Converts slashes to platform-specific directory separators.
|
||||
*/
|
||||
public static function platformSlashes(string $path): string
|
||||
{
|
||||
return DIRECTORY_SEPARATOR === '/'
|
||||
? strtr($path, '\\', '/')
|
||||
: str_replace(':\\\\', '://', strtr($path, '/', '\\')); // protocol://
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,512 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Nette Framework (https://nette.org)
|
||||
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Nette\Utils;
|
||||
|
||||
use Nette;
|
||||
use function array_merge, count, func_get_args, func_num_args, glob, implode, is_array, is_dir, iterator_to_array, preg_match, preg_quote, preg_replace, preg_split, rtrim, spl_object_id, sprintf, str_ends_with, str_starts_with, strnatcmp, strpbrk, strrpos, strtolower, strtr, substr, usort;
|
||||
use const GLOB_NOESCAPE, GLOB_NOSORT, GLOB_ONLYDIR;
|
||||
|
||||
|
||||
/**
|
||||
* Finder allows searching through directory trees using iterator.
|
||||
*
|
||||
* Finder::findFiles('*.php')
|
||||
* ->size('> 10kB')
|
||||
* ->from('.')
|
||||
* ->exclude('temp');
|
||||
*
|
||||
* @implements \IteratorAggregate<string, FileInfo>
|
||||
*/
|
||||
class Finder implements \IteratorAggregate
|
||||
{
|
||||
use Nette\SmartObject;
|
||||
|
||||
/** @var array<array{string, string}> */
|
||||
private array $find = [];
|
||||
|
||||
/** @var string[] */
|
||||
private array $in = [];
|
||||
|
||||
/** @var \Closure[] */
|
||||
private array $filters = [];
|
||||
|
||||
/** @var \Closure[] */
|
||||
private array $descentFilters = [];
|
||||
|
||||
/** @var array<string|self> */
|
||||
private array $appends = [];
|
||||
private bool $childFirst = false;
|
||||
|
||||
/** @var ?callable */
|
||||
private $sort;
|
||||
private int $maxDepth = -1;
|
||||
private bool $ignoreUnreadableDirs = true;
|
||||
|
||||
|
||||
/**
|
||||
* Begins search for files and directories matching mask.
|
||||
*/
|
||||
public static function find(string|array $masks = ['*']): static
|
||||
{
|
||||
$masks = is_array($masks) ? $masks : func_get_args(); // compatibility with variadic
|
||||
return (new static)->addMask($masks, 'dir')->addMask($masks, 'file');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Begins search for files matching mask.
|
||||
*/
|
||||
public static function findFiles(string|array $masks = ['*']): static
|
||||
{
|
||||
$masks = is_array($masks) ? $masks : func_get_args(); // compatibility with variadic
|
||||
return (new static)->addMask($masks, 'file');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Begins search for directories matching mask.
|
||||
*/
|
||||
public static function findDirectories(string|array $masks = ['*']): static
|
||||
{
|
||||
$masks = is_array($masks) ? $masks : func_get_args(); // compatibility with variadic
|
||||
return (new static)->addMask($masks, 'dir');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Finds files matching the specified masks.
|
||||
*/
|
||||
public function files(string|array $masks = ['*']): static
|
||||
{
|
||||
return $this->addMask((array) $masks, 'file');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Finds directories matching the specified masks.
|
||||
*/
|
||||
public function directories(string|array $masks = ['*']): static
|
||||
{
|
||||
return $this->addMask((array) $masks, 'dir');
|
||||
}
|
||||
|
||||
|
||||
private function addMask(array $masks, string $mode): static
|
||||
{
|
||||
foreach ($masks as $mask) {
|
||||
$mask = FileSystem::unixSlashes($mask);
|
||||
if ($mode === 'dir') {
|
||||
$mask = rtrim($mask, '/');
|
||||
}
|
||||
if ($mask === '' || ($mode === 'file' && str_ends_with($mask, '/'))) {
|
||||
throw new Nette\InvalidArgumentException("Invalid mask '$mask'");
|
||||
}
|
||||
if (str_starts_with($mask, '**/')) {
|
||||
$mask = substr($mask, 3);
|
||||
}
|
||||
$this->find[] = [$mask, $mode];
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Searches in the given directories. Wildcards are allowed.
|
||||
*/
|
||||
public function in(string|array $paths): static
|
||||
{
|
||||
$paths = is_array($paths) ? $paths : func_get_args(); // compatibility with variadic
|
||||
$this->addLocation($paths, '');
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Searches recursively from the given directories. Wildcards are allowed.
|
||||
*/
|
||||
public function from(string|array $paths): static
|
||||
{
|
||||
$paths = is_array($paths) ? $paths : func_get_args(); // compatibility with variadic
|
||||
$this->addLocation($paths, '/**');
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
private function addLocation(array $paths, string $ext): void
|
||||
{
|
||||
foreach ($paths as $path) {
|
||||
if ($path === '') {
|
||||
throw new Nette\InvalidArgumentException("Invalid directory '$path'");
|
||||
}
|
||||
$path = rtrim(FileSystem::unixSlashes($path), '/');
|
||||
$this->in[] = $path . $ext;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Lists directory's contents before the directory itself. By default, this is disabled.
|
||||
*/
|
||||
public function childFirst(bool $state = true): static
|
||||
{
|
||||
$this->childFirst = $state;
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Ignores unreadable directories. By default, this is enabled.
|
||||
*/
|
||||
public function ignoreUnreadableDirs(bool $state = true): static
|
||||
{
|
||||
$this->ignoreUnreadableDirs = $state;
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set a compare function for sorting directory entries. The function will be called to sort entries from the same directory.
|
||||
* @param callable(FileInfo, FileInfo): int $callback
|
||||
*/
|
||||
public function sortBy(callable $callback): static
|
||||
{
|
||||
$this->sort = $callback;
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sorts files in each directory naturally by name.
|
||||
*/
|
||||
public function sortByName(): static
|
||||
{
|
||||
$this->sort = fn(FileInfo $a, FileInfo $b): int => strnatcmp($a->getBasename(), $b->getBasename());
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Adds the specified paths or appends a new finder that returns.
|
||||
*/
|
||||
public function append(string|array|null $paths = null): static
|
||||
{
|
||||
if ($paths === null) {
|
||||
return $this->appends[] = new static;
|
||||
}
|
||||
|
||||
$this->appends = array_merge($this->appends, (array) $paths);
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/********************* filtering ****************d*g**/
|
||||
|
||||
|
||||
/**
|
||||
* Skips entries that matches the given masks relative to the ones defined with the in() or from() methods.
|
||||
*/
|
||||
public function exclude(string|array $masks): static
|
||||
{
|
||||
$masks = is_array($masks) ? $masks : func_get_args(); // compatibility with variadic
|
||||
foreach ($masks as $mask) {
|
||||
$mask = FileSystem::unixSlashes($mask);
|
||||
if (!preg_match('~^/?(\*\*/)?(.+)(/\*\*|/\*|/|)$~D', $mask, $m)) {
|
||||
throw new Nette\InvalidArgumentException("Invalid mask '$mask'");
|
||||
}
|
||||
$end = $m[3];
|
||||
$re = $this->buildPattern($m[2]);
|
||||
$filter = fn(FileInfo $file): bool => ($end && !$file->isDir())
|
||||
|| !preg_match($re, FileSystem::unixSlashes($file->getRelativePathname()));
|
||||
|
||||
$this->descentFilter($filter);
|
||||
if ($end !== '/*') {
|
||||
$this->filter($filter);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Yields only entries which satisfy the given filter.
|
||||
* @param callable(FileInfo): bool $callback
|
||||
*/
|
||||
public function filter(callable $callback): static
|
||||
{
|
||||
$this->filters[] = \Closure::fromCallable($callback);
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* It descends only to directories that match the specified filter.
|
||||
* @param callable(FileInfo): bool $callback
|
||||
*/
|
||||
public function descentFilter(callable $callback): static
|
||||
{
|
||||
$this->descentFilters[] = \Closure::fromCallable($callback);
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sets the maximum depth of entries.
|
||||
*/
|
||||
public function limitDepth(?int $depth): static
|
||||
{
|
||||
$this->maxDepth = $depth ?? -1;
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Restricts the search by size. $operator accepts "[operator] [size] [unit]" example: >=10kB
|
||||
*/
|
||||
public function size(string $operator, ?int $size = null): static
|
||||
{
|
||||
if (func_num_args() === 1) { // in $operator is predicate
|
||||
if (!preg_match('#^(?:([=<>!]=?|<>)\s*)?((?:\d*\.)?\d+)\s*(K|M|G|)B?$#Di', $operator, $matches)) {
|
||||
throw new Nette\InvalidArgumentException('Invalid size predicate format.');
|
||||
}
|
||||
|
||||
[, $operator, $size, $unit] = $matches;
|
||||
$units = ['' => 1, 'k' => 1e3, 'm' => 1e6, 'g' => 1e9];
|
||||
$size *= $units[strtolower($unit)];
|
||||
$operator = $operator ?: '=';
|
||||
}
|
||||
|
||||
return $this->filter(fn(FileInfo $file): bool => !$file->isFile() || Helpers::compare($file->getSize(), $operator, $size));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Restricts the search by modified time. $operator accepts "[operator] [date]" example: >1978-01-23
|
||||
*/
|
||||
public function date(string $operator, string|int|\DateTimeInterface|null $date = null): static
|
||||
{
|
||||
if (func_num_args() === 1) { // in $operator is predicate
|
||||
if (!preg_match('#^(?:([=<>!]=?|<>)\s*)?(.+)$#Di', $operator, $matches)) {
|
||||
throw new Nette\InvalidArgumentException('Invalid date predicate format.');
|
||||
}
|
||||
|
||||
[, $operator, $date] = $matches;
|
||||
$operator = $operator ?: '=';
|
||||
}
|
||||
|
||||
$date = DateTime::from($date)->getTimestamp();
|
||||
return $this->filter(fn(FileInfo $file): bool => !$file->isFile() || Helpers::compare($file->getMTime(), $operator, $date));
|
||||
}
|
||||
|
||||
|
||||
/********************* iterator generator ****************d*g**/
|
||||
|
||||
|
||||
/**
|
||||
* Returns an array with all found files and directories.
|
||||
* @return list<FileInfo>
|
||||
*/
|
||||
public function collect(): array
|
||||
{
|
||||
return iterator_to_array($this->getIterator(), preserve_keys: false);
|
||||
}
|
||||
|
||||
|
||||
/** @return \Generator<string, FileInfo> */
|
||||
public function getIterator(): \Generator
|
||||
{
|
||||
$plan = $this->buildPlan();
|
||||
foreach ($plan as $dir => $searches) {
|
||||
yield from $this->traverseDir($dir, $searches);
|
||||
}
|
||||
|
||||
foreach ($this->appends as $item) {
|
||||
if ($item instanceof self) {
|
||||
yield from $item->getIterator();
|
||||
} else {
|
||||
$item = FileSystem::platformSlashes($item);
|
||||
yield $item => new FileInfo($item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param array<object{pattern: string, mode: string, recursive: bool}> $searches
|
||||
* @param string[] $subdirs
|
||||
* @return \Generator<string, FileInfo>
|
||||
*/
|
||||
private function traverseDir(string $dir, array $searches, array $subdirs = []): \Generator
|
||||
{
|
||||
if ($this->maxDepth >= 0 && count($subdirs) > $this->maxDepth) {
|
||||
return;
|
||||
} elseif (!is_dir($dir)) {
|
||||
throw new Nette\InvalidStateException(sprintf("Directory '%s' does not exist.", rtrim($dir, '/\\')));
|
||||
}
|
||||
|
||||
try {
|
||||
$pathNames = new \FilesystemIterator($dir, \FilesystemIterator::FOLLOW_SYMLINKS | \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::CURRENT_AS_PATHNAME | \FilesystemIterator::UNIX_PATHS);
|
||||
} catch (\UnexpectedValueException $e) {
|
||||
if ($this->ignoreUnreadableDirs) {
|
||||
return;
|
||||
} else {
|
||||
throw new Nette\InvalidStateException($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
$files = $this->convertToFiles($pathNames, implode('/', $subdirs), FileSystem::isAbsolute($dir));
|
||||
|
||||
if ($this->sort) {
|
||||
$files = iterator_to_array($files);
|
||||
usort($files, $this->sort);
|
||||
}
|
||||
|
||||
foreach ($files as $file) {
|
||||
$pathName = $file->getPathname();
|
||||
$cache = $subSearch = [];
|
||||
|
||||
if ($file->isDir()) {
|
||||
foreach ($searches as $search) {
|
||||
if ($search->recursive && $this->proveFilters($this->descentFilters, $file, $cache)) {
|
||||
$subSearch[] = $search;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->childFirst && $subSearch) {
|
||||
yield from $this->traverseDir($pathName, $subSearch, array_merge($subdirs, [$file->getBasename()]));
|
||||
}
|
||||
|
||||
$relativePathname = FileSystem::unixSlashes($file->getRelativePathname());
|
||||
foreach ($searches as $search) {
|
||||
if (
|
||||
$file->{'is' . $search->mode}()
|
||||
&& preg_match($search->pattern, $relativePathname)
|
||||
&& $this->proveFilters($this->filters, $file, $cache)
|
||||
) {
|
||||
yield $pathName => $file;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$this->childFirst && $subSearch) {
|
||||
yield from $this->traverseDir($pathName, $subSearch, array_merge($subdirs, [$file->getBasename()]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private function convertToFiles(iterable $pathNames, string $relativePath, bool $absolute): \Generator
|
||||
{
|
||||
foreach ($pathNames as $pathName) {
|
||||
if (!$absolute) {
|
||||
$pathName = preg_replace('~\.?/~A', '', $pathName);
|
||||
}
|
||||
$pathName = FileSystem::platformSlashes($pathName);
|
||||
yield new FileInfo($pathName, $relativePath);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private function proveFilters(array $filters, FileInfo $file, array &$cache): bool
|
||||
{
|
||||
foreach ($filters as $filter) {
|
||||
$res = &$cache[spl_object_id($filter)];
|
||||
$res ??= $filter($file);
|
||||
if (!$res) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/** @return array<string, array<object{pattern: string, mode: string, recursive: bool}>> */
|
||||
private function buildPlan(): array
|
||||
{
|
||||
$plan = $dirCache = [];
|
||||
foreach ($this->find as [$mask, $mode]) {
|
||||
$splits = [];
|
||||
if (FileSystem::isAbsolute($mask)) {
|
||||
if ($this->in) {
|
||||
throw new Nette\InvalidStateException("You cannot combine the absolute path in the mask '$mask' and the directory to search '{$this->in[0]}'.");
|
||||
}
|
||||
$splits[] = self::splitRecursivePart($mask);
|
||||
} else {
|
||||
foreach ($this->in ?: ['.'] as $in) {
|
||||
$in = strtr($in, ['[' => '[[]', ']' => '[]]']); // in path, do not treat [ and ] as a pattern by glob()
|
||||
$splits[] = self::splitRecursivePart($in . '/' . $mask);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($splits as [$base, $rest, $recursive]) {
|
||||
$base = $base === '' ? '.' : $base;
|
||||
$dirs = $dirCache[$base] ??= strpbrk($base, '*?[')
|
||||
? glob($base, GLOB_NOSORT | GLOB_ONLYDIR | GLOB_NOESCAPE)
|
||||
: [strtr($base, ['[[]' => '[', '[]]' => ']'])]; // unescape [ and ]
|
||||
|
||||
if (!$dirs) {
|
||||
throw new Nette\InvalidStateException(sprintf("Directory '%s' does not exist.", rtrim($base, '/\\')));
|
||||
}
|
||||
|
||||
$search = (object) ['pattern' => $this->buildPattern($rest), 'mode' => $mode, 'recursive' => $recursive];
|
||||
foreach ($dirs as $dir) {
|
||||
$plan[$dir][] = $search;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $plan;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Since glob() does not know ** wildcard, we divide the path into a part for glob and a part for manual traversal.
|
||||
*/
|
||||
private static function splitRecursivePart(string $path): array
|
||||
{
|
||||
$a = strrpos($path, '/');
|
||||
$parts = preg_split('~(?<=^|/)\*\*($|/)~', substr($path, 0, $a + 1), 2);
|
||||
return isset($parts[1])
|
||||
? [$parts[0], $parts[1] . substr($path, $a + 1), true]
|
||||
: [$parts[0], substr($path, $a + 1), false];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Converts wildcards to regular expression.
|
||||
*/
|
||||
private function buildPattern(string $mask): string
|
||||
{
|
||||
if ($mask === '*') {
|
||||
return '##';
|
||||
} elseif (str_starts_with($mask, './')) {
|
||||
$anchor = '^';
|
||||
$mask = substr($mask, 2);
|
||||
} else {
|
||||
$anchor = '(?:^|/)';
|
||||
}
|
||||
|
||||
$pattern = strtr(
|
||||
preg_quote($mask, '#'),
|
||||
[
|
||||
'\*\*/' => '(.+/)?',
|
||||
'\*' => '[^/]*',
|
||||
'\?' => '[^/]',
|
||||
'\[\!' => '[^',
|
||||
'\[' => '[',
|
||||
'\]' => ']',
|
||||
'\-' => '-',
|
||||
],
|
||||
);
|
||||
return '#' . $anchor . $pattern . '$#D' . (Helpers::IsWindows ? 'i' : '');
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Nette Framework (https://nette.org)
|
||||
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Nette\Utils;
|
||||
|
||||
use Nette;
|
||||
use function abs, is_finite, is_nan, max, round;
|
||||
|
||||
|
||||
/**
|
||||
* Floating-point numbers comparison.
|
||||
*/
|
||||
class Floats
|
||||
{
|
||||
use Nette\StaticClass;
|
||||
|
||||
private const Epsilon = 1e-10;
|
||||
|
||||
|
||||
public static function isZero(float $value): bool
|
||||
{
|
||||
return abs($value) < self::Epsilon;
|
||||
}
|
||||
|
||||
|
||||
public static function isInteger(float $value): bool
|
||||
{
|
||||
return abs(round($value) - $value) < self::Epsilon;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Compare two floats. If $a < $b it returns -1, if they are equal it returns 0 and if $a > $b it returns 1
|
||||
* @throws \LogicException if one of parameters is NAN
|
||||
*/
|
||||
public static function compare(float $a, float $b): int
|
||||
{
|
||||
if (is_nan($a) || is_nan($b)) {
|
||||
throw new \LogicException('Trying to compare NAN');
|
||||
|
||||
} elseif (!is_finite($a) && !is_finite($b) && $a === $b) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$diff = abs($a - $b);
|
||||
if (($diff < self::Epsilon || ($diff / max(abs($a), abs($b)) < self::Epsilon))) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return $a < $b ? -1 : 1;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns true if $a = $b
|
||||
* @throws \LogicException if one of parameters is NAN
|
||||
*/
|
||||
public static function areEqual(float $a, float $b): bool
|
||||
{
|
||||
return self::compare($a, $b) === 0;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns true if $a < $b
|
||||
* @throws \LogicException if one of parameters is NAN
|
||||
*/
|
||||
public static function isLessThan(float $a, float $b): bool
|
||||
{
|
||||
return self::compare($a, $b) < 0;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns true if $a <= $b
|
||||
* @throws \LogicException if one of parameters is NAN
|
||||
*/
|
||||
public static function isLessThanOrEqualTo(float $a, float $b): bool
|
||||
{
|
||||
return self::compare($a, $b) <= 0;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns true if $a > $b
|
||||
* @throws \LogicException if one of parameters is NAN
|
||||
*/
|
||||
public static function isGreaterThan(float $a, float $b): bool
|
||||
{
|
||||
return self::compare($a, $b) > 0;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns true if $a >= $b
|
||||
* @throws \LogicException if one of parameters is NAN
|
||||
*/
|
||||
public static function isGreaterThanOrEqualTo(float $a, float $b): bool
|
||||
{
|
||||
return self::compare($a, $b) >= 0;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,109 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Nette Framework (https://nette.org)
|
||||
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Nette\Utils;
|
||||
|
||||
use Nette;
|
||||
use function array_unique, ini_get, levenshtein, max, min, ob_end_clean, ob_get_clean, ob_start, preg_replace, strlen;
|
||||
use const PHP_OS_FAMILY;
|
||||
|
||||
|
||||
class Helpers
|
||||
{
|
||||
public const IsWindows = PHP_OS_FAMILY === 'Windows';
|
||||
|
||||
|
||||
/**
|
||||
* Executes a callback and returns the captured output as a string.
|
||||
*/
|
||||
public static function capture(callable $func): string
|
||||
{
|
||||
ob_start(fn() => '');
|
||||
try {
|
||||
$func();
|
||||
return ob_get_clean();
|
||||
} catch (\Throwable $e) {
|
||||
ob_end_clean();
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the last occurred PHP error or an empty string if no error occurred. Unlike error_get_last(),
|
||||
* it is nit affected by the PHP directive html_errors and always returns text, not HTML.
|
||||
*/
|
||||
public static function getLastError(): string
|
||||
{
|
||||
$message = error_get_last()['message'] ?? '';
|
||||
$message = ini_get('html_errors') ? Html::htmlToText($message) : $message;
|
||||
$message = preg_replace('#^\w+\(.*?\): #', '', $message);
|
||||
return $message;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Converts false to null, does not change other values.
|
||||
*/
|
||||
public static function falseToNull(mixed $value): mixed
|
||||
{
|
||||
return $value === false ? null : $value;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns value clamped to the inclusive range of min and max.
|
||||
*/
|
||||
public static function clamp(int|float $value, int|float $min, int|float $max): int|float
|
||||
{
|
||||
if ($min > $max) {
|
||||
throw new Nette\InvalidArgumentException("Minimum ($min) is not less than maximum ($max).");
|
||||
}
|
||||
|
||||
return min(max($value, $min), $max);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Looks for a string from possibilities that is most similar to value, but not the same (for 8-bit encoding).
|
||||
* @param string[] $possibilities
|
||||
*/
|
||||
public static function getSuggestion(array $possibilities, string $value): ?string
|
||||
{
|
||||
$best = null;
|
||||
$min = (strlen($value) / 4 + 1) * 10 + .1;
|
||||
foreach (array_unique($possibilities) as $item) {
|
||||
if ($item !== $value && ($len = levenshtein($item, $value, 10, 11, 10)) < $min) {
|
||||
$min = $len;
|
||||
$best = $item;
|
||||
}
|
||||
}
|
||||
|
||||
return $best;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Compares two values in the same way that PHP does. Recognizes operators: >, >=, <, <=, =, ==, ===, !=, !==, <>
|
||||
*/
|
||||
public static function compare(mixed $left, string $operator, mixed $right): bool
|
||||
{
|
||||
return match ($operator) {
|
||||
'>' => $left > $right,
|
||||
'>=' => $left >= $right,
|
||||
'<' => $left < $right,
|
||||
'<=' => $left <= $right,
|
||||
'=', '==' => $left == $right,
|
||||
'===' => $left === $right,
|
||||
'!=', '<>' => $left != $right,
|
||||
'!==' => $left !== $right,
|
||||
default => throw new Nette\InvalidArgumentException("Unknown operator '$operator'"),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,840 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Nette Framework (https://nette.org)
|
||||
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Nette\Utils;
|
||||
|
||||
use Nette;
|
||||
use Nette\HtmlStringable;
|
||||
use function array_merge, array_splice, count, explode, func_num_args, html_entity_decode, htmlspecialchars, http_build_query, implode, is_array, is_bool, is_float, is_object, is_string, json_encode, max, number_format, rtrim, str_contains, str_repeat, str_replace, strip_tags, strncmp, strpbrk, substr;
|
||||
use const ENT_HTML5, ENT_NOQUOTES, ENT_QUOTES;
|
||||
|
||||
|
||||
/**
|
||||
* HTML helper.
|
||||
*
|
||||
* @property string|null $accept
|
||||
* @property string|null $accesskey
|
||||
* @property string|null $action
|
||||
* @property string|null $align
|
||||
* @property string|null $allow
|
||||
* @property string|null $alt
|
||||
* @property bool|null $async
|
||||
* @property string|null $autocapitalize
|
||||
* @property string|null $autocomplete
|
||||
* @property bool|null $autofocus
|
||||
* @property bool|null $autoplay
|
||||
* @property string|null $charset
|
||||
* @property bool|null $checked
|
||||
* @property string|null $cite
|
||||
* @property string|null $class
|
||||
* @property int|null $cols
|
||||
* @property int|null $colspan
|
||||
* @property string|null $content
|
||||
* @property bool|null $contenteditable
|
||||
* @property bool|null $controls
|
||||
* @property string|null $coords
|
||||
* @property string|null $crossorigin
|
||||
* @property string|null $data
|
||||
* @property string|null $datetime
|
||||
* @property string|null $decoding
|
||||
* @property bool|null $default
|
||||
* @property bool|null $defer
|
||||
* @property string|null $dir
|
||||
* @property string|null $dirname
|
||||
* @property bool|null $disabled
|
||||
* @property bool|null $download
|
||||
* @property string|null $draggable
|
||||
* @property string|null $dropzone
|
||||
* @property string|null $enctype
|
||||
* @property string|null $for
|
||||
* @property string|null $form
|
||||
* @property string|null $formaction
|
||||
* @property string|null $formenctype
|
||||
* @property string|null $formmethod
|
||||
* @property bool|null $formnovalidate
|
||||
* @property string|null $formtarget
|
||||
* @property string|null $headers
|
||||
* @property int|null $height
|
||||
* @property bool|null $hidden
|
||||
* @property float|null $high
|
||||
* @property string|null $href
|
||||
* @property string|null $hreflang
|
||||
* @property string|null $id
|
||||
* @property string|null $integrity
|
||||
* @property string|null $inputmode
|
||||
* @property bool|null $ismap
|
||||
* @property string|null $itemprop
|
||||
* @property string|null $kind
|
||||
* @property string|null $label
|
||||
* @property string|null $lang
|
||||
* @property string|null $list
|
||||
* @property bool|null $loop
|
||||
* @property float|null $low
|
||||
* @property float|null $max
|
||||
* @property int|null $maxlength
|
||||
* @property int|null $minlength
|
||||
* @property string|null $media
|
||||
* @property string|null $method
|
||||
* @property float|null $min
|
||||
* @property bool|null $multiple
|
||||
* @property bool|null $muted
|
||||
* @property string|null $name
|
||||
* @property bool|null $novalidate
|
||||
* @property bool|null $open
|
||||
* @property float|null $optimum
|
||||
* @property string|null $pattern
|
||||
* @property string|null $ping
|
||||
* @property string|null $placeholder
|
||||
* @property string|null $poster
|
||||
* @property string|null $preload
|
||||
* @property string|null $radiogroup
|
||||
* @property bool|null $readonly
|
||||
* @property string|null $rel
|
||||
* @property bool|null $required
|
||||
* @property bool|null $reversed
|
||||
* @property int|null $rows
|
||||
* @property int|null $rowspan
|
||||
* @property string|null $sandbox
|
||||
* @property string|null $scope
|
||||
* @property bool|null $selected
|
||||
* @property string|null $shape
|
||||
* @property int|null $size
|
||||
* @property string|null $sizes
|
||||
* @property string|null $slot
|
||||
* @property int|null $span
|
||||
* @property string|null $spellcheck
|
||||
* @property string|null $src
|
||||
* @property string|null $srcdoc
|
||||
* @property string|null $srclang
|
||||
* @property string|null $srcset
|
||||
* @property int|null $start
|
||||
* @property float|null $step
|
||||
* @property string|null $style
|
||||
* @property int|null $tabindex
|
||||
* @property string|null $target
|
||||
* @property string|null $title
|
||||
* @property string|null $translate
|
||||
* @property string|null $type
|
||||
* @property string|null $usemap
|
||||
* @property string|null $value
|
||||
* @property int|null $width
|
||||
* @property string|null $wrap
|
||||
*
|
||||
* @method self accept(?string $val)
|
||||
* @method self accesskey(?string $val, bool $state = null)
|
||||
* @method self action(?string $val)
|
||||
* @method self align(?string $val)
|
||||
* @method self allow(?string $val, bool $state = null)
|
||||
* @method self alt(?string $val)
|
||||
* @method self async(?bool $val)
|
||||
* @method self autocapitalize(?string $val)
|
||||
* @method self autocomplete(?string $val)
|
||||
* @method self autofocus(?bool $val)
|
||||
* @method self autoplay(?bool $val)
|
||||
* @method self charset(?string $val)
|
||||
* @method self checked(?bool $val)
|
||||
* @method self cite(?string $val)
|
||||
* @method self class(?string $val, bool $state = null)
|
||||
* @method self cols(?int $val)
|
||||
* @method self colspan(?int $val)
|
||||
* @method self content(?string $val)
|
||||
* @method self contenteditable(?bool $val)
|
||||
* @method self controls(?bool $val)
|
||||
* @method self coords(?string $val)
|
||||
* @method self crossorigin(?string $val)
|
||||
* @method self datetime(?string $val)
|
||||
* @method self decoding(?string $val)
|
||||
* @method self default(?bool $val)
|
||||
* @method self defer(?bool $val)
|
||||
* @method self dir(?string $val)
|
||||
* @method self dirname(?string $val)
|
||||
* @method self disabled(?bool $val)
|
||||
* @method self download(?bool $val)
|
||||
* @method self draggable(?string $val)
|
||||
* @method self dropzone(?string $val)
|
||||
* @method self enctype(?string $val)
|
||||
* @method self for(?string $val)
|
||||
* @method self form(?string $val)
|
||||
* @method self formaction(?string $val)
|
||||
* @method self formenctype(?string $val)
|
||||
* @method self formmethod(?string $val)
|
||||
* @method self formnovalidate(?bool $val)
|
||||
* @method self formtarget(?string $val)
|
||||
* @method self headers(?string $val, bool $state = null)
|
||||
* @method self height(?int $val)
|
||||
* @method self hidden(?bool $val)
|
||||
* @method self high(?float $val)
|
||||
* @method self hreflang(?string $val)
|
||||
* @method self id(?string $val)
|
||||
* @method self integrity(?string $val)
|
||||
* @method self inputmode(?string $val)
|
||||
* @method self ismap(?bool $val)
|
||||
* @method self itemprop(?string $val)
|
||||
* @method self kind(?string $val)
|
||||
* @method self label(?string $val)
|
||||
* @method self lang(?string $val)
|
||||
* @method self list(?string $val)
|
||||
* @method self loop(?bool $val)
|
||||
* @method self low(?float $val)
|
||||
* @method self max(?float $val)
|
||||
* @method self maxlength(?int $val)
|
||||
* @method self minlength(?int $val)
|
||||
* @method self media(?string $val)
|
||||
* @method self method(?string $val)
|
||||
* @method self min(?float $val)
|
||||
* @method self multiple(?bool $val)
|
||||
* @method self muted(?bool $val)
|
||||
* @method self name(?string $val)
|
||||
* @method self novalidate(?bool $val)
|
||||
* @method self open(?bool $val)
|
||||
* @method self optimum(?float $val)
|
||||
* @method self pattern(?string $val)
|
||||
* @method self ping(?string $val, bool $state = null)
|
||||
* @method self placeholder(?string $val)
|
||||
* @method self poster(?string $val)
|
||||
* @method self preload(?string $val)
|
||||
* @method self radiogroup(?string $val)
|
||||
* @method self readonly(?bool $val)
|
||||
* @method self rel(?string $val)
|
||||
* @method self required(?bool $val)
|
||||
* @method self reversed(?bool $val)
|
||||
* @method self rows(?int $val)
|
||||
* @method self rowspan(?int $val)
|
||||
* @method self sandbox(?string $val, bool $state = null)
|
||||
* @method self scope(?string $val)
|
||||
* @method self selected(?bool $val)
|
||||
* @method self shape(?string $val)
|
||||
* @method self size(?int $val)
|
||||
* @method self sizes(?string $val)
|
||||
* @method self slot(?string $val)
|
||||
* @method self span(?int $val)
|
||||
* @method self spellcheck(?string $val)
|
||||
* @method self src(?string $val)
|
||||
* @method self srcdoc(?string $val)
|
||||
* @method self srclang(?string $val)
|
||||
* @method self srcset(?string $val)
|
||||
* @method self start(?int $val)
|
||||
* @method self step(?float $val)
|
||||
* @method self style(?string $property, string $val = null)
|
||||
* @method self tabindex(?int $val)
|
||||
* @method self target(?string $val)
|
||||
* @method self title(?string $val)
|
||||
* @method self translate(?string $val)
|
||||
* @method self type(?string $val)
|
||||
* @method self usemap(?string $val)
|
||||
* @method self value(?string $val)
|
||||
* @method self width(?int $val)
|
||||
* @method self wrap(?string $val)
|
||||
*/
|
||||
class Html implements \ArrayAccess, \Countable, \IteratorAggregate, HtmlStringable
|
||||
{
|
||||
use Nette\SmartObject;
|
||||
|
||||
/** @var array<string, mixed> element's attributes */
|
||||
public $attrs = [];
|
||||
|
||||
/** void elements */
|
||||
public static $emptyElements = [
|
||||
'img' => 1, 'hr' => 1, 'br' => 1, 'input' => 1, 'meta' => 1, 'area' => 1, 'embed' => 1, 'keygen' => 1,
|
||||
'source' => 1, 'base' => 1, 'col' => 1, 'link' => 1, 'param' => 1, 'basefont' => 1, 'frame' => 1,
|
||||
'isindex' => 1, 'wbr' => 1, 'command' => 1, 'track' => 1,
|
||||
];
|
||||
|
||||
/** @var array<int, HtmlStringable|string> nodes */
|
||||
protected $children = [];
|
||||
|
||||
/** element's name */
|
||||
private string $name = '';
|
||||
|
||||
private bool $isEmpty = false;
|
||||
|
||||
|
||||
/**
|
||||
* Constructs new HTML element.
|
||||
* @param array|string $attrs element's attributes or plain text content
|
||||
*/
|
||||
public static function el(?string $name = null, array|string|null $attrs = null): static
|
||||
{
|
||||
$el = new static;
|
||||
$parts = explode(' ', (string) $name, 2);
|
||||
$el->setName($parts[0]);
|
||||
|
||||
if (is_array($attrs)) {
|
||||
$el->attrs = $attrs;
|
||||
|
||||
} elseif ($attrs !== null) {
|
||||
$el->setText($attrs);
|
||||
}
|
||||
|
||||
if (isset($parts[1])) {
|
||||
foreach (Strings::matchAll($parts[1] . ' ', '#([a-z0-9:-]+)(?:=(["\'])?(.*?)(?(2)\2|\s))?#i') as $m) {
|
||||
$el->attrs[$m[1]] = $m[3] ?? true;
|
||||
}
|
||||
}
|
||||
|
||||
return $el;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns an object representing HTML text.
|
||||
*/
|
||||
public static function fromHtml(string $html): static
|
||||
{
|
||||
return (new static)->setHtml($html);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns an object representing plain text.
|
||||
*/
|
||||
public static function fromText(string $text): static
|
||||
{
|
||||
return (new static)->setText($text);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Converts to HTML.
|
||||
*/
|
||||
final public function toHtml(): string
|
||||
{
|
||||
return $this->render();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Converts to plain text.
|
||||
*/
|
||||
final public function toText(): string
|
||||
{
|
||||
return $this->getText();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Converts given HTML code to plain text.
|
||||
*/
|
||||
public static function htmlToText(string $html): string
|
||||
{
|
||||
return html_entity_decode(strip_tags($html), ENT_QUOTES | ENT_HTML5, 'UTF-8');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Changes element's name.
|
||||
*/
|
||||
final public function setName(string $name, ?bool $isEmpty = null): static
|
||||
{
|
||||
$this->name = $name;
|
||||
$this->isEmpty = $isEmpty ?? isset(static::$emptyElements[$name]);
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns element's name.
|
||||
*/
|
||||
final public function getName(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Is element empty?
|
||||
*/
|
||||
final public function isEmpty(): bool
|
||||
{
|
||||
return $this->isEmpty;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sets multiple attributes.
|
||||
*/
|
||||
public function addAttributes(array $attrs): static
|
||||
{
|
||||
$this->attrs = array_merge($this->attrs, $attrs);
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Appends value to element's attribute.
|
||||
*/
|
||||
public function appendAttribute(string $name, mixed $value, mixed $option = true): static
|
||||
{
|
||||
if (is_array($value)) {
|
||||
$prev = isset($this->attrs[$name]) ? (array) $this->attrs[$name] : [];
|
||||
$this->attrs[$name] = $value + $prev;
|
||||
|
||||
} elseif ((string) $value === '') {
|
||||
$tmp = &$this->attrs[$name]; // appending empty value? -> ignore, but ensure it exists
|
||||
|
||||
} elseif (!isset($this->attrs[$name]) || is_array($this->attrs[$name])) { // needs array
|
||||
$this->attrs[$name][$value] = $option;
|
||||
|
||||
} else {
|
||||
$this->attrs[$name] = [$this->attrs[$name] => true, $value => $option];
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sets element's attribute.
|
||||
*/
|
||||
public function setAttribute(string $name, mixed $value): static
|
||||
{
|
||||
$this->attrs[$name] = $value;
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns element's attribute.
|
||||
*/
|
||||
public function getAttribute(string $name): mixed
|
||||
{
|
||||
return $this->attrs[$name] ?? null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Unsets element's attribute.
|
||||
*/
|
||||
public function removeAttribute(string $name): static
|
||||
{
|
||||
unset($this->attrs[$name]);
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Unsets element's attributes.
|
||||
*/
|
||||
public function removeAttributes(array $attributes): static
|
||||
{
|
||||
foreach ($attributes as $name) {
|
||||
unset($this->attrs[$name]);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Overloaded setter for element's attribute.
|
||||
*/
|
||||
final public function __set(string $name, mixed $value): void
|
||||
{
|
||||
$this->attrs[$name] = $value;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Overloaded getter for element's attribute.
|
||||
*/
|
||||
final public function &__get(string $name): mixed
|
||||
{
|
||||
return $this->attrs[$name];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Overloaded tester for element's attribute.
|
||||
*/
|
||||
final public function __isset(string $name): bool
|
||||
{
|
||||
return isset($this->attrs[$name]);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Overloaded unsetter for element's attribute.
|
||||
*/
|
||||
final public function __unset(string $name): void
|
||||
{
|
||||
unset($this->attrs[$name]);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Overloaded setter for element's attribute.
|
||||
*/
|
||||
final public function __call(string $m, array $args): mixed
|
||||
{
|
||||
$p = substr($m, 0, 3);
|
||||
if ($p === 'get' || $p === 'set' || $p === 'add') {
|
||||
$m = substr($m, 3);
|
||||
$m[0] = $m[0] | "\x20";
|
||||
if ($p === 'get') {
|
||||
return $this->attrs[$m] ?? null;
|
||||
|
||||
} elseif ($p === 'add') {
|
||||
$args[] = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (count($args) === 0) { // invalid
|
||||
|
||||
} elseif (count($args) === 1) { // set
|
||||
$this->attrs[$m] = $args[0];
|
||||
|
||||
} else { // add
|
||||
$this->appendAttribute($m, $args[0], $args[1]);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Special setter for element's attribute.
|
||||
*/
|
||||
final public function href(string $path, array $query = []): static
|
||||
{
|
||||
if ($query) {
|
||||
$query = http_build_query($query, '', '&');
|
||||
if ($query !== '') {
|
||||
$path .= '?' . $query;
|
||||
}
|
||||
}
|
||||
|
||||
$this->attrs['href'] = $path;
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Setter for data-* attributes. Booleans are converted to 'true' resp. 'false'.
|
||||
*/
|
||||
public function data(string $name, mixed $value = null): static
|
||||
{
|
||||
if (func_num_args() === 1) {
|
||||
$this->attrs['data'] = $name;
|
||||
} else {
|
||||
$this->attrs["data-$name"] = is_bool($value)
|
||||
? json_encode($value)
|
||||
: $value;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sets element's HTML content.
|
||||
*/
|
||||
final public function setHtml(mixed $html): static
|
||||
{
|
||||
$this->children = [(string) $html];
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns element's HTML content.
|
||||
*/
|
||||
final public function getHtml(): string
|
||||
{
|
||||
return implode('', $this->children);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sets element's textual content.
|
||||
*/
|
||||
final public function setText(mixed $text): static
|
||||
{
|
||||
if (!$text instanceof HtmlStringable) {
|
||||
$text = htmlspecialchars((string) $text, ENT_NOQUOTES, 'UTF-8');
|
||||
}
|
||||
|
||||
$this->children = [(string) $text];
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns element's textual content.
|
||||
*/
|
||||
final public function getText(): string
|
||||
{
|
||||
return self::htmlToText($this->getHtml());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Adds new element's child.
|
||||
*/
|
||||
final public function addHtml(mixed $child): static
|
||||
{
|
||||
return $this->insert(null, $child);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Appends plain-text string to element content.
|
||||
*/
|
||||
public function addText(mixed $text): static
|
||||
{
|
||||
if (!$text instanceof HtmlStringable) {
|
||||
$text = htmlspecialchars((string) $text, ENT_NOQUOTES, 'UTF-8');
|
||||
}
|
||||
|
||||
return $this->insert(null, $text);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Creates and adds a new Html child.
|
||||
*/
|
||||
final public function create(string $name, array|string|null $attrs = null): static
|
||||
{
|
||||
$this->insert(null, $child = static::el($name, $attrs));
|
||||
return $child;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Inserts child node.
|
||||
*/
|
||||
public function insert(?int $index, HtmlStringable|string $child, bool $replace = false): static
|
||||
{
|
||||
$child = $child instanceof self ? $child : (string) $child;
|
||||
if ($index === null) { // append
|
||||
$this->children[] = $child;
|
||||
|
||||
} else { // insert or replace
|
||||
array_splice($this->children, $index, $replace ? 1 : 0, [$child]);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Inserts (replaces) child node (\ArrayAccess implementation).
|
||||
* @param int|null $index position or null for appending
|
||||
* @param Html|string $child Html node or raw HTML string
|
||||
*/
|
||||
final public function offsetSet($index, $child): void
|
||||
{
|
||||
$this->insert($index, $child, replace: true);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns child node (\ArrayAccess implementation).
|
||||
* @param int $index
|
||||
*/
|
||||
final public function offsetGet($index): HtmlStringable|string
|
||||
{
|
||||
return $this->children[$index];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Exists child node? (\ArrayAccess implementation).
|
||||
* @param int $index
|
||||
*/
|
||||
final public function offsetExists($index): bool
|
||||
{
|
||||
return isset($this->children[$index]);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Removes child node (\ArrayAccess implementation).
|
||||
* @param int $index
|
||||
*/
|
||||
public function offsetUnset($index): void
|
||||
{
|
||||
if (isset($this->children[$index])) {
|
||||
array_splice($this->children, $index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns children count.
|
||||
*/
|
||||
final public function count(): int
|
||||
{
|
||||
return count($this->children);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Removes all children.
|
||||
*/
|
||||
public function removeChildren(): void
|
||||
{
|
||||
$this->children = [];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Iterates over elements.
|
||||
* @return \ArrayIterator<int, HtmlStringable|string>
|
||||
*/
|
||||
final public function getIterator(): \ArrayIterator
|
||||
{
|
||||
return new \ArrayIterator($this->children);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns all children.
|
||||
*/
|
||||
final public function getChildren(): array
|
||||
{
|
||||
return $this->children;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Renders element's start tag, content and end tag.
|
||||
*/
|
||||
final public function render(?int $indent = null): string
|
||||
{
|
||||
$s = $this->startTag();
|
||||
|
||||
if (!$this->isEmpty) {
|
||||
// add content
|
||||
if ($indent !== null) {
|
||||
$indent++;
|
||||
}
|
||||
|
||||
foreach ($this->children as $child) {
|
||||
if ($child instanceof self) {
|
||||
$s .= $child->render($indent);
|
||||
} else {
|
||||
$s .= $child;
|
||||
}
|
||||
}
|
||||
|
||||
// add end tag
|
||||
$s .= $this->endTag();
|
||||
}
|
||||
|
||||
if ($indent !== null) {
|
||||
return "\n" . str_repeat("\t", $indent - 1) . $s . "\n" . str_repeat("\t", max(0, $indent - 2));
|
||||
}
|
||||
|
||||
return $s;
|
||||
}
|
||||
|
||||
|
||||
final public function __toString(): string
|
||||
{
|
||||
return $this->render();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns element's start tag.
|
||||
*/
|
||||
final public function startTag(): string
|
||||
{
|
||||
return $this->name
|
||||
? '<' . $this->name . $this->attributes() . '>'
|
||||
: '';
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns element's end tag.
|
||||
*/
|
||||
final public function endTag(): string
|
||||
{
|
||||
return $this->name && !$this->isEmpty ? '</' . $this->name . '>' : '';
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns element's attributes.
|
||||
* @internal
|
||||
*/
|
||||
final public function attributes(): string
|
||||
{
|
||||
if (!is_array($this->attrs)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$s = '';
|
||||
$attrs = $this->attrs;
|
||||
foreach ($attrs as $key => $value) {
|
||||
if ($value === null || $value === false) {
|
||||
continue;
|
||||
|
||||
} elseif ($value === true) {
|
||||
$s .= ' ' . $key;
|
||||
|
||||
continue;
|
||||
|
||||
} elseif (is_array($value)) {
|
||||
if (strncmp($key, 'data-', 5) === 0) {
|
||||
$value = Json::encode($value);
|
||||
|
||||
} else {
|
||||
$tmp = null;
|
||||
foreach ($value as $k => $v) {
|
||||
if ($v != null) { // intentionally ==, skip nulls & empty string
|
||||
// composite 'style' vs. 'others'
|
||||
$tmp[] = $v === true
|
||||
? $k
|
||||
: (is_string($k) ? $k . ':' . $v : $v);
|
||||
}
|
||||
}
|
||||
|
||||
if ($tmp === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$value = implode($key === 'style' || !strncmp($key, 'on', 2) ? ';' : ' ', $tmp);
|
||||
}
|
||||
} elseif (is_float($value)) {
|
||||
$value = rtrim(rtrim(number_format($value, 10, '.', ''), '0'), '.');
|
||||
|
||||
} else {
|
||||
$value = (string) $value;
|
||||
}
|
||||
|
||||
$q = str_contains($value, '"') ? "'" : '"';
|
||||
$s .= ' ' . $key . '=' . $q
|
||||
. str_replace(
|
||||
['&', $q, '<'],
|
||||
['&', $q === '"' ? '"' : ''', '<'],
|
||||
$value,
|
||||
)
|
||||
. (str_contains($value, '`') && strpbrk($value, ' <>"\'') === false ? ' ' : '')
|
||||
. $q;
|
||||
}
|
||||
|
||||
$s = str_replace('@', '@', $s);
|
||||
return $s;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Clones all children too.
|
||||
*/
|
||||
public function __clone()
|
||||
{
|
||||
foreach ($this->children as $key => $value) {
|
||||
if (is_object($value)) {
|
||||
$this->children[$key] = clone $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue