diff --git a/.github/dependabot.yml b/.github/dependabot.yml index f3203de..8a4ee0b 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,6 +1,7 @@ version: 2 updates: + - package-ecosystem: "composer" directory: "/" schedule: @@ -9,5 +10,15 @@ updates: include: "scope" prefix: "composer" labels: - - "dependency-update" + - "enhancement" versioning-strategy: "widen" + + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + commit-message: + include: "scope" + prefix: "github-actions" + labels: + - "enhancement" diff --git a/.github/workflows/integrate.yaml b/.github/workflows/integrate.yaml index 46e645b..92affe7 100644 --- a/.github/workflows/integrate.yaml +++ b/.github/workflows/integrate.yaml @@ -15,7 +15,7 @@ jobs: strategy: matrix: php-version: - - "8.0" + - "8.1" steps: - name: "Checkout" @@ -63,8 +63,13 @@ jobs: matrix: php-version: - "8.0" - - "8.1" - + code-coverage: + - "none" + include: + - php-version: "8.1" + code-coverage: "pcov" + + steps: - name: "Checkout" uses: "actions/checkout@v2" @@ -72,7 +77,7 @@ jobs: - name: "Install PHP" uses: "shivammathur/setup-php@v2" with: - coverage: "none" + coverage: "${{ matrix.code-coverage }}" php-version: "${{ matrix.php-version }}" ini-values: zend.assertions=1 @@ -91,53 +96,21 @@ jobs: run: "composer update --no-interaction --no-progress" - name: "Run tests" - timeout-minutes: 3 + if: ${{ matrix.code-coverage == 'none' }} + timeout-minutes: 1 run: "vendor/bin/phpunit --no-coverage --no-logging" - code-coverage: - name: "Code Coverage" - - runs-on: "ubuntu-latest" - - strategy: - matrix: - php-version: - - "8.0" - - steps: - - name: "Checkout" - uses: "actions/checkout@v2" - - - name: "Install PHP" - uses: "shivammathur/setup-php@v2" - with: - coverage: "pcov" - php-version: "${{ matrix.php-version }}" - ini-values: zend.assertions=1 - - - name: "Get composer cache directory" - id: composercache - run: echo "::set-output name=dir::$(composer config cache-files-dir)" - - - name: "Cache dependencies" - uses: actions/cache@v2 - with: - path: ${{ steps.composercache.outputs.dir }} - key: ${{ runner.os }}-php-${{ matrix.php-version }}-${{ matrix.dependencies }}-composer-${{ hashFiles('**/composer.json') }} - restore-keys: ${{ runner.os }}-php-${{ matrix.php-version }}-${{ matrix.dependencies }}-composer- - - - name: "Install dependencies" - run: "composer update --no-interaction --no-progress" - - name: "Run tests" - timeout-minutes: 3 - run: "vendor/bin/phpunit --coverage-clover=coverage.xml" + if: ${{ matrix.code-coverage != 'none' }} + timeout-minutes: 1 + run: "vendor/bin/phpunit" - name: "Send code coverage report to Codecov.io" + if: ${{ matrix.code-coverage != 'none' }} uses: codecov/codecov-action@v1 with: token: ${{ secrets.CODECOV_TOKEN }} - file: ./coverage.xml + file: ./coverage/clover.xml fail_ci_if_error: true coding-standards: @@ -148,7 +121,7 @@ jobs: strategy: matrix: php-version: - - "8.0" + - "8.1" steps: - name: "Checkout" @@ -186,7 +159,7 @@ jobs: strategy: matrix: php-version: - - "8.0" + - "8.1" steps: - name: "Checkout" @@ -197,6 +170,7 @@ jobs: with: coverage: "none" php-version: "${{ matrix.php-version }}" + ini-values: zend.assertions=1 tools: cs2pr - name: "Get composer cache directory" diff --git a/.gitignore b/.gitignore index c17df81..2f28ed8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ -.idea/ -coverage/ -vendor/ -.php-cs-fixer.cache -.phpunit.result.cache -composer.lock +/.idea/ +/coverage/ +/vendor/ +/.php-cs-fixer.cache +/.phpunit.result.cache +/composer.lock diff --git a/composer.json b/composer.json index daf666a..68134ca 100644 --- a/composer.json +++ b/composer.json @@ -15,13 +15,13 @@ }, "require-dev": { "malukenho/mcbumpface": "^1.1.5", - "phpstan/phpstan": "^1.2.0", - "phpstan/phpstan-phpunit": "^1.0.0", - "phpunit/phpunit": "^9.5.10", + "phpstan/phpstan": "^1.5.3", + "phpstan/phpstan-phpunit": "^1.1.0", + "phpstan/phpstan-strict-rules": "^1.1.0", + "phpunit/phpunit": "^9.5.19", "slam/php-cs-fixer-extensions": "^3.1.0", "slam/php-debug-r": "^1.7.0", - "slam/phpstan-extensions": "^6.0.0", - "symfony/console": "^6.0.1" + "symfony/console": "^6.0.5" }, "autoload": { "psr-4": { @@ -33,6 +33,11 @@ "SlamTest\\ErrorHandler\\": "tests/" } }, + "config": { + "allow-plugins": { + "malukenho/mcbumpface": true + } + }, "extra": { "mc-bumpface": { "stripVersionPrefixes": true diff --git a/lib/ErrorHandler.php b/lib/ErrorHandler.php index 5b4c498..ddc79e6 100644 --- a/lib/ErrorHandler.php +++ b/lib/ErrorHandler.php @@ -36,6 +36,7 @@ final class ErrorHandler private bool $autoExit = true; private ?bool $cli = null; private ?int $terminalWidth = null; + /** * @var null|resource */ @@ -44,23 +45,29 @@ final class ErrorHandler private ?bool $logErrors = null; private bool $logVariables = true; private ?bool $displayErrors = null; + /** * @var callable */ private $emailCallback; + /** * @var callable */ private $errorLogCallback = '\\error_log'; + /** * @var array */ private array $scream = []; + /** * @var array> */ private array $exceptionsTypesFor404 = []; + private bool $shouldEmail404Exceptions = true; + public function __construct(callable $emailCallback) { $this->emailCallback = $emailCallback; @@ -109,13 +116,15 @@ public function setTerminalWidth(int $terminalWidth): void public function getTerminalWidth(): int { if (null === $this->terminalWidth) { - $width = \getenv('COLUMNS'); - - if (false === $width && 1 === \preg_match('{rows.(\d+);.columns.(\d+);}i', (string) \exec('stty -a 2> /dev/null | grep columns'), $match)) { - $width = $match[2]; // @codeCoverageIgnore + $width = (int) \getenv('COLUMNS'); + if (0 === $width && 1 === \preg_match('{rows.(\d+);.columns.(\d+);}i', (string) \exec('stty -a 2> /dev/null | grep columns'), $match)) { + $width = (int) $match[2]; // @codeCoverageIgnore + } + if (0 === $width) { + $width = 80; // @codeCoverageIgnore } - $this->setTerminalWidth((int) $width ?: 80); + $this->setTerminalWidth($width); \assert(null !== $this->terminalWidth); } @@ -228,11 +237,11 @@ public function exceptionHandler(Throwable $exception): void if ($this->isCli()) { $currentEx = $exception; do { - $width = $this->getTerminalWidth() ? $this->getTerminalWidth() - 3 : 120; + $width = 0 !== $this->getTerminalWidth() ? $this->getTerminalWidth() - 3 : 120; $lines = [ 'Message: ' . $currentEx->getMessage(), '', - 'Class: ' . \get_class($currentEx), + 'Class: ' . $currentEx::class, 'Code: ' . $this->getExceptionCode($currentEx), 'File: ' . $currentEx->getFile() . ':' . $currentEx->getLine(), ]; @@ -273,7 +282,7 @@ public function exceptionHandler(Throwable $exception): void // @codeCoverageIgnoreStart if (! \headers_sent()) { $header = 'HTTP/1.1 500 Internal Server Error'; - if (\in_array(\get_class($exception), $this->exceptionsTypesFor404, true)) { + if (\in_array($exception::class, $this->exceptionsTypesFor404, true)) { $header = 'HTTP/1.1 404 Not Found'; } \header($header); @@ -288,7 +297,7 @@ public function renderHtmlException(Throwable $exception): string $ajax = (isset($_SERVER['X_REQUESTED_WITH']) && 'XMLHttpRequest' === $_SERVER['X_REQUESTED_WITH']); $output = ''; $errorType = '500: Internal Server Error'; - if (\in_array(\get_class($exception), $this->exceptionsTypesFor404, true)) { + if (\in_array($exception::class, $this->exceptionsTypesFor404, true)) { $errorType = '404: Not Found'; } if (! $ajax) { @@ -309,7 +318,7 @@ public function renderHtmlException(Throwable $exception): string . 'Stack trace:
%s
' . '%s', \htmlspecialchars($currentEx->getMessage()), - \get_class($currentEx), + $currentEx::class, $this->getExceptionCode($currentEx), $currentEx->getFile(), $currentEx->getLine(), @@ -347,7 +356,7 @@ public function logException(Throwable $exception): void $output = \sprintf( '%s%s: %s in %s:%s%s%s', ($i > 0 ? '{PR ' . $i . '} ' : ''), - \get_class($exception), + $exception::class, $exception->getMessage(), $exception->getFile(), $exception->getLine(), @@ -363,7 +372,13 @@ public function logException(Throwable $exception): void public function emailException(Throwable $exception): void { - if (! $this->logErrors()) { + if ( + ! $this->logErrors() + || ( + ! $this->shouldEmail404Exceptions + && \in_array($exception::class, $this->exceptionsTypesFor404, true) + ) + ) { return; } @@ -389,7 +404,7 @@ public function emailException(Throwable $exception): void $currentEx = $exception; do { $bodyArray = [ - 'Class' => \get_class($currentEx), + 'Class' => $currentEx::class, 'Code' => $this->getExceptionCode($currentEx), 'Message' => $currentEx->getMessage(), 'File' => $currentEx->getFile() . ':' . $currentEx->getLine(), @@ -405,10 +420,10 @@ public function emailException(Throwable $exception): void $username = null; if ($this->logVariables()) { - if (! empty($_POST)) { + if ([] !== $_POST) { $bodyText .= '$_POST = ' . \print_r($_POST, true) . \PHP_EOL; } - if (isset($_SESSION) && ! empty($_SESSION)) { + if (isset($_SESSION) && [] !== $_SESSION) { $sessionText = \print_r(\class_exists(DoctrineDebug::class) ? DoctrineDebug::export($_SESSION, 4) : $_SESSION, true); $bodyText .= '$_SESSION = ' . $sessionText . \PHP_EOL; @@ -422,7 +437,7 @@ public function emailException(Throwable $exception): void $subject = \sprintf( 'Error%s: %s', - $username ? \sprintf(' [%s]', $username) : '', + null !== $username ? \sprintf(' [%s]', $username) : '', $exception->getMessage() ); @@ -465,4 +480,14 @@ public function get404ExceptionTypes(): array { return $this->exceptionsTypesFor404; } + + public function setShouldEmail404Exceptions(bool $shouldEmail404Exceptions): void + { + $this->shouldEmail404Exceptions = $shouldEmail404Exceptions; + } + + public function shouldEmail404Exceptions(): bool + { + return $this->shouldEmail404Exceptions; + } } diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 726830b..2b61af1 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1,10 +1,5 @@ parameters: ignoreErrors: - - - message: "#^Cannot cast mixed to string\\.$#" - count: 1 - path: lib/ErrorHandler.php - - message: "#^Expression \"\\$arrayPerVerificaErrori\\['undefined_index'\\]\" on a separate line does not do anything\\.$#" count: 1 diff --git a/phpstan.neon b/phpstan.neon index 7c1ea95..20444af 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,6 +1,6 @@ includes: - vendor/phpstan/phpstan-phpunit/extension.neon - - vendor/slam/phpstan-extensions/conf/slam-rules.neon + - vendor/phpstan/phpstan-strict-rules/rules.neon - phpstan-baseline.neon parameters: diff --git a/phpunit.xml b/phpunit.xml index aa8afac..a1bb563 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -2,7 +2,6 @@ @@ -11,6 +10,8 @@ ./lib + + diff --git a/tests/ErrorHandlerTest.php b/tests/ErrorHandlerTest.php index e2ff0ca..525e3df 100644 --- a/tests/ErrorHandlerTest.php +++ b/tests/ErrorHandlerTest.php @@ -58,13 +58,13 @@ public function testDefaultConfiguration(): void self::assertTrue($errorHandler->isCli()); self::assertTrue($errorHandler->autoExit()); - self::assertNotNull($errorHandler->getTerminalWidth()); + self::assertGreaterThan(0, $errorHandler->getTerminalWidth()); self::assertSame(\STDERR, $errorHandler->getErrorOutputStream()); self::assertFalse($errorHandler->logErrors()); $errorHandler->setCli(false); $errorHandler->setAutoExit(false); - $errorHandler->setTerminalWidth($width = \mt_rand(1, 999)); + $errorHandler->setTerminalWidth($width = \mt_rand(1, 999)); $errorHandler->setErrorOutputStream($memoryStream = \fopen('php://memory', 'r+')); $errorHandler->setLogErrors(true); @@ -296,6 +296,27 @@ public function test404SpecificExceptionForHeaders(): void self::assertStringContainsString('404: Not Found', $this->errorHandler->renderHtmlException(new RuntimeException())); } + public function test404ExceptionCanBeDisabledToSendEmail(): void + { + $this->errorHandler->setLogErrors(true); + $exceptionTypes = [RuntimeException::class]; + $this->errorHandler->set404ExceptionTypes($exceptionTypes); + + self::assertTrue($this->errorHandler->shouldEmail404Exceptions()); + + $this->errorHandler->emailException(new RuntimeException()); + + self::assertNotEmpty($this->emailsSent); + $this->emailsSent = []; + + $this->errorHandler->setShouldEmail404Exceptions(false); + self::assertFalse($this->errorHandler->shouldEmail404Exceptions()); + + $this->errorHandler->emailException(new RuntimeException()); + + self::assertEmpty($this->emailsSent); + } + public function testCanSetCustomErrorLogCallback(): void { $this->errorHandler->setLogErrors(true);