Commit version 24.12.13800

This commit is contained in:
2025-01-06 17:35:06 -05:00
parent b7f6a79c2c
commit 55d9218816
6133 changed files with 4239740 additions and 1374287 deletions

View File

@ -0,0 +1,786 @@
<?php
declare(strict_types=1);
namespace Brick\Math;
use Brick\Math\Exception\DivisionByZeroException;
use Brick\Math\Exception\MathException;
use Brick\Math\Exception\NegativeNumberException;
use Brick\Math\Internal\Calculator;
/**
* Immutable, arbitrary-precision signed decimal numbers.
*
* @psalm-immutable
*/
final class BigDecimal extends BigNumber
{
/**
* The unscaled value of this decimal number.
*
* This is a string of digits with an optional leading minus sign.
* No leading zero must be present.
* No leading minus sign must be present if the value is 0.
*/
private string $value;
/**
* The scale (number of digits after the decimal point) of this decimal number.
*
* This must be zero or more.
*/
private int $scale;
/**
* Protected constructor. Use a factory method to obtain an instance.
*
* @param string $value The unscaled value, validated.
* @param int $scale The scale, validated.
*/
protected function __construct(string $value, int $scale = 0)
{
$this->value = $value;
$this->scale = $scale;
}
/**
* Creates a BigDecimal of the given value.
*
* @throws MathException If the value cannot be converted to a BigDecimal.
*
* @psalm-pure
*/
public static function of(BigNumber|int|float|string $value) : BigDecimal
{
return parent::of($value)->toBigDecimal();
}
/**
* Creates a BigDecimal from an unscaled value and a scale.
*
* Example: `(12345, 3)` will result in the BigDecimal `12.345`.
*
* @param BigNumber|int|float|string $value The unscaled value. Must be convertible to a BigInteger.
* @param int $scale The scale of the number, positive or zero.
*
* @throws \InvalidArgumentException If the scale is negative.
*
* @psalm-pure
*/
public static function ofUnscaledValue(BigNumber|int|float|string $value, int $scale = 0) : BigDecimal
{
if ($scale < 0) {
throw new \InvalidArgumentException('The scale cannot be negative.');
}
return new BigDecimal((string) BigInteger::of($value), $scale);
}
/**
* Returns a BigDecimal representing zero, with a scale of zero.
*
* @psalm-pure
*/
public static function zero() : BigDecimal
{
/**
* @psalm-suppress ImpureStaticVariable
* @var BigDecimal|null $zero
*/
static $zero;
if ($zero === null) {
$zero = new BigDecimal('0');
}
return $zero;
}
/**
* Returns a BigDecimal representing one, with a scale of zero.
*
* @psalm-pure
*/
public static function one() : BigDecimal
{
/**
* @psalm-suppress ImpureStaticVariable
* @var BigDecimal|null $one
*/
static $one;
if ($one === null) {
$one = new BigDecimal('1');
}
return $one;
}
/**
* Returns a BigDecimal representing ten, with a scale of zero.
*
* @psalm-pure
*/
public static function ten() : BigDecimal
{
/**
* @psalm-suppress ImpureStaticVariable
* @var BigDecimal|null $ten
*/
static $ten;
if ($ten === null) {
$ten = new BigDecimal('10');
}
return $ten;
}
/**
* Returns the sum of this number and the given one.
*
* The result has a scale of `max($this->scale, $that->scale)`.
*
* @param BigNumber|int|float|string $that The number to add. Must be convertible to a BigDecimal.
*
* @throws MathException If the number is not valid, or is not convertible to a BigDecimal.
*/
public function plus(BigNumber|int|float|string $that) : BigDecimal
{
$that = BigDecimal::of($that);
if ($that->value === '0' && $that->scale <= $this->scale) {
return $this;
}
if ($this->value === '0' && $this->scale <= $that->scale) {
return $that;
}
[$a, $b] = $this->scaleValues($this, $that);
$value = Calculator::get()->add($a, $b);
$scale = $this->scale > $that->scale ? $this->scale : $that->scale;
return new BigDecimal($value, $scale);
}
/**
* Returns the difference of this number and the given one.
*
* The result has a scale of `max($this->scale, $that->scale)`.
*
* @param BigNumber|int|float|string $that The number to subtract. Must be convertible to a BigDecimal.
*
* @throws MathException If the number is not valid, or is not convertible to a BigDecimal.
*/
public function minus(BigNumber|int|float|string $that) : BigDecimal
{
$that = BigDecimal::of($that);
if ($that->value === '0' && $that->scale <= $this->scale) {
return $this;
}
[$a, $b] = $this->scaleValues($this, $that);
$value = Calculator::get()->sub($a, $b);
$scale = $this->scale > $that->scale ? $this->scale : $that->scale;
return new BigDecimal($value, $scale);
}
/**
* Returns the product of this number and the given one.
*
* The result has a scale of `$this->scale + $that->scale`.
*
* @param BigNumber|int|float|string $that The multiplier. Must be convertible to a BigDecimal.
*
* @throws MathException If the multiplier is not a valid number, or is not convertible to a BigDecimal.
*/
public function multipliedBy(BigNumber|int|float|string $that) : BigDecimal
{
$that = BigDecimal::of($that);
if ($that->value === '1' && $that->scale === 0) {
return $this;
}
if ($this->value === '1' && $this->scale === 0) {
return $that;
}
$value = Calculator::get()->mul($this->value, $that->value);
$scale = $this->scale + $that->scale;
return new BigDecimal($value, $scale);
}
/**
* Returns the result of the division of this number by the given one, at the given scale.
*
* @param BigNumber|int|float|string $that The divisor.
* @param int|null $scale The desired scale, or null to use the scale of this number.
* @param int $roundingMode An optional rounding mode.
*
* @throws \InvalidArgumentException If the scale or rounding mode is invalid.
* @throws MathException If the number is invalid, is zero, or rounding was necessary.
*/
public function dividedBy(BigNumber|int|float|string $that, ?int $scale = null, int $roundingMode = RoundingMode::UNNECESSARY) : BigDecimal
{
$that = BigDecimal::of($that);
if ($that->isZero()) {
throw DivisionByZeroException::divisionByZero();
}
if ($scale === null) {
$scale = $this->scale;
} elseif ($scale < 0) {
throw new \InvalidArgumentException('Scale cannot be negative.');
}
if ($that->value === '1' && $that->scale === 0 && $scale === $this->scale) {
return $this;
}
$p = $this->valueWithMinScale($that->scale + $scale);
$q = $that->valueWithMinScale($this->scale - $scale);
$result = Calculator::get()->divRound($p, $q, $roundingMode);
return new BigDecimal($result, $scale);
}
/**
* Returns the exact result of the division of this number by the given one.
*
* The scale of the result is automatically calculated to fit all the fraction digits.
*
* @param BigNumber|int|float|string $that The divisor. Must be convertible to a BigDecimal.
*
* @throws MathException If the divisor is not a valid number, is not convertible to a BigDecimal, is zero,
* or the result yields an infinite number of digits.
*/
public function exactlyDividedBy(BigNumber|int|float|string $that) : BigDecimal
{
$that = BigDecimal::of($that);
if ($that->value === '0') {
throw DivisionByZeroException::divisionByZero();
}
[, $b] = $this->scaleValues($this, $that);
$d = \rtrim($b, '0');
$scale = \strlen($b) - \strlen($d);
$calculator = Calculator::get();
foreach ([5, 2] as $prime) {
for (;;) {
$lastDigit = (int) $d[-1];
if ($lastDigit % $prime !== 0) {
break;
}
$d = $calculator->divQ($d, (string) $prime);
$scale++;
}
}
return $this->dividedBy($that, $scale)->stripTrailingZeros();
}
/**
* Returns this number exponentiated to the given value.
*
* The result has a scale of `$this->scale * $exponent`.
*
* @throws \InvalidArgumentException If the exponent is not in the range 0 to 1,000,000.
*/
public function power(int $exponent) : BigDecimal
{
if ($exponent === 0) {
return BigDecimal::one();
}
if ($exponent === 1) {
return $this;
}
if ($exponent < 0 || $exponent > Calculator::MAX_POWER) {
throw new \InvalidArgumentException(\sprintf(
'The exponent %d is not in the range 0 to %d.',
$exponent,
Calculator::MAX_POWER
));
}
return new BigDecimal(Calculator::get()->pow($this->value, $exponent), $this->scale * $exponent);
}
/**
* Returns the quotient of the division of this number by this given one.
*
* The quotient has a scale of `0`.
*
* @param BigNumber|int|float|string $that The divisor. Must be convertible to a BigDecimal.
*
* @throws MathException If the divisor is not a valid decimal number, or is zero.
*/
public function quotient(BigNumber|int|float|string $that) : BigDecimal
{
$that = BigDecimal::of($that);
if ($that->isZero()) {
throw DivisionByZeroException::divisionByZero();
}
$p = $this->valueWithMinScale($that->scale);
$q = $that->valueWithMinScale($this->scale);
$quotient = Calculator::get()->divQ($p, $q);
return new BigDecimal($quotient, 0);
}
/**
* Returns the remainder of the division of this number by this given one.
*
* The remainder has a scale of `max($this->scale, $that->scale)`.
*
* @param BigNumber|int|float|string $that The divisor. Must be convertible to a BigDecimal.
*
* @throws MathException If the divisor is not a valid decimal number, or is zero.
*/
public function remainder(BigNumber|int|float|string $that) : BigDecimal
{
$that = BigDecimal::of($that);
if ($that->isZero()) {
throw DivisionByZeroException::divisionByZero();
}
$p = $this->valueWithMinScale($that->scale);
$q = $that->valueWithMinScale($this->scale);
$remainder = Calculator::get()->divR($p, $q);
$scale = $this->scale > $that->scale ? $this->scale : $that->scale;
return new BigDecimal($remainder, $scale);
}
/**
* Returns the quotient and remainder of the division of this number by the given one.
*
* The quotient has a scale of `0`, and the remainder has a scale of `max($this->scale, $that->scale)`.
*
* @param BigNumber|int|float|string $that The divisor. Must be convertible to a BigDecimal.
*
* @return BigDecimal[] An array containing the quotient and the remainder.
*
* @throws MathException If the divisor is not a valid decimal number, or is zero.
*/
public function quotientAndRemainder(BigNumber|int|float|string $that) : array
{
$that = BigDecimal::of($that);
if ($that->isZero()) {
throw DivisionByZeroException::divisionByZero();
}
$p = $this->valueWithMinScale($that->scale);
$q = $that->valueWithMinScale($this->scale);
[$quotient, $remainder] = Calculator::get()->divQR($p, $q);
$scale = $this->scale > $that->scale ? $this->scale : $that->scale;
$quotient = new BigDecimal($quotient, 0);
$remainder = new BigDecimal($remainder, $scale);
return [$quotient, $remainder];
}
/**
* Returns the square root of this number, rounded down to the given number of decimals.
*
* @throws \InvalidArgumentException If the scale is negative.
* @throws NegativeNumberException If this number is negative.
*/
public function sqrt(int $scale) : BigDecimal
{
if ($scale < 0) {
throw new \InvalidArgumentException('Scale cannot be negative.');
}
if ($this->value === '0') {
return new BigDecimal('0', $scale);
}
if ($this->value[0] === '-') {
throw new NegativeNumberException('Cannot calculate the square root of a negative number.');
}
$value = $this->value;
$addDigits = 2 * $scale - $this->scale;
if ($addDigits > 0) {
// add zeros
$value .= \str_repeat('0', $addDigits);
} elseif ($addDigits < 0) {
// trim digits
if (-$addDigits >= \strlen($this->value)) {
// requesting a scale too low, will always yield a zero result
return new BigDecimal('0', $scale);
}
$value = \substr($value, 0, $addDigits);
}
$value = Calculator::get()->sqrt($value);
return new BigDecimal($value, $scale);
}
/**
* Returns a copy of this BigDecimal with the decimal point moved $n places to the left.
*/
public function withPointMovedLeft(int $n) : BigDecimal
{
if ($n === 0) {
return $this;
}
if ($n < 0) {
return $this->withPointMovedRight(-$n);
}
return new BigDecimal($this->value, $this->scale + $n);
}
/**
* Returns a copy of this BigDecimal with the decimal point moved $n places to the right.
*/
public function withPointMovedRight(int $n) : BigDecimal
{
if ($n === 0) {
return $this;
}
if ($n < 0) {
return $this->withPointMovedLeft(-$n);
}
$value = $this->value;
$scale = $this->scale - $n;
if ($scale < 0) {
if ($value !== '0') {
$value .= \str_repeat('0', -$scale);
}
$scale = 0;
}
return new BigDecimal($value, $scale);
}
/**
* Returns a copy of this BigDecimal with any trailing zeros removed from the fractional part.
*/
public function stripTrailingZeros() : BigDecimal
{
if ($this->scale === 0) {
return $this;
}
$trimmedValue = \rtrim($this->value, '0');
if ($trimmedValue === '') {
return BigDecimal::zero();
}
$trimmableZeros = \strlen($this->value) - \strlen($trimmedValue);
if ($trimmableZeros === 0) {
return $this;
}
if ($trimmableZeros > $this->scale) {
$trimmableZeros = $this->scale;
}
$value = \substr($this->value, 0, -$trimmableZeros);
$scale = $this->scale - $trimmableZeros;
return new BigDecimal($value, $scale);
}
/**
* Returns the absolute value of this number.
*/
public function abs() : BigDecimal
{
return $this->isNegative() ? $this->negated() : $this;
}
/**
* Returns the negated value of this number.
*/
public function negated() : BigDecimal
{
return new BigDecimal(Calculator::get()->neg($this->value), $this->scale);
}
public function compareTo(BigNumber|int|float|string $that) : int
{
$that = BigNumber::of($that);
if ($that instanceof BigInteger) {
$that = $that->toBigDecimal();
}
if ($that instanceof BigDecimal) {
[$a, $b] = $this->scaleValues($this, $that);
return Calculator::get()->cmp($a, $b);
}
return - $that->compareTo($this);
}
public function getSign() : int
{
return ($this->value === '0') ? 0 : (($this->value[0] === '-') ? -1 : 1);
}
public function getUnscaledValue() : BigInteger
{
return self::newBigInteger($this->value);
}
public function getScale() : int
{
return $this->scale;
}
/**
* Returns a string representing the integral part of this decimal number.
*
* Example: `-123.456` => `-123`.
*/
public function getIntegralPart() : string
{
if ($this->scale === 0) {
return $this->value;
}
$value = $this->getUnscaledValueWithLeadingZeros();
return \substr($value, 0, -$this->scale);
}
/**
* Returns a string representing the fractional part of this decimal number.
*
* If the scale is zero, an empty string is returned.
*
* Examples: `-123.456` => '456', `123` => ''.
*/
public function getFractionalPart() : string
{
if ($this->scale === 0) {
return '';
}
$value = $this->getUnscaledValueWithLeadingZeros();
return \substr($value, -$this->scale);
}
/**
* Returns whether this decimal number has a non-zero fractional part.
*/
public function hasNonZeroFractionalPart() : bool
{
return $this->getFractionalPart() !== \str_repeat('0', $this->scale);
}
public function toBigInteger() : BigInteger
{
$zeroScaleDecimal = $this->scale === 0 ? $this : $this->dividedBy(1, 0);
return self::newBigInteger($zeroScaleDecimal->value);
}
public function toBigDecimal() : BigDecimal
{
return $this;
}
public function toBigRational() : BigRational
{
$numerator = self::newBigInteger($this->value);
$denominator = self::newBigInteger('1' . \str_repeat('0', $this->scale));
return self::newBigRational($numerator, $denominator, false);
}
public function toScale(int $scale, int $roundingMode = RoundingMode::UNNECESSARY) : BigDecimal
{
if ($scale === $this->scale) {
return $this;
}
return $this->dividedBy(BigDecimal::one(), $scale, $roundingMode);
}
public function toInt() : int
{
return $this->toBigInteger()->toInt();
}
public function toFloat() : float
{
return (float) (string) $this;
}
public function __toString() : string
{
if ($this->scale === 0) {
return $this->value;
}
$value = $this->getUnscaledValueWithLeadingZeros();
return \substr($value, 0, -$this->scale) . '.' . \substr($value, -$this->scale);
}
/**
* This method is required for serializing the object and SHOULD NOT be accessed directly.
*
* @internal
*
* @return array{value: string, scale: int}
*/
public function __serialize(): array
{
return ['value' => $this->value, 'scale' => $this->scale];
}
/**
* This method is only here to allow unserializing the object and cannot be accessed directly.
*
* @internal
* @psalm-suppress RedundantPropertyInitializationCheck
*
* @param array{value: string, scale: int} $data
*
* @throws \LogicException
*/
public function __unserialize(array $data): void
{
if (isset($this->value)) {
throw new \LogicException('__unserialize() is an internal function, it must not be called directly.');
}
$this->value = $data['value'];
$this->scale = $data['scale'];
}
/**
* This method is required by interface Serializable and SHOULD NOT be accessed directly.
*
* @internal
*/
public function serialize() : string
{
return $this->value . ':' . $this->scale;
}
/**
* This method is only here to implement interface Serializable and cannot be accessed directly.
*
* @internal
* @psalm-suppress RedundantPropertyInitializationCheck
*
* @throws \LogicException
*/
public function unserialize($value) : void
{
if (isset($this->value)) {
throw new \LogicException('unserialize() is an internal function, it must not be called directly.');
}
[$value, $scale] = \explode(':', $value);
$this->value = $value;
$this->scale = (int) $scale;
}
/**
* Puts the internal values of the given decimal numbers on the same scale.
*
* @return array{string, string} The scaled integer values of $x and $y.
*/
private function scaleValues(BigDecimal $x, BigDecimal $y) : array
{
$a = $x->value;
$b = $y->value;
if ($b !== '0' && $x->scale > $y->scale) {
$b .= \str_repeat('0', $x->scale - $y->scale);
} elseif ($a !== '0' && $x->scale < $y->scale) {
$a .= \str_repeat('0', $y->scale - $x->scale);
}
return [$a, $b];
}
private function valueWithMinScale(int $scale) : string
{
$value = $this->value;
if ($this->value !== '0' && $scale > $this->scale) {
$value .= \str_repeat('0', $scale - $this->scale);
}
return $value;
}
/**
* Adds leading zeros if necessary to the unscaled value to represent the full decimal number.
*/
private function getUnscaledValueWithLeadingZeros() : string
{
$value = $this->value;
$targetLength = $this->scale + 1;
$negative = ($value[0] === '-');
$length = \strlen($value);
if ($negative) {
$length--;
}
if ($length >= $targetLength) {
return $this->value;
}
if ($negative) {
$value = \substr($value, 1);
}
$value = \str_pad($value, $targetLength, '0', STR_PAD_LEFT);
if ($negative) {
$value = '-' . $value;
}
return $value;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,512 @@
<?php
declare(strict_types=1);
namespace Brick\Math;
use Brick\Math\Exception\DivisionByZeroException;
use Brick\Math\Exception\MathException;
use Brick\Math\Exception\NumberFormatException;
use Brick\Math\Exception\RoundingNecessaryException;
/**
* Common interface for arbitrary-precision rational numbers.
*
* @psalm-immutable
*/
abstract class BigNumber implements \Serializable, \JsonSerializable
{
/**
* The regular expression used to parse integer, decimal and rational numbers.
*/
private const PARSE_REGEXP =
'/^' .
'(?<sign>[\-\+])?' .
'(?:' .
'(?:' .
'(?<integral>[0-9]+)?' .
'(?<point>\.)?' .
'(?<fractional>[0-9]+)?' .
'(?:[eE](?<exponent>[\-\+]?[0-9]+))?' .
')|(?:' .
'(?<numerator>[0-9]+)' .
'\/?' .
'(?<denominator>[0-9]+)' .
')' .
')' .
'$/';
/**
* Creates a BigNumber of the given value.
*
* The concrete return type is dependent on the given value, with the following rules:
*
* - BigNumber instances are returned as is
* - integer numbers are returned as BigInteger
* - floating point numbers are converted to a string then parsed as such
* - strings containing a `/` character are returned as BigRational
* - strings containing a `.` character or using an exponential notation are returned as BigDecimal
* - strings containing only digits with an optional leading `+` or `-` sign are returned as BigInteger
*
* @throws NumberFormatException If the format of the number is not valid.
* @throws DivisionByZeroException If the value represents a rational number with a denominator of zero.
*
* @psalm-pure
*/
public static function of(BigNumber|int|float|string $value) : BigNumber
{
if ($value instanceof BigNumber) {
return $value;
}
if (\is_int($value)) {
return new BigInteger((string) $value);
}
$value = \is_float($value) ? self::floatToString($value) : $value;
$throw = static function() use ($value) : void {
throw new NumberFormatException(\sprintf(
'The given value "%s" does not represent a valid number.',
$value
));
};
if (\preg_match(self::PARSE_REGEXP, $value, $matches) !== 1) {
$throw();
}
$getMatch = static fn(string $value): ?string => (($matches[$value] ?? '') !== '') ? $matches[$value] : null;
$sign = $getMatch('sign');
$numerator = $getMatch('numerator');
$denominator = $getMatch('denominator');
if ($numerator !== null) {
assert($denominator !== null);
if ($sign !== null) {
$numerator = $sign . $numerator;
}
$numerator = self::cleanUp($numerator);
$denominator = self::cleanUp($denominator);
if ($denominator === '0') {
throw DivisionByZeroException::denominatorMustNotBeZero();
}
return new BigRational(
new BigInteger($numerator),
new BigInteger($denominator),
false
);
}
$point = $getMatch('point');
$integral = $getMatch('integral');
$fractional = $getMatch('fractional');
$exponent = $getMatch('exponent');
if ($integral === null && $fractional === null) {
$throw();
}
if ($integral === null) {
$integral = '0';
}
if ($point !== null || $exponent !== null) {
$fractional = ($fractional ?? '');
$exponent = ($exponent !== null) ? (int) $exponent : 0;
if ($exponent === PHP_INT_MIN || $exponent === PHP_INT_MAX) {
throw new NumberFormatException('Exponent too large.');
}
$unscaledValue = self::cleanUp(($sign ?? ''). $integral . $fractional);
$scale = \strlen($fractional) - $exponent;
if ($scale < 0) {
if ($unscaledValue !== '0') {
$unscaledValue .= \str_repeat('0', - $scale);
}
$scale = 0;
}
return new BigDecimal($unscaledValue, $scale);
}
$integral = self::cleanUp(($sign ?? '') . $integral);
return new BigInteger($integral);
}
/**
* Safely converts float to string, avoiding locale-dependent issues.
*
* @see https://github.com/brick/math/pull/20
*
* @psalm-pure
* @psalm-suppress ImpureFunctionCall
*/
private static function floatToString(float $float) : string
{
$currentLocale = \setlocale(LC_NUMERIC, '0');
\setlocale(LC_NUMERIC, 'C');
$result = (string) $float;
\setlocale(LC_NUMERIC, $currentLocale);
return $result;
}
/**
* Proxy method to access BigInteger's protected constructor from sibling classes.
*
* @internal
* @psalm-pure
*/
protected function newBigInteger(string $value) : BigInteger
{
return new BigInteger($value);
}
/**
* Proxy method to access BigDecimal's protected constructor from sibling classes.
*
* @internal
* @psalm-pure
*/
protected function newBigDecimal(string $value, int $scale = 0) : BigDecimal
{
return new BigDecimal($value, $scale);
}
/**
* Proxy method to access BigRational's protected constructor from sibling classes.
*
* @internal
* @psalm-pure
*/
protected function newBigRational(BigInteger $numerator, BigInteger $denominator, bool $checkDenominator) : BigRational
{
return new BigRational($numerator, $denominator, $checkDenominator);
}
/**
* Returns the minimum of the given values.
*
* @param BigNumber|int|float|string ...$values The numbers to compare. All the numbers need to be convertible
* to an instance of the class this method is called on.
*
* @throws \InvalidArgumentException If no values are given.
* @throws MathException If an argument is not valid.
*
* @psalm-suppress LessSpecificReturnStatement
* @psalm-suppress MoreSpecificReturnType
* @psalm-pure
*/
public static function min(BigNumber|int|float|string ...$values) : static
{
$min = null;
foreach ($values as $value) {
$value = static::of($value);
if ($min === null || $value->isLessThan($min)) {
$min = $value;
}
}
if ($min === null) {
throw new \InvalidArgumentException(__METHOD__ . '() expects at least one value.');
}
return $min;
}
/**
* Returns the maximum of the given values.
*
* @param BigNumber|int|float|string ...$values The numbers to compare. All the numbers need to be convertible
* to an instance of the class this method is called on.
*
* @throws \InvalidArgumentException If no values are given.
* @throws MathException If an argument is not valid.
*
* @psalm-suppress LessSpecificReturnStatement
* @psalm-suppress MoreSpecificReturnType
* @psalm-pure
*/
public static function max(BigNumber|int|float|string ...$values) : static
{
$max = null;
foreach ($values as $value) {
$value = static::of($value);
if ($max === null || $value->isGreaterThan($max)) {
$max = $value;
}
}
if ($max === null) {
throw new \InvalidArgumentException(__METHOD__ . '() expects at least one value.');
}
return $max;
}
/**
* Returns the sum of the given values.
*
* @param BigNumber|int|float|string ...$values The numbers to add. All the numbers need to be convertible
* to an instance of the class this method is called on.
*
* @throws \InvalidArgumentException If no values are given.
* @throws MathException If an argument is not valid.
*
* @psalm-pure
*/
public static function sum(BigNumber|int|float|string ...$values) : static
{
/** @var static|null $sum */
$sum = null;
foreach ($values as $value) {
$value = static::of($value);
$sum = $sum === null ? $value : self::add($sum, $value);
}
if ($sum === null) {
throw new \InvalidArgumentException(__METHOD__ . '() expects at least one value.');
}
return $sum;
}
/**
* Adds two BigNumber instances in the correct order to avoid a RoundingNecessaryException.
*
* @todo This could be better resolved by creating an abstract protected method in BigNumber, and leaving to
* concrete classes the responsibility to perform the addition themselves or delegate it to the given number,
* depending on their ability to perform the operation. This will also require a version bump because we're
* potentially breaking custom BigNumber implementations (if any...)
*
* @psalm-pure
*/
private static function add(BigNumber $a, BigNumber $b) : BigNumber
{
if ($a instanceof BigRational) {
return $a->plus($b);
}
if ($b instanceof BigRational) {
return $b->plus($a);
}
if ($a instanceof BigDecimal) {
return $a->plus($b);
}
if ($b instanceof BigDecimal) {
return $b->plus($a);
}
/** @var BigInteger $a */
return $a->plus($b);
}
/**
* Removes optional leading zeros and + sign from the given number.
*
* @param string $number The number, validated as a non-empty string of digits with optional leading sign.
*
* @psalm-pure
*/
private static function cleanUp(string $number) : string
{
$firstChar = $number[0];
if ($firstChar === '+' || $firstChar === '-') {
$number = \substr($number, 1);
}
$number = \ltrim($number, '0');
if ($number === '') {
return '0';
}
if ($firstChar === '-') {
return '-' . $number;
}
return $number;
}
/**
* Checks if this number is equal to the given one.
*/
public function isEqualTo(BigNumber|int|float|string $that) : bool
{
return $this->compareTo($that) === 0;
}
/**
* Checks if this number is strictly lower than the given one.
*/
public function isLessThan(BigNumber|int|float|string $that) : bool
{
return $this->compareTo($that) < 0;
}
/**
* Checks if this number is lower than or equal to the given one.
*/
public function isLessThanOrEqualTo(BigNumber|int|float|string $that) : bool
{
return $this->compareTo($that) <= 0;
}
/**
* Checks if this number is strictly greater than the given one.
*/
public function isGreaterThan(BigNumber|int|float|string $that) : bool
{
return $this->compareTo($that) > 0;
}
/**
* Checks if this number is greater than or equal to the given one.
*/
public function isGreaterThanOrEqualTo(BigNumber|int|float|string $that) : bool
{
return $this->compareTo($that) >= 0;
}
/**
* Checks if this number equals zero.
*/
public function isZero() : bool
{
return $this->getSign() === 0;
}
/**
* Checks if this number is strictly negative.
*/
public function isNegative() : bool
{
return $this->getSign() < 0;
}
/**
* Checks if this number is negative or zero.
*/
public function isNegativeOrZero() : bool
{
return $this->getSign() <= 0;
}
/**
* Checks if this number is strictly positive.
*/
public function isPositive() : bool
{
return $this->getSign() > 0;
}
/**
* Checks if this number is positive or zero.
*/
public function isPositiveOrZero() : bool
{
return $this->getSign() >= 0;
}
/**
* Returns the sign of this number.
*
* @return int -1 if the number is negative, 0 if zero, 1 if positive.
*/
abstract public function getSign() : int;
/**
* Compares this number to the given one.
*
* @return int [-1,0,1] If `$this` is lower than, equal to, or greater than `$that`.
*
* @throws MathException If the number is not valid.
*/
abstract public function compareTo(BigNumber|int|float|string $that) : int;
/**
* Converts this number to a BigInteger.
*
* @throws RoundingNecessaryException If this number cannot be converted to a BigInteger without rounding.
*/
abstract public function toBigInteger() : BigInteger;
/**
* Converts this number to a BigDecimal.
*
* @throws RoundingNecessaryException If this number cannot be converted to a BigDecimal without rounding.
*/
abstract public function toBigDecimal() : BigDecimal;
/**
* Converts this number to a BigRational.
*/
abstract public function toBigRational() : BigRational;
/**
* Converts this number to a BigDecimal with the given scale, using rounding if necessary.
*
* @param int $scale The scale of the resulting `BigDecimal`.
* @param int $roundingMode A `RoundingMode` constant.
*
* @throws RoundingNecessaryException If this number cannot be converted to the given scale without rounding.
* This only applies when RoundingMode::UNNECESSARY is used.
*/
abstract public function toScale(int $scale, int $roundingMode = RoundingMode::UNNECESSARY) : BigDecimal;
/**
* Returns the exact value of this number as a native integer.
*
* If this number cannot be converted to a native integer without losing precision, an exception is thrown.
* Note that the acceptable range for an integer depends on the platform and differs for 32-bit and 64-bit.
*
* @throws MathException If this number cannot be exactly converted to a native integer.
*/
abstract public function toInt() : int;
/**
* Returns an approximation of this number as a floating-point value.
*
* Note that this method can discard information as the precision of a floating-point value
* is inherently limited.
*
* If the number is greater than the largest representable floating point number, positive infinity is returned.
* If the number is less than the smallest representable floating point number, negative infinity is returned.
*/
abstract public function toFloat() : float;
/**
* Returns a string representation of this number.
*
* The output of this method can be parsed by the `of()` factory method;
* this will yield an object equal to this one, without any information loss.
*/
abstract public function __toString() : string;
public function jsonSerialize() : string
{
return $this->__toString();
}
}

View File

@ -0,0 +1,445 @@
<?php
declare(strict_types=1);
namespace Brick\Math;
use Brick\Math\Exception\DivisionByZeroException;
use Brick\Math\Exception\MathException;
use Brick\Math\Exception\NumberFormatException;
use Brick\Math\Exception\RoundingNecessaryException;
/**
* An arbitrarily large rational number.
*
* This class is immutable.
*
* @psalm-immutable
*/
final class BigRational extends BigNumber
{
/**
* The numerator.
*/
private BigInteger $numerator;
/**
* The denominator. Always strictly positive.
*/
private BigInteger $denominator;
/**
* Protected constructor. Use a factory method to obtain an instance.
*
* @param BigInteger $numerator The numerator.
* @param BigInteger $denominator The denominator.
* @param bool $checkDenominator Whether to check the denominator for negative and zero.
*
* @throws DivisionByZeroException If the denominator is zero.
*/
protected function __construct(BigInteger $numerator, BigInteger $denominator, bool $checkDenominator)
{
if ($checkDenominator) {
if ($denominator->isZero()) {
throw DivisionByZeroException::denominatorMustNotBeZero();
}
if ($denominator->isNegative()) {
$numerator = $numerator->negated();
$denominator = $denominator->negated();
}
}
$this->numerator = $numerator;
$this->denominator = $denominator;
}
/**
* Creates a BigRational of the given value.
*
* @throws MathException If the value cannot be converted to a BigRational.
*
* @psalm-pure
*/
public static function of(BigNumber|int|float|string $value) : BigRational
{
return parent::of($value)->toBigRational();
}
/**
* Creates a BigRational out of a numerator and a denominator.
*
* If the denominator is negative, the signs of both the numerator and the denominator
* will be inverted to ensure that the denominator is always positive.
*
* @param BigNumber|int|float|string $numerator The numerator. Must be convertible to a BigInteger.
* @param BigNumber|int|float|string $denominator The denominator. Must be convertible to a BigInteger.
*
* @throws NumberFormatException If an argument does not represent a valid number.
* @throws RoundingNecessaryException If an argument represents a non-integer number.
* @throws DivisionByZeroException If the denominator is zero.
*
* @psalm-pure
*/
public static function nd(
BigNumber|int|float|string $numerator,
BigNumber|int|float|string $denominator,
) : BigRational {
$numerator = BigInteger::of($numerator);
$denominator = BigInteger::of($denominator);
return new BigRational($numerator, $denominator, true);
}
/**
* Returns a BigRational representing zero.
*
* @psalm-pure
*/
public static function zero() : BigRational
{
/**
* @psalm-suppress ImpureStaticVariable
* @var BigRational|null $zero
*/
static $zero;
if ($zero === null) {
$zero = new BigRational(BigInteger::zero(), BigInteger::one(), false);
}
return $zero;
}
/**
* Returns a BigRational representing one.
*
* @psalm-pure
*/
public static function one() : BigRational
{
/**
* @psalm-suppress ImpureStaticVariable
* @var BigRational|null $one
*/
static $one;
if ($one === null) {
$one = new BigRational(BigInteger::one(), BigInteger::one(), false);
}
return $one;
}
/**
* Returns a BigRational representing ten.
*
* @psalm-pure
*/
public static function ten() : BigRational
{
/**
* @psalm-suppress ImpureStaticVariable
* @var BigRational|null $ten
*/
static $ten;
if ($ten === null) {
$ten = new BigRational(BigInteger::ten(), BigInteger::one(), false);
}
return $ten;
}
public function getNumerator() : BigInteger
{
return $this->numerator;
}
public function getDenominator() : BigInteger
{
return $this->denominator;
}
/**
* Returns the quotient of the division of the numerator by the denominator.
*/
public function quotient() : BigInteger
{
return $this->numerator->quotient($this->denominator);
}
/**
* Returns the remainder of the division of the numerator by the denominator.
*/
public function remainder() : BigInteger
{
return $this->numerator->remainder($this->denominator);
}
/**
* Returns the quotient and remainder of the division of the numerator by the denominator.
*
* @return BigInteger[]
*/
public function quotientAndRemainder() : array
{
return $this->numerator->quotientAndRemainder($this->denominator);
}
/**
* Returns the sum of this number and the given one.
*
* @param BigNumber|int|float|string $that The number to add.
*
* @throws MathException If the number is not valid.
*/
public function plus(BigNumber|int|float|string $that) : BigRational
{
$that = BigRational::of($that);
$numerator = $this->numerator->multipliedBy($that->denominator);
$numerator = $numerator->plus($that->numerator->multipliedBy($this->denominator));
$denominator = $this->denominator->multipliedBy($that->denominator);
return new BigRational($numerator, $denominator, false);
}
/**
* Returns the difference of this number and the given one.
*
* @param BigNumber|int|float|string $that The number to subtract.
*
* @throws MathException If the number is not valid.
*/
public function minus(BigNumber|int|float|string $that) : BigRational
{
$that = BigRational::of($that);
$numerator = $this->numerator->multipliedBy($that->denominator);
$numerator = $numerator->minus($that->numerator->multipliedBy($this->denominator));
$denominator = $this->denominator->multipliedBy($that->denominator);
return new BigRational($numerator, $denominator, false);
}
/**
* Returns the product of this number and the given one.
*
* @param BigNumber|int|float|string $that The multiplier.
*
* @throws MathException If the multiplier is not a valid number.
*/
public function multipliedBy(BigNumber|int|float|string $that) : BigRational
{
$that = BigRational::of($that);
$numerator = $this->numerator->multipliedBy($that->numerator);
$denominator = $this->denominator->multipliedBy($that->denominator);
return new BigRational($numerator, $denominator, false);
}
/**
* Returns the result of the division of this number by the given one.
*
* @param BigNumber|int|float|string $that The divisor.
*
* @throws MathException If the divisor is not a valid number, or is zero.
*/
public function dividedBy(BigNumber|int|float|string $that) : BigRational
{
$that = BigRational::of($that);
$numerator = $this->numerator->multipliedBy($that->denominator);
$denominator = $this->denominator->multipliedBy($that->numerator);
return new BigRational($numerator, $denominator, true);
}
/**
* Returns this number exponentiated to the given value.
*
* @throws \InvalidArgumentException If the exponent is not in the range 0 to 1,000,000.
*/
public function power(int $exponent) : BigRational
{
if ($exponent === 0) {
$one = BigInteger::one();
return new BigRational($one, $one, false);
}
if ($exponent === 1) {
return $this;
}
return new BigRational(
$this->numerator->power($exponent),
$this->denominator->power($exponent),
false
);
}
/**
* Returns the reciprocal of this BigRational.
*
* The reciprocal has the numerator and denominator swapped.
*
* @throws DivisionByZeroException If the numerator is zero.
*/
public function reciprocal() : BigRational
{
return new BigRational($this->denominator, $this->numerator, true);
}
/**
* Returns the absolute value of this BigRational.
*/
public function abs() : BigRational
{
return new BigRational($this->numerator->abs(), $this->denominator, false);
}
/**
* Returns the negated value of this BigRational.
*/
public function negated() : BigRational
{
return new BigRational($this->numerator->negated(), $this->denominator, false);
}
/**
* Returns the simplified value of this BigRational.
*/
public function simplified() : BigRational
{
$gcd = $this->numerator->gcd($this->denominator);
$numerator = $this->numerator->quotient($gcd);
$denominator = $this->denominator->quotient($gcd);
return new BigRational($numerator, $denominator, false);
}
public function compareTo(BigNumber|int|float|string $that) : int
{
return $this->minus($that)->getSign();
}
public function getSign() : int
{
return $this->numerator->getSign();
}
public function toBigInteger() : BigInteger
{
$simplified = $this->simplified();
if (! $simplified->denominator->isEqualTo(1)) {
throw new RoundingNecessaryException('This rational number cannot be represented as an integer value without rounding.');
}
return $simplified->numerator;
}
public function toBigDecimal() : BigDecimal
{
return $this->numerator->toBigDecimal()->exactlyDividedBy($this->denominator);
}
public function toBigRational() : BigRational
{
return $this;
}
public function toScale(int $scale, int $roundingMode = RoundingMode::UNNECESSARY) : BigDecimal
{
return $this->numerator->toBigDecimal()->dividedBy($this->denominator, $scale, $roundingMode);
}
public function toInt() : int
{
return $this->toBigInteger()->toInt();
}
public function toFloat() : float
{
$simplified = $this->simplified();
return $simplified->numerator->toFloat() / $simplified->denominator->toFloat();
}
public function __toString() : string
{
$numerator = (string) $this->numerator;
$denominator = (string) $this->denominator;
if ($denominator === '1') {
return $numerator;
}
return $this->numerator . '/' . $this->denominator;
}
/**
* This method is required for serializing the object and SHOULD NOT be accessed directly.
*
* @internal
*
* @return array{numerator: BigInteger, denominator: BigInteger}
*/
public function __serialize(): array
{
return ['numerator' => $this->numerator, 'denominator' => $this->denominator];
}
/**
* This method is only here to allow unserializing the object and cannot be accessed directly.
*
* @internal
* @psalm-suppress RedundantPropertyInitializationCheck
*
* @param array{numerator: BigInteger, denominator: BigInteger} $data
*
* @throws \LogicException
*/
public function __unserialize(array $data): void
{
if (isset($this->numerator)) {
throw new \LogicException('__unserialize() is an internal function, it must not be called directly.');
}
$this->numerator = $data['numerator'];
$this->denominator = $data['denominator'];
}
/**
* This method is required by interface Serializable and SHOULD NOT be accessed directly.
*
* @internal
*/
public function serialize() : string
{
return $this->numerator . '/' . $this->denominator;
}
/**
* This method is only here to implement interface Serializable and cannot be accessed directly.
*
* @internal
* @psalm-suppress RedundantPropertyInitializationCheck
*
* @throws \LogicException
*/
public function unserialize($value) : void
{
if (isset($this->numerator)) {
throw new \LogicException('unserialize() is an internal function, it must not be called directly.');
}
[$numerator, $denominator] = \explode('/', $value);
$this->numerator = BigInteger::of($numerator);
$this->denominator = BigInteger::of($denominator);
}
}

View File

@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace Brick\Math\Exception;
/**
* Exception thrown when a division by zero occurs.
*/
class DivisionByZeroException extends MathException
{
/**
* @psalm-pure
*/
public static function divisionByZero() : DivisionByZeroException
{
return new self('Division by zero.');
}
/**
* @psalm-pure
*/
public static function modulusMustNotBeZero() : DivisionByZeroException
{
return new self('The modulus must not be zero.');
}
/**
* @psalm-pure
*/
public static function denominatorMustNotBeZero() : DivisionByZeroException
{
return new self('The denominator of a rational number cannot be zero.');
}
}

View File

@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace Brick\Math\Exception;
use Brick\Math\BigInteger;
/**
* Exception thrown when an integer overflow occurs.
*/
class IntegerOverflowException extends MathException
{
/**
* @psalm-pure
*/
public static function toIntOverflow(BigInteger $value) : IntegerOverflowException
{
$message = '%s is out of range %d to %d and cannot be represented as an integer.';
return new self(\sprintf($message, (string) $value, PHP_INT_MIN, PHP_INT_MAX));
}
}

View File

@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Brick\Math\Exception;
/**
* Base class for all math exceptions.
*/
class MathException extends \Exception
{
}

View File

@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Brick\Math\Exception;
/**
* Exception thrown when attempting to perform an unsupported operation, such as a square root, on a negative number.
*/
class NegativeNumberException extends MathException
{
}

View File

@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace Brick\Math\Exception;
/**
* Exception thrown when attempting to create a number from a string with an invalid format.
*/
class NumberFormatException extends MathException
{
/**
* @param string $char The failing character.
*
* @psalm-pure
*/
public static function charNotInAlphabet(string $char) : self
{
$ord = \ord($char);
if ($ord < 32 || $ord > 126) {
$char = \strtoupper(\dechex($ord));
if ($ord < 10) {
$char = '0' . $char;
}
} else {
$char = '"' . $char . '"';
}
return new self(sprintf('Char %s is not a valid character in the given alphabet.', $char));
}
}

View File

@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace Brick\Math\Exception;
/**
* Exception thrown when a number cannot be represented at the requested scale without rounding.
*/
class RoundingNecessaryException extends MathException
{
/**
* @psalm-pure
*/
public static function roundingNecessary() : RoundingNecessaryException
{
return new self('Rounding is necessary to represent the result of the operation at this scale.');
}
}

View File

@ -0,0 +1,676 @@
<?php
declare(strict_types=1);
namespace Brick\Math\Internal;
use Brick\Math\Exception\RoundingNecessaryException;
use Brick\Math\RoundingMode;
/**
* Performs basic operations on arbitrary size integers.
*
* Unless otherwise specified, all parameters must be validated as non-empty strings of digits,
* without leading zero, and with an optional leading minus sign if the number is not zero.
*
* Any other parameter format will lead to undefined behaviour.
* All methods must return strings respecting this format, unless specified otherwise.
*
* @internal
*
* @psalm-immutable
*/
abstract class Calculator
{
/**
* The maximum exponent value allowed for the pow() method.
*/
public const MAX_POWER = 1000000;
/**
* The alphabet for converting from and to base 2 to 36, lowercase.
*/
public const ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyz';
/**
* The Calculator instance in use.
*/
private static ?Calculator $instance = null;
/**
* Sets the Calculator instance to use.
*
* An instance is typically set only in unit tests: the autodetect is usually the best option.
*
* @param Calculator|null $calculator The calculator instance, or NULL to revert to autodetect.
*/
final public static function set(?Calculator $calculator) : void
{
self::$instance = $calculator;
}
/**
* Returns the Calculator instance to use.
*
* If none has been explicitly set, the fastest available implementation will be returned.
*
* @psalm-pure
* @psalm-suppress ImpureStaticProperty
*/
final public static function get() : Calculator
{
if (self::$instance === null) {
/** @psalm-suppress ImpureMethodCall */
self::$instance = self::detect();
}
return self::$instance;
}
/**
* Returns the fastest available Calculator implementation.
*
* @codeCoverageIgnore
*/
private static function detect() : Calculator
{
if (\extension_loaded('gmp')) {
return new Calculator\GmpCalculator();
}
if (\extension_loaded('bcmath')) {
return new Calculator\BcMathCalculator();
}
return new Calculator\NativeCalculator();
}
/**
* Extracts the sign & digits of the operands.
*
* @return array{bool, bool, string, string} Whether $a and $b are negative, followed by their digits.
*/
final protected function init(string $a, string $b) : array
{
return [
$aNeg = ($a[0] === '-'),
$bNeg = ($b[0] === '-'),
$aNeg ? \substr($a, 1) : $a,
$bNeg ? \substr($b, 1) : $b,
];
}
/**
* Returns the absolute value of a number.
*/
final public function abs(string $n) : string
{
return ($n[0] === '-') ? \substr($n, 1) : $n;
}
/**
* Negates a number.
*/
final public function neg(string $n) : string
{
if ($n === '0') {
return '0';
}
if ($n[0] === '-') {
return \substr($n, 1);
}
return '-' . $n;
}
/**
* Compares two numbers.
*
* @return int [-1, 0, 1] If the first number is less than, equal to, or greater than the second number.
*/
final public function cmp(string $a, string $b) : int
{
[$aNeg, $bNeg, $aDig, $bDig] = $this->init($a, $b);
if ($aNeg && ! $bNeg) {
return -1;
}
if ($bNeg && ! $aNeg) {
return 1;
}
$aLen = \strlen($aDig);
$bLen = \strlen($bDig);
if ($aLen < $bLen) {
$result = -1;
} elseif ($aLen > $bLen) {
$result = 1;
} else {
$result = $aDig <=> $bDig;
}
return $aNeg ? -$result : $result;
}
/**
* Adds two numbers.
*/
abstract public function add(string $a, string $b) : string;
/**
* Subtracts two numbers.
*/
abstract public function sub(string $a, string $b) : string;
/**
* Multiplies two numbers.
*/
abstract public function mul(string $a, string $b) : string;
/**
* Returns the quotient of the division of two numbers.
*
* @param string $a The dividend.
* @param string $b The divisor, must not be zero.
*
* @return string The quotient.
*/
abstract public function divQ(string $a, string $b) : string;
/**
* Returns the remainder of the division of two numbers.
*
* @param string $a The dividend.
* @param string $b The divisor, must not be zero.
*
* @return string The remainder.
*/
abstract public function divR(string $a, string $b) : string;
/**
* Returns the quotient and remainder of the division of two numbers.
*
* @param string $a The dividend.
* @param string $b The divisor, must not be zero.
*
* @return array{string, string} An array containing the quotient and remainder.
*/
abstract public function divQR(string $a, string $b) : array;
/**
* Exponentiates a number.
*
* @param string $a The base number.
* @param int $e The exponent, validated as an integer between 0 and MAX_POWER.
*
* @return string The power.
*/
abstract public function pow(string $a, int $e) : string;
/**
* @param string $b The modulus; must not be zero.
*/
public function mod(string $a, string $b) : string
{
return $this->divR($this->add($this->divR($a, $b), $b), $b);
}
/**
* Returns the modular multiplicative inverse of $x modulo $m.
*
* If $x has no multiplicative inverse mod m, this method must return null.
*
* This method can be overridden by the concrete implementation if the underlying library has built-in support.
*
* @param string $m The modulus; must not be negative or zero.
*/
public function modInverse(string $x, string $m) : ?string
{
if ($m === '1') {
return '0';
}
$modVal = $x;
if ($x[0] === '-' || ($this->cmp($this->abs($x), $m) >= 0)) {
$modVal = $this->mod($x, $m);
}
[$g, $x] = $this->gcdExtended($modVal, $m);
if ($g !== '1') {
return null;
}
return $this->mod($this->add($this->mod($x, $m), $m), $m);
}
/**
* Raises a number into power with modulo.
*
* @param string $base The base number; must be positive or zero.
* @param string $exp The exponent; must be positive or zero.
* @param string $mod The modulus; must be strictly positive.
*/
abstract public function modPow(string $base, string $exp, string $mod) : string;
/**
* Returns the greatest common divisor of the two numbers.
*
* This method can be overridden by the concrete implementation if the underlying library
* has built-in support for GCD calculations.
*
* @return string The GCD, always positive, or zero if both arguments are zero.
*/
public function gcd(string $a, string $b) : string
{
if ($a === '0') {
return $this->abs($b);
}
if ($b === '0') {
return $this->abs($a);
}
return $this->gcd($b, $this->divR($a, $b));
}
/**
* @return array{string, string, string} GCD, X, Y
*/
private function gcdExtended(string $a, string $b) : array
{
if ($a === '0') {
return [$b, '0', '1'];
}
[$gcd, $x1, $y1] = $this->gcdExtended($this->mod($b, $a), $a);
$x = $this->sub($y1, $this->mul($this->divQ($b, $a), $x1));
$y = $x1;
return [$gcd, $x, $y];
}
/**
* Returns the square root of the given number, rounded down.
*
* The result is the largest x such that x² ≤ n.
* The input MUST NOT be negative.
*/
abstract public function sqrt(string $n) : string;
/**
* Converts a number from an arbitrary base.
*
* This method can be overridden by the concrete implementation if the underlying library
* has built-in support for base conversion.
*
* @param string $number The number, positive or zero, non-empty, case-insensitively validated for the given base.
* @param int $base The base of the number, validated from 2 to 36.
*
* @return string The converted number, following the Calculator conventions.
*/
public function fromBase(string $number, int $base) : string
{
return $this->fromArbitraryBase(\strtolower($number), self::ALPHABET, $base);
}
/**
* Converts a number to an arbitrary base.
*
* This method can be overridden by the concrete implementation if the underlying library
* has built-in support for base conversion.
*
* @param string $number The number to convert, following the Calculator conventions.
* @param int $base The base to convert to, validated from 2 to 36.
*
* @return string The converted number, lowercase.
*/
public function toBase(string $number, int $base) : string
{
$negative = ($number[0] === '-');
if ($negative) {
$number = \substr($number, 1);
}
$number = $this->toArbitraryBase($number, self::ALPHABET, $base);
if ($negative) {
return '-' . $number;
}
return $number;
}
/**
* Converts a non-negative number in an arbitrary base using a custom alphabet, to base 10.
*
* @param string $number The number to convert, validated as a non-empty string,
* containing only chars in the given alphabet/base.
* @param string $alphabet The alphabet that contains every digit, validated as 2 chars minimum.
* @param int $base The base of the number, validated from 2 to alphabet length.
*
* @return string The number in base 10, following the Calculator conventions.
*/
final public function fromArbitraryBase(string $number, string $alphabet, int $base) : string
{
// remove leading "zeros"
$number = \ltrim($number, $alphabet[0]);
if ($number === '') {
return '0';
}
// optimize for "one"
if ($number === $alphabet[1]) {
return '1';
}
$result = '0';
$power = '1';
$base = (string) $base;
for ($i = \strlen($number) - 1; $i >= 0; $i--) {
$index = \strpos($alphabet, $number[$i]);
if ($index !== 0) {
$result = $this->add($result, ($index === 1)
? $power
: $this->mul($power, (string) $index)
);
}
if ($i !== 0) {
$power = $this->mul($power, $base);
}
}
return $result;
}
/**
* Converts a non-negative number to an arbitrary base using a custom alphabet.
*
* @param string $number The number to convert, positive or zero, following the Calculator conventions.
* @param string $alphabet The alphabet that contains every digit, validated as 2 chars minimum.
* @param int $base The base to convert to, validated from 2 to alphabet length.
*
* @return string The converted number in the given alphabet.
*/
final public function toArbitraryBase(string $number, string $alphabet, int $base) : string
{
if ($number === '0') {
return $alphabet[0];
}
$base = (string) $base;
$result = '';
while ($number !== '0') {
[$number, $remainder] = $this->divQR($number, $base);
$remainder = (int) $remainder;
$result .= $alphabet[$remainder];
}
return \strrev($result);
}
/**
* Performs a rounded division.
*
* Rounding is performed when the remainder of the division is not zero.
*
* @param string $a The dividend.
* @param string $b The divisor, must not be zero.
* @param int $roundingMode The rounding mode.
*
* @throws \InvalidArgumentException If the rounding mode is invalid.
* @throws RoundingNecessaryException If RoundingMode::UNNECESSARY is provided but rounding is necessary.
*
* @psalm-suppress ImpureFunctionCall
*/
final public function divRound(string $a, string $b, int $roundingMode) : string
{
[$quotient, $remainder] = $this->divQR($a, $b);
$hasDiscardedFraction = ($remainder !== '0');
$isPositiveOrZero = ($a[0] === '-') === ($b[0] === '-');
$discardedFractionSign = function() use ($remainder, $b) : int {
$r = $this->abs($this->mul($remainder, '2'));
$b = $this->abs($b);
return $this->cmp($r, $b);
};
$increment = false;
switch ($roundingMode) {
case RoundingMode::UNNECESSARY:
if ($hasDiscardedFraction) {
throw RoundingNecessaryException::roundingNecessary();
}
break;
case RoundingMode::UP:
$increment = $hasDiscardedFraction;
break;
case RoundingMode::DOWN:
break;
case RoundingMode::CEILING:
$increment = $hasDiscardedFraction && $isPositiveOrZero;
break;
case RoundingMode::FLOOR:
$increment = $hasDiscardedFraction && ! $isPositiveOrZero;
break;
case RoundingMode::HALF_UP:
$increment = $discardedFractionSign() >= 0;
break;
case RoundingMode::HALF_DOWN:
$increment = $discardedFractionSign() > 0;
break;
case RoundingMode::HALF_CEILING:
$increment = $isPositiveOrZero ? $discardedFractionSign() >= 0 : $discardedFractionSign() > 0;
break;
case RoundingMode::HALF_FLOOR:
$increment = $isPositiveOrZero ? $discardedFractionSign() > 0 : $discardedFractionSign() >= 0;
break;
case RoundingMode::HALF_EVEN:
$lastDigit = (int) $quotient[-1];
$lastDigitIsEven = ($lastDigit % 2 === 0);
$increment = $lastDigitIsEven ? $discardedFractionSign() > 0 : $discardedFractionSign() >= 0;
break;
default:
throw new \InvalidArgumentException('Invalid rounding mode.');
}
if ($increment) {
return $this->add($quotient, $isPositiveOrZero ? '1' : '-1');
}
return $quotient;
}
/**
* Calculates bitwise AND of two numbers.
*
* This method can be overridden by the concrete implementation if the underlying library
* has built-in support for bitwise operations.
*/
public function and(string $a, string $b) : string
{
return $this->bitwise('and', $a, $b);
}
/**
* Calculates bitwise OR of two numbers.
*
* This method can be overridden by the concrete implementation if the underlying library
* has built-in support for bitwise operations.
*/
public function or(string $a, string $b) : string
{
return $this->bitwise('or', $a, $b);
}
/**
* Calculates bitwise XOR of two numbers.
*
* This method can be overridden by the concrete implementation if the underlying library
* has built-in support for bitwise operations.
*/
public function xor(string $a, string $b) : string
{
return $this->bitwise('xor', $a, $b);
}
/**
* Performs a bitwise operation on a decimal number.
*
* @param 'and'|'or'|'xor' $operator The operator to use.
* @param string $a The left operand.
* @param string $b The right operand.
*/
private function bitwise(string $operator, string $a, string $b) : string
{
[$aNeg, $bNeg, $aDig, $bDig] = $this->init($a, $b);
$aBin = $this->toBinary($aDig);
$bBin = $this->toBinary($bDig);
$aLen = \strlen($aBin);
$bLen = \strlen($bBin);
if ($aLen > $bLen) {
$bBin = \str_repeat("\x00", $aLen - $bLen) . $bBin;
} elseif ($bLen > $aLen) {
$aBin = \str_repeat("\x00", $bLen - $aLen) . $aBin;
}
if ($aNeg) {
$aBin = $this->twosComplement($aBin);
}
if ($bNeg) {
$bBin = $this->twosComplement($bBin);
}
switch ($operator) {
case 'and':
$value = $aBin & $bBin;
$negative = ($aNeg and $bNeg);
break;
case 'or':
$value = $aBin | $bBin;
$negative = ($aNeg or $bNeg);
break;
case 'xor':
$value = $aBin ^ $bBin;
$negative = ($aNeg xor $bNeg);
break;
// @codeCoverageIgnoreStart
default:
throw new \InvalidArgumentException('Invalid bitwise operator.');
// @codeCoverageIgnoreEnd
}
if ($negative) {
$value = $this->twosComplement($value);
}
$result = $this->toDecimal($value);
return $negative ? $this->neg($result) : $result;
}
/**
* @param string $number A positive, binary number.
*/
private function twosComplement(string $number) : string
{
$xor = \str_repeat("\xff", \strlen($number));
$number ^= $xor;
for ($i = \strlen($number) - 1; $i >= 0; $i--) {
$byte = \ord($number[$i]);
if (++$byte !== 256) {
$number[$i] = \chr($byte);
break;
}
$number[$i] = "\x00";
if ($i === 0) {
$number = "\x01" . $number;
}
}
return $number;
}
/**
* Converts a decimal number to a binary string.
*
* @param string $number The number to convert, positive or zero, only digits.
*/
private function toBinary(string $number) : string
{
$result = '';
while ($number !== '0') {
[$number, $remainder] = $this->divQR($number, '256');
$result .= \chr((int) $remainder);
}
return \strrev($result);
}
/**
* Returns the positive decimal representation of a binary number.
*
* @param string $bytes The bytes representing the number.
*/
private function toDecimal(string $bytes) : string
{
$result = '0';
$power = '1';
for ($i = \strlen($bytes) - 1; $i >= 0; $i--) {
$index = \ord($bytes[$i]);
if ($index !== 0) {
$result = $this->add($result, ($index === 1)
? $power
: $this->mul($power, (string) $index)
);
}
if ($i !== 0) {
$power = $this->mul($power, '256');
}
}
return $result;
}
}

View File

@ -0,0 +1,75 @@
<?php
declare(strict_types=1);
namespace Brick\Math\Internal\Calculator;
use Brick\Math\Internal\Calculator;
/**
* Calculator implementation built around the bcmath library.
*
* @internal
*
* @psalm-immutable
*/
class BcMathCalculator extends Calculator
{
public function add(string $a, string $b) : string
{
return \bcadd($a, $b, 0);
}
public function sub(string $a, string $b) : string
{
return \bcsub($a, $b, 0);
}
public function mul(string $a, string $b) : string
{
return \bcmul($a, $b, 0);
}
public function divQ(string $a, string $b) : string
{
return \bcdiv($a, $b, 0);
}
/**
* @psalm-suppress InvalidNullableReturnType
* @psalm-suppress NullableReturnStatement
*/
public function divR(string $a, string $b) : string
{
return \bcmod($a, $b, 0);
}
public function divQR(string $a, string $b) : array
{
$q = \bcdiv($a, $b, 0);
$r = \bcmod($a, $b, 0);
assert($r !== null);
return [$q, $r];
}
public function pow(string $a, int $e) : string
{
return \bcpow($a, (string) $e, 0);
}
public function modPow(string $base, string $exp, string $mod) : string
{
return \bcpowmod($base, $exp, $mod, 0);
}
/**
* @psalm-suppress InvalidNullableReturnType
* @psalm-suppress NullableReturnStatement
*/
public function sqrt(string $n) : string
{
return \bcsqrt($n, 0);
}
}

View File

@ -0,0 +1,108 @@
<?php
declare(strict_types=1);
namespace Brick\Math\Internal\Calculator;
use Brick\Math\Internal\Calculator;
/**
* Calculator implementation built around the GMP library.
*
* @internal
*
* @psalm-immutable
*/
class GmpCalculator extends Calculator
{
public function add(string $a, string $b) : string
{
return \gmp_strval(\gmp_add($a, $b));
}
public function sub(string $a, string $b) : string
{
return \gmp_strval(\gmp_sub($a, $b));
}
public function mul(string $a, string $b) : string
{
return \gmp_strval(\gmp_mul($a, $b));
}
public function divQ(string $a, string $b) : string
{
return \gmp_strval(\gmp_div_q($a, $b));
}
public function divR(string $a, string $b) : string
{
return \gmp_strval(\gmp_div_r($a, $b));
}
public function divQR(string $a, string $b) : array
{
[$q, $r] = \gmp_div_qr($a, $b);
return [
\gmp_strval($q),
\gmp_strval($r)
];
}
public function pow(string $a, int $e) : string
{
return \gmp_strval(\gmp_pow($a, $e));
}
public function modInverse(string $x, string $m) : ?string
{
$result = \gmp_invert($x, $m);
if ($result === false) {
return null;
}
return \gmp_strval($result);
}
public function modPow(string $base, string $exp, string $mod) : string
{
return \gmp_strval(\gmp_powm($base, $exp, $mod));
}
public function gcd(string $a, string $b) : string
{
return \gmp_strval(\gmp_gcd($a, $b));
}
public function fromBase(string $number, int $base) : string
{
return \gmp_strval(\gmp_init($number, $base));
}
public function toBase(string $number, int $base) : string
{
return \gmp_strval($number, $base);
}
public function and(string $a, string $b) : string
{
return \gmp_strval(\gmp_and($a, $b));
}
public function or(string $a, string $b) : string
{
return \gmp_strval(\gmp_or($a, $b));
}
public function xor(string $a, string $b) : string
{
return \gmp_strval(\gmp_xor($a, $b));
}
public function sqrt(string $n) : string
{
return \gmp_strval(\gmp_sqrt($n));
}
}

View File

@ -0,0 +1,581 @@
<?php
declare(strict_types=1);
namespace Brick\Math\Internal\Calculator;
use Brick\Math\Internal\Calculator;
/**
* Calculator implementation using only native PHP code.
*
* @internal
*
* @psalm-immutable
*/
class NativeCalculator extends Calculator
{
/**
* The max number of digits the platform can natively add, subtract, multiply or divide without overflow.
* For multiplication, this represents the max sum of the lengths of both operands.
*
* In addition, it is assumed that an extra digit can hold a carry (1) without overflowing.
* Example: 32-bit: max number 1,999,999,999 (9 digits + carry)
* 64-bit: max number 1,999,999,999,999,999,999 (18 digits + carry)
*/
private int $maxDigits;
/**
* @codeCoverageIgnore
*/
public function __construct()
{
switch (PHP_INT_SIZE) {
case 4:
$this->maxDigits = 9;
break;
case 8:
$this->maxDigits = 18;
break;
default:
throw new \RuntimeException('The platform is not 32-bit or 64-bit as expected.');
}
}
public function add(string $a, string $b) : string
{
/**
* @psalm-var numeric-string $a
* @psalm-var numeric-string $b
*/
$result = $a + $b;
if (is_int($result)) {
return (string) $result;
}
if ($a === '0') {
return $b;
}
if ($b === '0') {
return $a;
}
[$aNeg, $bNeg, $aDig, $bDig] = $this->init($a, $b);
$result = $aNeg === $bNeg ? $this->doAdd($aDig, $bDig) : $this->doSub($aDig, $bDig);
if ($aNeg) {
$result = $this->neg($result);
}
return $result;
}
public function sub(string $a, string $b) : string
{
return $this->add($a, $this->neg($b));
}
public function mul(string $a, string $b) : string
{
/**
* @psalm-var numeric-string $a
* @psalm-var numeric-string $b
*/
$result = $a * $b;
if (is_int($result)) {
return (string) $result;
}
if ($a === '0' || $b === '0') {
return '0';
}
if ($a === '1') {
return $b;
}
if ($b === '1') {
return $a;
}
if ($a === '-1') {
return $this->neg($b);
}
if ($b === '-1') {
return $this->neg($a);
}
[$aNeg, $bNeg, $aDig, $bDig] = $this->init($a, $b);
$result = $this->doMul($aDig, $bDig);
if ($aNeg !== $bNeg) {
$result = $this->neg($result);
}
return $result;
}
public function divQ(string $a, string $b) : string
{
return $this->divQR($a, $b)[0];
}
public function divR(string $a, string $b): string
{
return $this->divQR($a, $b)[1];
}
public function divQR(string $a, string $b) : array
{
if ($a === '0') {
return ['0', '0'];
}
if ($a === $b) {
return ['1', '0'];
}
if ($b === '1') {
return [$a, '0'];
}
if ($b === '-1') {
return [$this->neg($a), '0'];
}
/** @psalm-var numeric-string $a */
$na = $a * 1; // cast to number
if (is_int($na)) {
/** @psalm-var numeric-string $b */
$nb = $b * 1;
if (is_int($nb)) {
// the only division that may overflow is PHP_INT_MIN / -1,
// which cannot happen here as we've already handled a divisor of -1 above.
$r = $na % $nb;
$q = ($na - $r) / $nb;
assert(is_int($q));
return [
(string) $q,
(string) $r
];
}
}
[$aNeg, $bNeg, $aDig, $bDig] = $this->init($a, $b);
[$q, $r] = $this->doDiv($aDig, $bDig);
if ($aNeg !== $bNeg) {
$q = $this->neg($q);
}
if ($aNeg) {
$r = $this->neg($r);
}
return [$q, $r];
}
public function pow(string $a, int $e) : string
{
if ($e === 0) {
return '1';
}
if ($e === 1) {
return $a;
}
$odd = $e % 2;
$e -= $odd;
$aa = $this->mul($a, $a);
/** @psalm-suppress PossiblyInvalidArgument We're sure that $e / 2 is an int now */
$result = $this->pow($aa, $e / 2);
if ($odd === 1) {
$result = $this->mul($result, $a);
}
return $result;
}
/**
* Algorithm from: https://www.geeksforgeeks.org/modular-exponentiation-power-in-modular-arithmetic/
*/
public function modPow(string $base, string $exp, string $mod) : string
{
// special case: the algorithm below fails with 0 power 0 mod 1 (returns 1 instead of 0)
if ($base === '0' && $exp === '0' && $mod === '1') {
return '0';
}
// special case: the algorithm below fails with power 0 mod 1 (returns 1 instead of 0)
if ($exp === '0' && $mod === '1') {
return '0';
}
$x = $base;
$res = '1';
// numbers are positive, so we can use remainder instead of modulo
$x = $this->divR($x, $mod);
while ($exp !== '0') {
if (in_array($exp[-1], ['1', '3', '5', '7', '9'])) { // odd
$res = $this->divR($this->mul($res, $x), $mod);
}
$exp = $this->divQ($exp, '2');
$x = $this->divR($this->mul($x, $x), $mod);
}
return $res;
}
/**
* Adapted from https://cp-algorithms.com/num_methods/roots_newton.html
*/
public function sqrt(string $n) : string
{
if ($n === '0') {
return '0';
}
// initial approximation
$x = \str_repeat('9', \intdiv(\strlen($n), 2) ?: 1);
$decreased = false;
for (;;) {
$nx = $this->divQ($this->add($x, $this->divQ($n, $x)), '2');
if ($x === $nx || $this->cmp($nx, $x) > 0 && $decreased) {
break;
}
$decreased = $this->cmp($nx, $x) < 0;
$x = $nx;
}
return $x;
}
/**
* Performs the addition of two non-signed large integers.
*/
private function doAdd(string $a, string $b) : string
{
[$a, $b, $length] = $this->pad($a, $b);
$carry = 0;
$result = '';
for ($i = $length - $this->maxDigits;; $i -= $this->maxDigits) {
$blockLength = $this->maxDigits;
if ($i < 0) {
$blockLength += $i;
/** @psalm-suppress LoopInvalidation */
$i = 0;
}
/** @psalm-var numeric-string $blockA */
$blockA = \substr($a, $i, $blockLength);
/** @psalm-var numeric-string $blockB */
$blockB = \substr($b, $i, $blockLength);
$sum = (string) ($blockA + $blockB + $carry);
$sumLength = \strlen($sum);
if ($sumLength > $blockLength) {
$sum = \substr($sum, 1);
$carry = 1;
} else {
if ($sumLength < $blockLength) {
$sum = \str_repeat('0', $blockLength - $sumLength) . $sum;
}
$carry = 0;
}
$result = $sum . $result;
if ($i === 0) {
break;
}
}
if ($carry === 1) {
$result = '1' . $result;
}
return $result;
}
/**
* Performs the subtraction of two non-signed large integers.
*/
private function doSub(string $a, string $b) : string
{
if ($a === $b) {
return '0';
}
// Ensure that we always subtract to a positive result: biggest minus smallest.
$cmp = $this->doCmp($a, $b);
$invert = ($cmp === -1);
if ($invert) {
$c = $a;
$a = $b;
$b = $c;
}
[$a, $b, $length] = $this->pad($a, $b);
$carry = 0;
$result = '';
$complement = 10 ** $this->maxDigits;
for ($i = $length - $this->maxDigits;; $i -= $this->maxDigits) {
$blockLength = $this->maxDigits;
if ($i < 0) {
$blockLength += $i;
/** @psalm-suppress LoopInvalidation */
$i = 0;
}
/** @psalm-var numeric-string $blockA */
$blockA = \substr($a, $i, $blockLength);
/** @psalm-var numeric-string $blockB */
$blockB = \substr($b, $i, $blockLength);
$sum = $blockA - $blockB - $carry;
if ($sum < 0) {
$sum += $complement;
$carry = 1;
} else {
$carry = 0;
}
$sum = (string) $sum;
$sumLength = \strlen($sum);
if ($sumLength < $blockLength) {
$sum = \str_repeat('0', $blockLength - $sumLength) . $sum;
}
$result = $sum . $result;
if ($i === 0) {
break;
}
}
// Carry cannot be 1 when the loop ends, as a > b
assert($carry === 0);
$result = \ltrim($result, '0');
if ($invert) {
$result = $this->neg($result);
}
return $result;
}
/**
* Performs the multiplication of two non-signed large integers.
*/
private function doMul(string $a, string $b) : string
{
$x = \strlen($a);
$y = \strlen($b);
$maxDigits = \intdiv($this->maxDigits, 2);
$complement = 10 ** $maxDigits;
$result = '0';
for ($i = $x - $maxDigits;; $i -= $maxDigits) {
$blockALength = $maxDigits;
if ($i < 0) {
$blockALength += $i;
/** @psalm-suppress LoopInvalidation */
$i = 0;
}
$blockA = (int) \substr($a, $i, $blockALength);
$line = '';
$carry = 0;
for ($j = $y - $maxDigits;; $j -= $maxDigits) {
$blockBLength = $maxDigits;
if ($j < 0) {
$blockBLength += $j;
/** @psalm-suppress LoopInvalidation */
$j = 0;
}
$blockB = (int) \substr($b, $j, $blockBLength);
$mul = $blockA * $blockB + $carry;
$value = $mul % $complement;
$carry = ($mul - $value) / $complement;
$value = (string) $value;
$value = \str_pad($value, $maxDigits, '0', STR_PAD_LEFT);
$line = $value . $line;
if ($j === 0) {
break;
}
}
if ($carry !== 0) {
$line = $carry . $line;
}
$line = \ltrim($line, '0');
if ($line !== '') {
$line .= \str_repeat('0', $x - $blockALength - $i);
$result = $this->add($result, $line);
}
if ($i === 0) {
break;
}
}
return $result;
}
/**
* Performs the division of two non-signed large integers.
*
* @return string[] The quotient and remainder.
*/
private function doDiv(string $a, string $b) : array
{
$cmp = $this->doCmp($a, $b);
if ($cmp === -1) {
return ['0', $a];
}
$x = \strlen($a);
$y = \strlen($b);
// we now know that a >= b && x >= y
$q = '0'; // quotient
$r = $a; // remainder
$z = $y; // focus length, always $y or $y+1
for (;;) {
$focus = \substr($a, 0, $z);
$cmp = $this->doCmp($focus, $b);
if ($cmp === -1) {
if ($z === $x) { // remainder < dividend
break;
}
$z++;
}
$zeros = \str_repeat('0', $x - $z);
$q = $this->add($q, '1' . $zeros);
$a = $this->sub($a, $b . $zeros);
$r = $a;
if ($r === '0') { // remainder == 0
break;
}
$x = \strlen($a);
if ($x < $y) { // remainder < dividend
break;
}
$z = $y;
}
return [$q, $r];
}
/**
* Compares two non-signed large numbers.
*
* @return int [-1, 0, 1]
*/
private function doCmp(string $a, string $b) : int
{
$x = \strlen($a);
$y = \strlen($b);
$cmp = $x <=> $y;
if ($cmp !== 0) {
return $cmp;
}
return \strcmp($a, $b) <=> 0; // enforce [-1, 0, 1]
}
/**
* Pads the left of one of the given numbers with zeros if necessary to make both numbers the same length.
*
* The numbers must only consist of digits, without leading minus sign.
*
* @return array{string, string, int}
*/
private function pad(string $a, string $b) : array
{
$x = \strlen($a);
$y = \strlen($b);
if ($x > $y) {
$b = \str_repeat('0', $x - $y) . $b;
return [$a, $b, $x];
}
if ($x < $y) {
$a = \str_repeat('0', $y - $x) . $a;
return [$a, $b, $y];
}
return [$a, $b, $x];
}
}

20
libs/Brick/Math11/LICENSE Normal file
View File

@ -0,0 +1,20 @@
The MIT License (MIT)
Copyright (c) 2013-present Benjamin Morel
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -0,0 +1,107 @@
<?php
declare(strict_types=1);
namespace Brick\Math;
/**
* Specifies a rounding behavior for numerical operations capable of discarding precision.
*
* Each rounding mode indicates how the least significant returned digit of a rounded result
* is to be calculated. If fewer digits are returned than the digits needed to represent the
* exact numerical result, the discarded digits will be referred to as the discarded fraction
* regardless the digits' contribution to the value of the number. In other words, considered
* as a numerical value, the discarded fraction could have an absolute value greater than one.
*/
final class RoundingMode
{
/**
* Private constructor. This class is not instantiable.
*
* @codeCoverageIgnore
*/
private function __construct()
{
}
/**
* Asserts that the requested operation has an exact result, hence no rounding is necessary.
*
* If this rounding mode is specified on an operation that yields a result that
* cannot be represented at the requested scale, a RoundingNecessaryException is thrown.
*/
public const UNNECESSARY = 0;
/**
* Rounds away from zero.
*
* Always increments the digit prior to a nonzero discarded fraction.
* Note that this rounding mode never decreases the magnitude of the calculated value.
*/
public const UP = 1;
/**
* Rounds towards zero.
*
* Never increments the digit prior to a discarded fraction (i.e., truncates).
* Note that this rounding mode never increases the magnitude of the calculated value.
*/
public const DOWN = 2;
/**
* Rounds towards positive infinity.
*
* If the result is positive, behaves as for UP; if negative, behaves as for DOWN.
* Note that this rounding mode never decreases the calculated value.
*/
public const CEILING = 3;
/**
* Rounds towards negative infinity.
*
* If the result is positive, behave as for DOWN; if negative, behave as for UP.
* Note that this rounding mode never increases the calculated value.
*/
public const FLOOR = 4;
/**
* Rounds towards "nearest neighbor" unless both neighbors are equidistant, in which case round up.
*
* Behaves as for UP if the discarded fraction is >= 0.5; otherwise, behaves as for DOWN.
* Note that this is the rounding mode commonly taught at school.
*/
public const HALF_UP = 5;
/**
* Rounds towards "nearest neighbor" unless both neighbors are equidistant, in which case round down.
*
* Behaves as for UP if the discarded fraction is > 0.5; otherwise, behaves as for DOWN.
*/
public const HALF_DOWN = 6;
/**
* Rounds towards "nearest neighbor" unless both neighbors are equidistant, in which case round towards positive infinity.
*
* If the result is positive, behaves as for HALF_UP; if negative, behaves as for HALF_DOWN.
*/
public const HALF_CEILING = 7;
/**
* Rounds towards "nearest neighbor" unless both neighbors are equidistant, in which case round towards negative infinity.
*
* If the result is positive, behaves as for HALF_DOWN; if negative, behaves as for HALF_UP.
*/
public const HALF_FLOOR = 8;
/**
* Rounds towards the "nearest neighbor" unless both neighbors are equidistant, in which case rounds towards the even neighbor.
*
* Behaves as for HALF_UP if the digit to the left of the discarded fraction is odd;
* behaves as for HALF_DOWN if it's even.
*
* Note that this is the rounding mode that statistically minimizes
* cumulative error when applied repeatedly over a sequence of calculations.
* It is sometimes known as "Banker's rounding", and is chiefly used in the USA.
*/
public const HALF_EVEN = 9;
}

View File

@ -5,11 +5,12 @@ declare(strict_types=1);
namespace Doctrine\SqlFormatter;
use function sprintf;
use const PHP_EOL;
final class CliHighlighter implements Highlighter
{
const HIGHLIGHT_FUNCTIONS = 'functions';
public const HIGHLIGHT_FUNCTIONS = 'functions';
/** @var array<string, string> */
private $escapeSequences;
@ -33,9 +34,9 @@ final class CliHighlighter implements Highlighter
];
}
public function highlightToken(int $type, string $value) : string
public function highlightToken(int $type, string $value): string
{
if ($type === Token::TOKEN_TYPE_BOUNDARY && ($value==='(' || $value===')')) {
if ($type === Token::TOKEN_TYPE_BOUNDARY && ($value === '(' || $value === ')')) {
return $value;
}
@ -47,7 +48,7 @@ final class CliHighlighter implements Highlighter
return $prefix . $value . "\x1b[0m";
}
private function prefix(int $type)
private function prefix(int $type): ?string
{
if (! isset(self::TOKEN_TYPE_TO_HIGHLIGHT[$type])) {
return null;
@ -56,7 +57,7 @@ final class CliHighlighter implements Highlighter
return $this->escapeSequences[self::TOKEN_TYPE_TO_HIGHLIGHT[$type]];
}
public function highlightError(string $value) : string
public function highlightError(string $value): string
{
return sprintf(
'%s%s%s%s',
@ -67,12 +68,12 @@ final class CliHighlighter implements Highlighter
);
}
public function highlightErrorMessage(string $value) : string
public function highlightErrorMessage(string $value): string
{
return $this->highlightError($value);
}
public function output(string $string) : string
public function output(string $string): string
{
return $string . "\n";
}

View File

@ -20,7 +20,7 @@ final class Cursor
$this->tokens = $tokens;
}
public function next(int $exceptTokenType = null)
public function next(?int $exceptTokenType = null): ?Token
{
while ($token = $this->tokens[++$this->position] ?? null) {
if ($exceptTokenType !== null && $token->isOfType($exceptTokenType)) {
@ -33,7 +33,7 @@ final class Cursor
return null;
}
public function previous(int $exceptTokenType = null)
public function previous(?int $exceptTokenType = null): ?Token
{
while ($token = $this->tokens[--$this->position] ?? null) {
if ($exceptTokenType !== null && $token->isOfType($exceptTokenType)) {
@ -46,7 +46,7 @@ final class Cursor
return null;
}
public function subCursor() : self
public function subCursor(): self
{
$cursor = new self($this->tokens);
$cursor->position = $this->position;

View File

@ -6,7 +6,7 @@ namespace Doctrine\SqlFormatter;
interface Highlighter
{
const TOKEN_TYPE_TO_HIGHLIGHT = [
public const TOKEN_TYPE_TO_HIGHLIGHT = [
Token::TOKEN_TYPE_BOUNDARY => self::HIGHLIGHT_BOUNDARY,
Token::TOKEN_TYPE_WORD => self::HIGHLIGHT_WORD,
Token::TOKEN_TYPE_BACKTICK_QUOTE => self::HIGHLIGHT_BACKTICK_QUOTE,
@ -20,30 +20,30 @@ interface Highlighter
Token::TOKEN_TYPE_BLOCK_COMMENT => self::HIGHLIGHT_COMMENT,
];
const HIGHLIGHT_BOUNDARY = 'boundary';
const HIGHLIGHT_WORD = 'word';
const HIGHLIGHT_BACKTICK_QUOTE = 'backtickQuote';
const HIGHLIGHT_QUOTE = 'quote';
const HIGHLIGHT_RESERVED = 'reserved';
const HIGHLIGHT_NUMBER = 'number';
const HIGHLIGHT_VARIABLE = 'variable';
const HIGHLIGHT_COMMENT = 'comment';
const HIGHLIGHT_ERROR = 'error';
public const HIGHLIGHT_BOUNDARY = 'boundary';
public const HIGHLIGHT_WORD = 'word';
public const HIGHLIGHT_BACKTICK_QUOTE = 'backtickQuote';
public const HIGHLIGHT_QUOTE = 'quote';
public const HIGHLIGHT_RESERVED = 'reserved';
public const HIGHLIGHT_NUMBER = 'number';
public const HIGHLIGHT_VARIABLE = 'variable';
public const HIGHLIGHT_COMMENT = 'comment';
public const HIGHLIGHT_ERROR = 'error';
/**
* Highlights a token depending on its type.
*/
public function highlightToken(int $type, string $value) : string;
public function highlightToken(int $type, string $value): string;
/**
* Highlights a token which causes an issue
*/
public function highlightError(string $value) : string;
public function highlightError(string $value): string;
/**
* Highlights an error message
*/
public function highlightErrorMessage(string $value) : string;
public function highlightErrorMessage(string $value): string;
/**
* Helper function for building string output
@ -52,5 +52,5 @@ interface Highlighter
*
* @return string The quoted string
*/
public function output(string $string) : string;
public function output(string $string): string;
}

View File

@ -7,13 +7,14 @@ namespace Doctrine\SqlFormatter;
use function htmlentities;
use function sprintf;
use function trim;
use const ENT_COMPAT;
use const ENT_IGNORE;
use const PHP_EOL;
final class HtmlHighlighter implements Highlighter
{
const HIGHLIGHT_PRE = 'pre';
public const HIGHLIGHT_PRE = 'pre';
/**
* This flag tells us if queries need to be enclosed in <pre> tags
@ -45,11 +46,11 @@ final class HtmlHighlighter implements Highlighter
$this->usePre = $usePre;
}
public function highlightToken(int $type, string $value) : string
public function highlightToken(int $type, string $value): string
{
$value = htmlentities($value, ENT_COMPAT | ENT_IGNORE, 'UTF-8');
if ($type === Token::TOKEN_TYPE_BOUNDARY && ($value==='(' || $value===')')) {
if ($type === Token::TOKEN_TYPE_BOUNDARY && ($value === '(' || $value === ')')) {
return $value;
}
@ -61,7 +62,7 @@ final class HtmlHighlighter implements Highlighter
return '<span ' . $attributes . '>' . $value . '</span>';
}
public function attributes(int $type)
public function attributes(int $type): ?string
{
if (! isset(self::TOKEN_TYPE_TO_HIGHLIGHT[$type])) {
return null;
@ -70,7 +71,7 @@ final class HtmlHighlighter implements Highlighter
return $this->htmlAttributes[self::TOKEN_TYPE_TO_HIGHLIGHT[$type]];
}
public function highlightError(string $value) : string
public function highlightError(string $value): string
{
return sprintf(
'%s<span %s>%s</span>',
@ -80,16 +81,16 @@ final class HtmlHighlighter implements Highlighter
);
}
public function highlightErrorMessage(string $value) : string
public function highlightErrorMessage(string $value): string
{
return $this->highlightError($value);
}
public function output(string $string) : string
public function output(string $string): string
{
$string =trim($string);
$string = trim($string);
// This is derp truncate for long list
// Added by Observium Developers. IN list truncated for a long list
$string = preg_replace('!(IN</span>\s*)(\()([^\)]+)(\))!', '$1$2<div class="text-truncate" onclick="revealHiddenOverflow(this)">$3</div>$4', $string);
if (! $this->usePre) {

View File

@ -6,22 +6,22 @@ namespace Doctrine\SqlFormatter;
final class NullHighlighter implements Highlighter
{
public function highlightToken(int $type, string $value) : string
public function highlightToken(int $type, string $value): string
{
return $value;
}
public function highlightError(string $value) : string
public function highlightError(string $value): string
{
return $value;
}
public function highlightErrorMessage(string $value) : string
public function highlightErrorMessage(string $value): string
{
return ' ' . $value;
}
public function output(string $string) : string
public function output(string $string): string
{
return $string;
}

View File

@ -23,6 +23,7 @@ use function str_repeat;
use function str_replace;
use function strlen;
use function trim;
use const PHP_SAPI;
final class SqlFormatter
@ -33,7 +34,7 @@ final class SqlFormatter
/** @var Tokenizer */
private $tokenizer;
public function __construct(Highlighter $highlighter = null)
public function __construct(?Highlighter $highlighter = null)
{
$this->tokenizer = new Tokenizer();
$this->highlighter = $highlighter ?? (PHP_SAPI === 'cli' ? new CliHighlighter() : new HtmlHighlighter());
@ -46,7 +47,7 @@ final class SqlFormatter
*
* @return string The SQL string with HTML styles and formatting wrapped in a <pre> tag
*/
public function format(string $string, string $indentString = ' ') : string
public function format(string $string, string $indentString = ' '): string
{
// This variable will be populated with formatted html
$return = '';
@ -148,7 +149,7 @@ final class SqlFormatter
// Allow up to 3 non-whitespace tokens inside inline parentheses
$length = 0;
$subCursor = $cursor->subCursor();
for ($j=1; $j<=250; $j++) {
for ($j = 1; $j <= 250; $j++) {
// Reached end of string
$next = $subCursor->next(Token::TOKEN_TYPE_WHITESPACE);
if (! $next) {
@ -164,17 +165,19 @@ final class SqlFormatter
}
// Reached an invalid token for inline parentheses
if ($next->value()===';' || $next->value()==='(') {
if ($next->value() === ';' || $next->value() === '(') {
break;
}
// Reached an invalid token type for inline parentheses
if ($next->isOfType(
Token::TOKEN_TYPE_RESERVED_TOPLEVEL,
Token::TOKEN_TYPE_RESERVED_NEWLINE,
Token::TOKEN_TYPE_COMMENT,
Token::TOKEN_TYPE_BLOCK_COMMENT
)) {
if (
$next->isOfType(
Token::TOKEN_TYPE_RESERVED_TOPLEVEL,
Token::TOKEN_TYPE_RESERVED_NEWLINE,
Token::TOKEN_TYPE_COMMENT,
Token::TOKEN_TYPE_BLOCK_COMMENT
)
) {
break;
}
@ -206,8 +209,8 @@ final class SqlFormatter
$indentLevel--;
// Reset indent level
while ($j=array_shift($indentTypes)) {
if ($j!=='special') {
while ($j = array_shift($indentTypes)) {
if ($j !== 'special') {
break;
}
@ -232,7 +235,7 @@ final class SqlFormatter
// If the last indent type was 'special', decrease the special indent for this round
reset($indentTypes);
if (current($indentTypes)==='special') {
if (current($indentTypes) === 'special') {
$indentLevel--;
array_shift($indentTypes);
}
@ -256,9 +259,11 @@ final class SqlFormatter
if ($token->value() === 'LIMIT' && ! $inlineParentheses) {
$clauseLimit = true;
}
} elseif ($clauseLimit &&
} elseif (
$clauseLimit &&
$token->value() !== ',' &&
! $token->isOfType(Token::TOKEN_TYPE_NUMBER, Token::TOKEN_TYPE_WHITESPACE)) {
! $token->isOfType(Token::TOKEN_TYPE_NUMBER, Token::TOKEN_TYPE_WHITESPACE)
) {
// Checks if we are out of the limit clause
$clauseLimit = false;
} elseif ($token->value() === ',' && ! $inlineParentheses) {
@ -294,9 +299,11 @@ final class SqlFormatter
}
// If the token shouldn't have a space before it
if ($token->value() === '.' ||
if (
$token->value() === '.' ||
$token->value() === ',' ||
$token->value() === ';') {
$token->value() === ';'
) {
$return = rtrim($return, ' ');
}
@ -322,12 +329,14 @@ final class SqlFormatter
continue;
}
if ($prev->isOfType(
Token::TOKEN_TYPE_QUOTE,
Token::TOKEN_TYPE_BACKTICK_QUOTE,
Token::TOKEN_TYPE_WORD,
Token::TOKEN_TYPE_NUMBER
)) {
if (
$prev->isOfType(
Token::TOKEN_TYPE_QUOTE,
Token::TOKEN_TYPE_BACKTICK_QUOTE,
Token::TOKEN_TYPE_WORD,
Token::TOKEN_TYPE_NUMBER
)
) {
continue;
}
@ -355,7 +364,7 @@ final class SqlFormatter
*
* @return string The SQL string with HTML styles applied
*/
public function highlight(string $string) : string
public function highlight(string $string): string
{
$cursor = $this->tokenizer->tokenize($string);
@ -378,7 +387,7 @@ final class SqlFormatter
*
* @return string The SQL string without comments
*/
public function compress(string $string) : string
public function compress(string $string): string
{
$result = '';
$cursor = $this->tokenizer->tokenize($string);
@ -392,11 +401,13 @@ final class SqlFormatter
// Remove extra whitespace in reserved words (e.g "OUTER JOIN" becomes "OUTER JOIN")
if ($token->isOfType(
Token::TOKEN_TYPE_RESERVED,
Token::TOKEN_TYPE_RESERVED_NEWLINE,
Token::TOKEN_TYPE_RESERVED_TOPLEVEL
)) {
if (
$token->isOfType(
Token::TOKEN_TYPE_RESERVED,
Token::TOKEN_TYPE_RESERVED_NEWLINE,
Token::TOKEN_TYPE_RESERVED_TOPLEVEL
)
) {
$newValue = preg_replace('/\s+/', ' ', $token->value());
assert($newValue !== null);
$token = $token->withValue($newValue);

View File

@ -10,23 +10,23 @@ use function strpos;
final class Token
{
// Constants for token types
const TOKEN_TYPE_WHITESPACE = 0;
const TOKEN_TYPE_WORD = 1;
const TOKEN_TYPE_QUOTE = 2;
const TOKEN_TYPE_BACKTICK_QUOTE = 3;
const TOKEN_TYPE_RESERVED = 4;
const TOKEN_TYPE_RESERVED_TOPLEVEL = 5;
const TOKEN_TYPE_RESERVED_NEWLINE = 6;
const TOKEN_TYPE_BOUNDARY = 7;
const TOKEN_TYPE_COMMENT = 8;
const TOKEN_TYPE_BLOCK_COMMENT = 9;
const TOKEN_TYPE_NUMBER = 10;
const TOKEN_TYPE_ERROR = 11;
const TOKEN_TYPE_VARIABLE = 12;
public const TOKEN_TYPE_WHITESPACE = 0;
public const TOKEN_TYPE_WORD = 1;
public const TOKEN_TYPE_QUOTE = 2;
public const TOKEN_TYPE_BACKTICK_QUOTE = 3;
public const TOKEN_TYPE_RESERVED = 4;
public const TOKEN_TYPE_RESERVED_TOPLEVEL = 5;
public const TOKEN_TYPE_RESERVED_NEWLINE = 6;
public const TOKEN_TYPE_BOUNDARY = 7;
public const TOKEN_TYPE_COMMENT = 8;
public const TOKEN_TYPE_BLOCK_COMMENT = 9;
public const TOKEN_TYPE_NUMBER = 10;
public const TOKEN_TYPE_ERROR = 11;
public const TOKEN_TYPE_VARIABLE = 12;
// Constants for different components of a token
const TOKEN_TYPE = 0;
const TOKEN_VALUE = 1;
public const TOKEN_TYPE = 0;
public const TOKEN_VALUE = 1;
/** @var int */
private $type;
@ -40,29 +40,29 @@ final class Token
$this->value = $value;
}
public function value() : string
public function value(): string
{
return $this->value;
}
public function type() : int
public function type(): int
{
return $this->type;
}
public function isOfType(int ...$types) : bool
public function isOfType(int ...$types): bool
{
return in_array($this->type, $types, true);
}
public function hasExtraWhitespace() : bool
public function hasExtraWhitespace(): bool
{
return strpos($this->value(), ' ')!== false ||
return strpos($this->value(), ' ') !== false ||
strpos($this->value(), "\n") !== false ||
strpos($this->value(), "\t") !== false;
}
public function withValue(string $value) : self
public function withValue(string $value): self
{
return new self($this->type(), $value);
}

View File

@ -70,6 +70,7 @@ final class Tokenizer
'CONVERT',
'CREATE',
'CROSS',
'CURRENT ROW',
'CURRENT_TIMESTAMP',
'DATABASE',
'DATABASES',
@ -108,11 +109,13 @@ final class Tokenizer
'FAST',
'FIELDS',
'FILE',
'FILTER',
'FIRST',
'FIXED',
'FLUSH',
'FOR',
'FORCE',
'FOLLOWING',
'FOREIGN',
'FULL',
'FULLTEXT',
@ -120,7 +123,8 @@ final class Tokenizer
'GLOBAL',
'GRANT',
'GRANTS',
'GROUP_CONCAT',
'GROUP',
'GROUPS',
'HEAP',
'HIGH_PRIORITY',
'HOSTS',
@ -180,6 +184,7 @@ final class Tokenizer
'MYISAM',
'NAMES',
'NATURAL',
'NO OTHERS',
'NOT',
'NOW()',
'NULL',
@ -192,12 +197,14 @@ final class Tokenizer
'ON UPDATE',
'ON DELETE',
'OUTFILE',
'OVER',
'PACK_KEYS',
'PAGE',
'PARTIAL',
'PARTITION',
'PARTITIONS',
'PASSWORD',
'PRECEDING',
'PRIMARY',
'PRIVILEGES',
'PROCEDURE',
@ -213,6 +220,7 @@ final class Tokenizer
'READ',
'READ_ONLY',
'READ_WRITE',
'RECURSIVE',
'REFERENCES',
'REGEXP',
'RELOAD',
@ -277,6 +285,7 @@ final class Tokenizer
'TEMPORARY',
'TERMINATED',
'THEN',
'TIES',
'TO',
'TRAILING',
'TRANSACTIONAL',
@ -284,6 +293,7 @@ final class Tokenizer
'TRUNCATE',
'TYPE',
'TYPES',
'UNBOUNDED',
'UNCOMMITTED',
'UNIQUE',
'UNLOCK',
@ -307,6 +317,7 @@ final class Tokenizer
* @var string[]
*/
private $reservedToplevel = [
'WITH',
'SELECT',
'FROM',
'WHERE',
@ -327,6 +338,11 @@ final class Tokenizer
'UNION',
'EXCEPT',
'INTERSECT',
'PARTITION BY',
'ROWS',
'RANGE',
'GROUPS',
'WINDOW',
];
/** @var string[] */
@ -341,6 +357,7 @@ final class Tokenizer
'XOR',
'OR',
'AND',
'EXCLUDE',
];
/** @var string[] */
@ -351,6 +368,7 @@ final class Tokenizer
'ADDTIME',
'AES_DECRYPT',
'AES_ENCRYPT',
'APPROX_COUNT_DISTINCT',
'AREA',
'ASBINARY',
'ASCII',
@ -380,6 +398,7 @@ final class Tokenizer
'CHARACTER_LENGTH',
'CHARSET',
'CHAR_LENGTH',
'CHECKSUM_AGG',
'COALESCE',
'COERCIBILITY',
'COLLATION',
@ -395,8 +414,10 @@ final class Tokenizer
'COS',
'COT',
'COUNT',
'COUNT_BIG',
'CRC32',
'CROSSES',
'CUME_DIST',
'CURDATE',
'CURRENT_DATE',
'CURRENT_TIME',
@ -418,6 +439,7 @@ final class Tokenizer
'DECODE',
'DEFAULT',
'DEGREES',
'DENSE_RANK',
'DES_DECRYPT',
'DES_ENCRYPT',
'DIFFERENCE',
@ -437,6 +459,7 @@ final class Tokenizer
'EXTRACTVALUE',
'FIELD',
'FIND_IN_SET',
'FIRST_VALUE',
'FLOOR',
'FORMAT',
'FOUND_ROWS',
@ -457,6 +480,8 @@ final class Tokenizer
'GET_LOCK',
'GLENGTH',
'GREATEST',
'GROUPING',
'GROUPING_ID',
'GROUP_CONCAT',
'GROUP_UNIQUE_USERS',
'HEX',
@ -478,9 +503,12 @@ final class Tokenizer
'ISSIMPLE',
'IS_FREE_LOCK',
'IS_USED_LOCK',
'LAG',
'LAST_DAY',
'LAST_INSERT_ID',
'LAST_VALUE',
'LCASE',
'LEAD',
'LEAST',
'LEFT',
'LENGTH',
@ -489,6 +517,7 @@ final class Tokenizer
'LINESTRING',
'LINESTRINGFROMTEXT',
'LINESTRINGFROMWKB',
'LISTAGG',
'LN',
'LOAD_FILE',
'LOCALTIME',
@ -536,6 +565,8 @@ final class Tokenizer
'MULTIPOLYGONFROMTEXT',
'MULTIPOLYGONFROMWKB',
'NAME_CONST',
'NTH_VALUE',
'NTILE',
'NULLIF',
'NUMGEOMETRIES',
'NUMINTERIORRINGS',
@ -546,6 +577,9 @@ final class Tokenizer
'ORD',
'OVERLAPS',
'PASSWORD',
'PERCENT_RANK',
'PERCENTILE_CONT',
'PERCENTILE_DISC',
'PERIOD_ADD',
'PERIOD_DIFF',
'PI',
@ -566,6 +600,7 @@ final class Tokenizer
'QUOTE',
'RADIANS',
'RAND',
'RANK',
'RELATED',
'RELEASE_LOCK',
'REPEAT',
@ -574,6 +609,7 @@ final class Tokenizer
'RIGHT',
'ROUND',
'ROW_COUNT',
'ROW_NUMBER',
'RPAD',
'RTRIM',
'SCHEMA',
@ -591,9 +627,12 @@ final class Tokenizer
'SRID',
'STARTPOINT',
'STD',
'STDEV',
'STDEVP',
'STDDEV',
'STDDEV_POP',
'STDDEV_SAMP',
'STRING_AGG',
'STRCMP',
'STR_TO_DATE',
'SUBDATE',
@ -630,7 +669,9 @@ final class Tokenizer
'UTC_TIME',
'UTC_TIMESTAMP',
'UUID',
'VAR',
'VARIANCE',
'VARP',
'VAR_POP',
'VAR_SAMP',
'VERSION',
@ -669,6 +710,7 @@ final class Tokenizer
private $boundaries = [
',',
';',
'::', // PostgreSQL cast operator
':',
')',
'(',
@ -727,7 +769,7 @@ final class Tokenizer
*
* @param string $string The SQL string
*/
public function tokenize(string $string) : Cursor
public function tokenize(string $string): Cursor
{
$tokens = [];
@ -774,7 +816,7 @@ final class Tokenizer
*
* @return Token An associative array containing the type and value of the token.
*/
private function createNextToken(string $string, Token $previous = null) : Token
private function createNextToken(string $string, ?Token $previous = null): Token
{
$matches = [];
// Whitespace
@ -783,9 +825,11 @@ final class Tokenizer
}
// Comment
if ($string[0] === '#' ||
(isset($string[1]) && ($string[0]==='-' && $string[1]==='-') ||
(isset($string[1]) && $string[0]==='/' && $string[1]==='*'))) {
if (
$string[0] === '#' ||
(isset($string[1]) && (($string[0] === '-' && $string[1] === '-') ||
($string[0] === '/' && $string[1] === '*')))
) {
// Comment until end of line
if ($string[0] === '-' || $string[0] === '#') {
$last = strpos($string, "\n");
@ -805,9 +849,9 @@ final class Tokenizer
}
// Quoted String
if ($string[0]==='"' || $string[0]==='\'' || $string[0]==='`' || $string[0]==='[') {
if ($string[0] === '"' || $string[0] === '\'' || $string[0] === '`' || $string[0] === '[') {
return new Token(
($string[0]==='`' || $string[0]==='['
($string[0] === '`' || $string[0] === '['
? Token::TOKEN_TYPE_BACKTICK_QUOTE
: Token::TOKEN_TYPE_QUOTE),
$this->getQuotedString($string)
@ -820,7 +864,7 @@ final class Tokenizer
$type = Token::TOKEN_TYPE_VARIABLE;
// If the variable name is quoted
if ($string[1]==='"' || $string[1]==='\'' || $string[1]==='`') {
if ($string[1] === '"' || $string[1] === '\'' || $string[1] === '`') {
$value = $string[0] . $this->getQuotedString(substr($string, 1));
} else {
// Non-quoted variable name
@ -836,11 +880,13 @@ final class Tokenizer
}
// Number (decimal, binary, or hex)
if (preg_match(
'/^([0-9]+(\.[0-9]+)?|0x[0-9a-fA-F]+|0b[01]+)($|\s|"\'`|' . $this->regexBoundaries . ')/',
$string,
$matches
)) {
if (
preg_match(
'/^([0-9]+(\.[0-9]+)?|0x[0-9a-fA-F]+|0b[01]+)($|\s|"\'`|' . $this->regexBoundaries . ')/',
$string,
$matches
)
) {
return new Token(Token::TOKEN_TYPE_NUMBER, $matches[1]);
}
@ -854,38 +900,44 @@ final class Tokenizer
if (! $previous || $previous->value() !== '.') {
$upper = strtoupper($string);
// Top Level Reserved Word
if (preg_match(
'/^(' . $this->regexReservedToplevel . ')($|\s|' . $this->regexBoundaries . ')/',
$upper,
$matches
)) {
if (
preg_match(
'/^(' . $this->regexReservedToplevel . ')($|\s|' . $this->regexBoundaries . ')/',
$upper,
$matches
)
) {
return new Token(
Token::TOKEN_TYPE_RESERVED_TOPLEVEL,
substr($string, 0, strlen($matches[1]))
substr($upper, 0, strlen($matches[1]))
);
}
// Newline Reserved Word
if (preg_match(
'/^(' . $this->regexReservedNewline . ')($|\s|' . $this->regexBoundaries . ')/',
$upper,
$matches
)) {
if (
preg_match(
'/^(' . $this->regexReservedNewline . ')($|\s|' . $this->regexBoundaries . ')/',
$upper,
$matches
)
) {
return new Token(
Token::TOKEN_TYPE_RESERVED_NEWLINE,
substr($string, 0, strlen($matches[1]))
substr($upper, 0, strlen($matches[1]))
);
}
// Other Reserved Word
if (preg_match(
'/^(' . $this->regexReserved . ')($|\s|' . $this->regexBoundaries . ')/',
$upper,
$matches
)) {
if (
preg_match(
'/^(' . $this->regexReserved . ')($|\s|' . $this->regexBoundaries . ')/',
$upper,
$matches
)
) {
return new Token(
Token::TOKEN_TYPE_RESERVED,
substr($string, 0, strlen($matches[1]))
substr($upper, 0, strlen($matches[1]))
);
}
}
@ -897,7 +949,7 @@ final class Tokenizer
if (preg_match('/^(' . $this->regexFunction . '[(]|\s|[)])/', $upper, $matches)) {
return new Token(
Token::TOKEN_TYPE_RESERVED,
substr($string, 0, strlen($matches[1])-1)
substr($upper, 0, strlen($matches[1]) - 1)
);
}
@ -914,14 +966,14 @@ final class Tokenizer
*
* @return string[] The quoted strings
*/
private function quoteRegex(array $strings) : array
private function quoteRegex(array $strings): array
{
return array_map(static function (string $string) : string {
return array_map(static function (string $string): string {
return preg_quote($string, '/');
}, $strings);
}
private function getQuotedString(string $string) : string
private function getQuotedString(string $string): string
{
$ret = '';
@ -930,14 +982,16 @@ final class Tokenizer
// 2. square bracket quoted string (SQL Server) using ]] to escape
// 3. double quoted string using "" or \" to escape
// 4. single quoted string using '' or \' to escape
if (preg_match(
'/^(((`[^`]*($|`))+)|
if (
preg_match(
'/^(((`[^`]*($|`))+)|
((\[[^\]]*($|\]))(\][^\]]*($|\]))*)|
(("[^"\\\\]*(?:\\\\.[^"\\\\]*)*("|$))+)|
((\'[^\'\\\\]*(?:\\\\.[^\'\\\\]*)*(\'|$))+))/sx',
$string,
$matches
)) {
$string,
$matches
)
) {
$ret = $matches[1];
}

View File

@ -1,5 +1,15 @@
# CHANGELOG
## 0.7.0 (2017-05-23)
- [MAJOR]: [PR #46](https://github.com/fabiang/xmpp/pull/46) Added support for password-protected chatrooms
- [MAJOR]: [PR #44](https://github.com/fabiang/xmpp/pull/44) Added anonymous authentication method
- [MAJOR]: [PR #34](https://github.com/fabiang/xmpp/pull/34) Added support for registereing user
- [MAJOR]: [PR #34](https://github.com/fabiang/xmpp/pull/34) Added vCard support
- [MAJOR]: [PR #34](https://github.com/fabiang/xmpp/pull/34) Added support for blocking and unblocking an user
- [MAJOR]: Drop support for PHP lower than 5.6
- [MAJOR]: [PR #31](https://github.com/fabiang/xmpp/pull/31): Possibility to set context for SocketClient
## 0.6.1 (2014-11-20)
- [PATCH] [Issue #4](https://github.com/fabiang/xmpp/issues/4): Incomplete buffer response

View File

@ -84,7 +84,7 @@ abstract class AbstractConnection implements ConnectionInterface
*
* @var EventListenerInterface[]
*/
protected $listeners = array();
protected $listeners = [];
/**
* Connected.
@ -259,7 +259,7 @@ abstract class AbstractConnection implements ConnectionInterface
*/
protected function log($message, $level = LogLevel::DEBUG)
{
$this->getEventManager()->trigger('logger', $this, array($message, $level));
$this->getEventManager()->trigger('logger', $this, [$message, $level]);
}
/**

View File

@ -89,7 +89,7 @@ XML;
*/
public static function factory(Options $options)
{
$socket = new SocketClient($options->getAddress());
$socket = new SocketClient($options->getAddress(), $options->getContextOptions());
$object = new static($socket);
$object->setOptions($options);
return $object;
@ -129,7 +129,7 @@ XML;
// check if we didn't receive any data
// if not we re-try to connect via TLS
if (false === $this->receivedAnyData) {
$matches = array();
$matches = [];
$previousAddress = $this->getOptions()->getAddress();
// only reconnect via tls if we've used tcp before.
if (preg_match('#tcp://(?<address>.+)#', $previousAddress, $matches)) {

View File

@ -66,14 +66,14 @@ class Event implements EventInterface
*
* @var array
*/
protected $parameters = array();
protected $parameters = [];
/**
* Event stack.
*
* @var array
*/
protected $eventStack = array();
protected $eventStack = [];
/**
* {@inheritDoc}

View File

@ -55,7 +55,7 @@ class EventManager implements EventManagerInterface
*
* @var array
*/
protected $events = array(self::WILDCARD => array());
protected $events = [self::WILDCARD => []];
/**
* Event object.
@ -90,7 +90,7 @@ class EventManager implements EventManagerInterface
}
if (!isset($this->events[$event])) {
$this->events[$event] = array();
$this->events[$event] = [];
}
if (!in_array($callback, $this->events[$event], true)) {
@ -107,13 +107,13 @@ class EventManager implements EventManagerInterface
return;
}
$events = array();
$events = [];
if (!empty($this->events[$event])) {
$events = $this->events[$event];
}
$callbacks = array_merge($events, $this->events[self::WILDCARD]);
$previous = array();
$previous = [];
$eventObject = clone $this->getEventObject();
$eventObject->setName($event);

View File

@ -69,6 +69,6 @@ class Logger extends AbstractEventListener
*/
public function attachEvents()
{
$this->getEventManager()->attach('logger', array($this, 'event'));
$this->getEventManager()->attach('logger', [$this, 'event']);
}
}

View File

@ -79,7 +79,8 @@ abstract class AbstractSessionEvent extends AbstractEventListener
$this->blocking = true;
$this->getConnection()->send(sprintf(
$data,
$this->getId()
$this->getId(),
$this->getOptions()->getResource()
));
}
}

View File

@ -63,7 +63,7 @@ class Authentication extends AbstractEventListener implements BlockingEventListe
*
* @var array
*/
protected $mechanisms = array();
protected $mechanisms = [];
/**
* {@inheritDoc}
@ -71,10 +71,10 @@ class Authentication extends AbstractEventListener implements BlockingEventListe
public function attachEvents()
{
$input = $this->getConnection()->getInputStream()->getEventManager();
$input->attach('{urn:ietf:params:xml:ns:xmpp-sasl}mechanisms', array($this, 'authenticate'));
$input->attach('{urn:ietf:params:xml:ns:xmpp-sasl}mechanism', array($this, 'collectMechanisms'));
$input->attach('{urn:ietf:params:xml:ns:xmpp-sasl}failure', array($this, 'failure'));
$input->attach('{urn:ietf:params:xml:ns:xmpp-sasl}success', array($this, 'success'));
$input->attach('{urn:ietf:params:xml:ns:xmpp-sasl}mechanisms', [$this, 'authenticate']);
$input->attach('{urn:ietf:params:xml:ns:xmpp-sasl}mechanism', [$this, 'collectMechanisms']);
$input->attach('{urn:ietf:params:xml:ns:xmpp-sasl}failure', [$this, 'failure']);
$input->attach('{urn:ietf:params:xml:ns:xmpp-sasl}success', [$this, 'success']);
}
/**

View File

@ -0,0 +1,66 @@
<?php
/**
* Copyright 2016 Fabian Grutschus. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. 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.
*
* 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.
*
* The views and conclusions contained in the software and documentation are those
* of the authors and should not be interpreted as representing official policies,
* either expressed or implied, of the copyright holders.
*
* @author Fabian Grutschus <f.grutschus@lubyte.de>
* @copyright 2016 Fabian Grutschus. All rights reserved.
* @license BSD
* @link http://github.com/fabiang/xmpp
*/
namespace Fabiang\Xmpp\EventListener\Stream\Authentication;
use Fabiang\Xmpp\EventListener\AbstractEventListener;
/**
* Handler for "anonymous" authentication mechanism.
*
* @package Xmpp\EventListener\Authentication
*/
class Anonymous extends AbstractEventListener implements AuthenticationInterface
{
/**
* {@inheritDoc}
*/
public function attachEvents()
{
}
/**
* {@inheritDoc}
*/
public function authenticate($username, $password)
{
$this->getConnection()->send(
'<auth xmlns="urn:ietf:params:xml:ns:xmpp-sasl" mechanism="ANONYMOUS"/>'
);
}
}

View File

@ -74,11 +74,11 @@ class DigestMd5 extends AbstractEventListener implements AuthenticationInterface
public function attachEvents()
{
$input = $this->getInputEventManager();
$input->attach('{urn:ietf:params:xml:ns:xmpp-sasl}challenge', array($this, 'challenge'));
$input->attach('{urn:ietf:params:xml:ns:xmpp-sasl}success', array($this, 'success'));
$input->attach('{urn:ietf:params:xml:ns:xmpp-sasl}challenge', [$this, 'challenge']);
$input->attach('{urn:ietf:params:xml:ns:xmpp-sasl}success', [$this, 'success']);
$output = $this->getOutputEventManager();
$output->attach('{urn:ietf:params:xml:ns:xmpp-sasl}auth', array($this, 'auth'));
$output->attach('{urn:ietf:params:xml:ns:xmpp-sasl}auth', [$this, 'auth']);
}
/**
@ -183,10 +183,10 @@ class DigestMd5 extends AbstractEventListener implements AuthenticationInterface
protected function parseCallenge($challenge)
{
if (!$challenge) {
return array();
return [];
}
$matches = array();
$matches = [];
preg_match_all('#(\w+)\=(?:"([^"]+)"|([^,]+))#', $challenge, $matches);
list(, $variables, $quoted, $unquoted) = $matches;
// filter empty strings; preserve keys

View File

@ -53,8 +53,8 @@ class Bind extends AbstractSessionEvent implements BlockingEventListenerInterfac
public function attachEvents()
{
$input = $this->getInputEventManager();
$input->attach('{urn:ietf:params:xml:ns:xmpp-bind}bind', array($this, 'bindFeatures'));
$input->attach('{urn:ietf:params:xml:ns:xmpp-bind}jid', array($this, 'jid'));
$input->attach('{urn:ietf:params:xml:ns:xmpp-bind}bind', [$this, 'bindFeatures']);
$input->attach('{urn:ietf:params:xml:ns:xmpp-bind}jid', [$this, 'jid']);
}
/**
@ -67,7 +67,7 @@ class Bind extends AbstractSessionEvent implements BlockingEventListenerInterfac
{
$this->respondeToFeatures(
$event,
'<iq type="set" id="%s"><bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"/></iq>'
'<iq type="set" id="%s"><bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"><resource>%s</resource></bind></iq>'
);
}

View File

@ -0,0 +1,144 @@
<?php
/**
* Copyright 2014 Fabian Grutschus. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. 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.
*
* 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.
*
* The views and conclusions contained in the software and documentation are those
* of the authors and should not be interpreted as representing official policies,
* either expressed or implied, of the copyright holders.
*
* @author Fabian Grutschus <f.grutschus@lubyte.de>
* @copyright 2014 Fabian Grutschus. All rights reserved.
* @license BSD
* @link http://github.com/fabiang/xmpp
*/
namespace Fabiang\Xmpp\EventListener\Stream;
use Fabiang\Xmpp\Event\XMLEvent;
use Fabiang\Xmpp\EventListener\AbstractEventListener;
use Fabiang\Xmpp\EventListener\BlockingEventListenerInterface;
use Fabiang\Xmpp\Protocol\User\User;
/**
* Listener
*
* @package Xmpp\EventListener
*/
class BlockedUsers extends AbstractEventListener implements BlockingEventListenerInterface
{
/**
* Blocking.
*
* @var boolean
*/
protected $blocking = false;
/**
* user object.
*
* @var User
*/
protected $userObject;
/**
* {@inheritDoc}
*/
public function attachEvents()
{
$this->getOutputEventManager()
->attach('{urn:xmpp:blocking}blocklist', [$this, 'query']);
$this->getInputEventManager()
->attach('{urn:xmpp:blocking}blocklist', [$this, 'result']);
}
/**
* Sending a query request for roster sets listener to blocking mode.
*
* @return void
*/
public function query()
{
$this->blocking = true;
}
/**
* Result received.
*
* @param \Fabiang\Xmpp\Event\XMLEvent $event
* @return void
*/
public function result(XMLEvent $event)
{
if ($event->isEndTag()) {
$users = [];
/* @var $element \DOMElement */
$element = $event->getParameter(0);
$items = $element->getElementsByTagName('item');
/* @var $item \DOMElement */
foreach ($items as $item) {
$users[] = $item->getAttribute('jid');
}
dd($users);
//$this->getOptions()->setUsers($users);
$this->blocking = false;
}
}
/**
* Get user object.
*
* @return User
*/
public function getUserObject()
{
if (null === $this->userObject) {
$this->setUserObject(new User);
}
return $this->userObject;
}
/**
* Set user object.
*
* @param User $userObject
* @return $this
*/
public function setUserObject(User $userObject)
{
$this->userObject = $userObject;
return $this;
}
/**
* {@inheritDoc}
*/
public function isBlocking()
{
return $this->blocking;
}
}

View File

@ -0,0 +1,111 @@
<?php
/**
* Copyright 2014 Fabian Grutschus. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. 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.
*
* 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.
*
* The views and conclusions contained in the software and documentation are those
* of the authors and should not be interpreted as representing official policies,
* either expressed or implied, of the copyright holders.
*
* @author Fabian Grutschus <f.grutschus@lubyte.de>
* @copyright 2014 Fabian Grutschus. All rights reserved.
* @license BSD
* @link http://github.com/fabiang/xmpp
*/
namespace Fabiang\Xmpp\EventListener\Stream;
use Fabiang\Xmpp\Event\XMLEvent;
use Fabiang\Xmpp\EventListener\AbstractEventListener;
use Fabiang\Xmpp\EventListener\BlockingEventListenerInterface;
use Fabiang\Xmpp\Protocol\User\User;
/**
* Listener
*
* @package Xmpp\EventListener
*/
class Register extends AbstractEventListener implements BlockingEventListenerInterface
{
/**
* Blocking.
*
* @var boolean
*/
protected $blocking = false;
/**
* user object.
*
* @var User
*/
protected $userObject;
/**
* {@inheritDoc}
*/
public function attachEvents()
{
$this->getOutputEventManager()
->attach('{http://jabber.org/protocol/commands}command', [$this, 'query']);
$this->getInputEventManager()
->attach('{http://jabber.org/protocol/commands}command', [$this, 'result']);
}
/**
* Sending a query request for roster sets listener to blocking mode.
*
* @return void
*/
public function query()
{
$this->blocking = true;
}
/**
* Result received.
*
* @param \Fabiang\Xmpp\Event\XMLEvent $event
* @return void
*/
public function result(XMLEvent $event)
{
if ($event->isEndTag()) {
/* @var $element \DOMElement */
$sid = $event->getParameter(0)->getAttribute('sessionid');
$this->getOptions()->setSid($sid);
$this->blocking = false;
}
}
/**
* {@inheritDoc}
*/
public function isBlocking()
{
return $this->blocking;
}
}

View File

@ -69,9 +69,9 @@ class Roster extends AbstractEventListener implements BlockingEventListenerInter
public function attachEvents()
{
$this->getOutputEventManager()
->attach('{jabber:iq:roster}query', array($this, 'query'));
->attach('{jabber:iq:roster}query', [$this, 'query']);
$this->getInputEventManager()
->attach('{jabber:iq:roster}query', array($this, 'result'));
->attach('{jabber:iq:roster}query', [$this, 'result']);
}
/**
@ -93,7 +93,7 @@ class Roster extends AbstractEventListener implements BlockingEventListenerInter
public function result(XMLEvent $event)
{
if ($event->isEndTag()) {
$users = array();
$users = [];
/* @var $element \DOMElement */
$element = $event->getParameter(0);

View File

@ -53,8 +53,8 @@ class Session extends AbstractSessionEvent implements BlockingEventListenerInter
public function attachEvents()
{
$input = $this->getInputEventManager();
$input->attach('{urn:ietf:params:xml:ns:xmpp-session}session', array($this, 'sessionStart'));
$input->attach('{jabber:client}iq', array($this, 'iq'));
$input->attach('{urn:ietf:params:xml:ns:xmpp-session}session', [$this, 'sessionStart']);
$input->attach('{jabber:client}iq', [$this, 'iq']);
}
/**

View File

@ -62,8 +62,8 @@ class StartTls extends AbstractEventListener implements BlockingEventListenerInt
public function attachEvents()
{
$input = $this->getInputEventManager();
$input->attach('{urn:ietf:params:xml:ns:xmpp-tls}starttls', array($this, 'starttlsEvent'));
$input->attach('{urn:ietf:params:xml:ns:xmpp-tls}proceed', array($this, 'proceed'));
$input->attach('{urn:ietf:params:xml:ns:xmpp-tls}starttls', [$this, 'starttlsEvent']);
$input->attach('{urn:ietf:params:xml:ns:xmpp-tls}proceed', [$this, 'proceed']);
}
/**

View File

@ -61,11 +61,11 @@ class Stream extends AbstractEventListener implements BlockingEventListenerInter
public function attachEvents()
{
$this->getOutputEventManager()
->attach('{http://etherx.jabber.org/streams}stream', array($this, 'streamStart'));
->attach('{http://etherx.jabber.org/streams}stream', [$this, 'streamStart']);
$input = $this->getInputEventManager();
$input->attach('{http://etherx.jabber.org/streams}stream', array($this, 'streamServer'));
$input->attach('{http://etherx.jabber.org/streams}features', array($this, 'features'));
$input->attach('{http://etherx.jabber.org/streams}stream', [$this, 'streamServer']);
$input->attach('{http://etherx.jabber.org/streams}features', [$this, 'features']);
}
/**

View File

@ -55,7 +55,7 @@ class StreamError extends AbstractEventListener
{
$this->getInputEventManager()->attach(
'{http://etherx.jabber.org/streams}error',
array($this, 'error')
[$this, 'error']
);
}

View File

@ -1,7 +1,7 @@
Simplified BSD License
======================
Copyright 2014 Fabian Grutschus.
Copyright 2014-2017 Fabian Grutschus.
All rights reserved.
Redistribution and use in source and binary forms, with or without modification,

View File

@ -99,6 +99,12 @@ class Options
*/
protected $jid;
/**
*
* @var string
*/
protected $sid;
/**
*
* @var boolean
@ -109,7 +115,7 @@ class Options
*
* @var array
*/
protected $users = array();
protected $users = [];
/**
* Timeout for connection.
@ -123,10 +129,20 @@ class Options
*
* @var array
*/
protected $authenticationClasses = array(
protected $authenticationClasses = [
'digest-md5' => '\\Fabiang\\Xmpp\\EventListener\\Stream\\Authentication\\DigestMd5',
'plain' => '\\Fabiang\\Xmpp\\EventListener\\Stream\\Authentication\\Plain'
);
'plain' => '\\Fabiang\\Xmpp\\EventListener\\Stream\\Authentication\\Plain',
'anonymous' => '\\Fabiang\\Xmpp\\EventListener\\Stream\\Authentication\\Anonymous'
];
/**
* Options used to create a stream context
*
* @var array
*/
protected $contextOptions = [];
/**
* Constructor.
@ -283,6 +299,18 @@ class Options
return $this;
}
/**
* Get resource.
*
* @return string
*/
public function getResource()
{
$username = $this->getUsername();
$username = explode('/', $username);
return isset($username[1]) ? $username[1] : '';
}
/**
* Get password.
*
@ -327,6 +355,28 @@ class Options
return $this;
}
/**
* Get users jid.
*
* @return string
*/
public function getSid()
{
return $this->sid;
}
/**
* Set users jid.
*
* @param string $jid
* @return $this
*/
public function setSid($sid)
{
$this->sid = (string) $sid;
return $this;
}
/**
* Is user authenticated.
*
@ -413,4 +463,26 @@ class Options
$this->timeout = (int) $timeout;
return $this;
}
/**
* Get context options for connection
*
* @return array
*/
public function getContextOptions()
{
return $this->contextOptions;
}
/**
* Set context options for connection
*
* @param array $contextOptions
* @return \Fabiang\Xmpp\Options
*/
public function setContextOptions($contextOptions)
{
$this->contextOptions = (array) $contextOptions;
return $this;
}
}

View File

@ -0,0 +1,113 @@
<?php
/**
* Copyright 2014 Fabian Grutschus. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. 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.
*
* 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.
*
* The views and conclusions contained in the software and documentation are those
* of the authors and should not be interpreted as representing official policies,
* either expressed or implied, of the copyright holders.
*
* @author Fabian Grutschus <f.grutschus@lubyte.de>
* @copyright 2014 Fabian Grutschus. All rights reserved.
* @license BSD
* @link http://github.com/fabiang/xmpp
*/
namespace Fabiang\Xmpp\Protocol;
use Fabiang\Xmpp\Util\XML;
/**
* Protocol setting for Xmpp.
*
* @package Xmpp\Protocol
*/
class BlockUser implements ProtocolImplementationInterface
{
protected $from;
// the jid of the user to block
protected $accountjid;
/**
* {@inheritDoc}
*/
public function toString()
{
return XML::quoteMessage(
'<iq from="%s" type="set" id="%s">
<block xmlns="urn:xmpp:blocking">
<item jid="%s"/>
</block>
</iq>',
$this->getFrom(),
XML::generateId(),
$this->getJabberID()
);
}
/**
* Get JabberID.
*
* @return string
*/
public function getJabberID()
{
return $this->accountjid;
}
/**
* Set abberID.
*
* @param string $accountjid
* @return $this
*/
public function setJabberID($accountjid)
{
$this->accountjid = (string) $accountjid;
return $this;
}
/**
* Get JabberID.
*
* @return string
*/
public function getFrom()
{
return $this->from;
}
/**
* Set abberID.
*
* @param string $nickname
* @return $this
*/
public function setFrom($from)
{
$this->from = (string) $from;
return $this;
}
}

View File

@ -0,0 +1,57 @@
<?php
/**
* Copyright 2014 Fabian Grutschus. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. 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.
*
* 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.
*
* The views and conclusions contained in the software and documentation are those
* of the authors and should not be interpreted as representing official policies,
* either expressed or implied, of the copyright holders.
*
* @author Fabian Grutschus <f.grutschus@lubyte.de>
* @copyright 2014 Fabian Grutschus. All rights reserved.
* @license BSD
* @link http://github.com/fabiang/xmpp
*/
namespace Fabiang\Xmpp\Protocol;
use Fabiang\Xmpp\Util\XML;
/**
* Protocol setting for Xmpp.
*
* @package Xmpp\Protocol
*/
class BlockedUsers implements ProtocolImplementationInterface
{
/**
* {@inheritDoc}
*/
public function toString()
{
return '<iq type="get" id="' . XML::generateId() . '">'
. '<blocklist xmlns="urn:xmpp:blocking"/></iq>';
}
}

View File

@ -47,6 +47,8 @@ use Fabiang\Xmpp\EventListener\Stream\Authentication;
use Fabiang\Xmpp\EventListener\Stream\Bind;
use Fabiang\Xmpp\EventListener\Stream\Session;
use Fabiang\Xmpp\EventListener\Stream\Roster as RosterListener;
use Fabiang\Xmpp\EventListener\Stream\Register as RegisterListener;
use Fabiang\Xmpp\EventListener\Stream\BlockedUsers as BlockedUsersListener;
/**
* Default Protocol implementation.
@ -82,6 +84,8 @@ class DefaultImplementation implements ImplementationInterface
$this->registerListener(new Bind);
$this->registerListener(new Session);
$this->registerListener(new RosterListener);
$this->registerListener(new RegisterListener);
$this->registerListener(new BlockedUsersListener);
}
/**

View File

@ -132,6 +132,13 @@ class Presence implements ProtocolImplementationInterface
*/
protected $nickname;
/**
* Channel password.
*
* @var string
*/
protected $password;
/**
* Constructor.
*
@ -155,7 +162,14 @@ class Presence implements ProtocolImplementationInterface
$presence .= ' to="' . XML::quote($this->getTo()) . '/' . XML::quote($this->getNickname()) . '"';
}
return $presence . '><priority>' . $this->getPriority() . '</priority></presence>';
$presence .= '><priority>' . $this->getPriority() . '</priority>';
if (null !== $this->getPassword()) {
$presence .= "<x xmlns='http://jabber.org/protocol/muc'><password>" . $this->getPassword() . "</password></x>";
}
$presence .= '</presence>';
return $presence;
}
/**
@ -223,4 +237,26 @@ class Presence implements ProtocolImplementationInterface
$this->priority = (int) $priority;
return $this;
}
/**
* Get channel password.
*
* @return string¦null
*/
public function getPassword()
{
return $this->password;
}
/**
* Set channel password.
*
* @param string|null $to
* @return $this
*/
public function setPassword($password = null)
{
$this->password = $password;
return $this;
}
}

View File

@ -0,0 +1,196 @@
<?php
namespace Fabiang\Xmpp\Protocol;
use Fabiang\Xmpp\Util\XML;
/**
* Protocol setting for Xmpp.
*
* @package Xmpp\Protocol
*/
class Register implements ProtocolImplementationInterface
{
protected $to;
protected $from;
protected $step;
protected $accountjid;
protected $password;
protected $sid;
/**
* Constructor.
*
* @param integer $priority
* @param string $to
* @param string $nickname
*/
public function __construct($to = null, $from = null, $step = 'one')
{
$this->setTo($to);
$this->setFrom($from);
$this->setStep($step);
}
/**
* {@inheritDoc}
*/
public function toString()
{
$req ='';
if($this->step == 'one')
{
$req = XML::quoteMessage(
"<iq from='%s' id='%s' to='%s' type='set' xml:lang='en'>
<command xmlns='http://jabber.org/protocol/commands' action='execute' node='http://jabber.org/protocol/admin#add-user'/>
</iq>",
$this->getFrom(),
XML::generateId(),
$this->getTo()
);
}
else
{
$req = XML::quoteMessage(
"<iq from='%s' id='%s' to='%s' type='set' xml:lang='en'>
<command xmlns='http://jabber.org/protocol/commands' node='http://jabber.org/protocol/admin#add-user' sessionid='%s'>
<x xmlns='jabber:x:data' type='submit'>
<field type='hidden' var='FORM_TYPE'>
<value>http://jabber.org/protocol/admin</value>
</field>
<field var='accountjid'>
<value>%s</value>
</field>
<field var='password'>
<value>%s</value>
</field>
<field var='password-verify'>
<value>%s</value>
</field>
</x>
</command>
</iq>",
$this->getFrom(),
XML::generateId(),
$this->getTo(),
$this->getSID(),
$this->getJabberID(),
$this->getPassword(),
$this->getPassword()
);
}
return $req;
}
/**
* Get JabberID.
*
* @return string
*/
public function getJabberID()
{
return $this->accountjid;
}
/**
* Set abberID.
*
* @param string $nickname
* @return $this
*/
public function setJabberID($accountjid)
{
$this->accountjid = (string) $accountjid;
return $this;
}
/**
* Get JabberID.
*
* @return string
*/
public function getTo()
{
return $this->to;
}
/**
* Set abberID.
*
* @param string $nickname
* @return $this
*/
public function setTo($to)
{
$this->to = (string) $to;
return $this;
}
/**
* Get JabberID.
*
* @return string
*/
public function getPassword()
{
return $this->password;
}
/**
* Set abberID.
*
* @param string $nickname
* @return $this
*/
public function setPassword($password)
{
$this->password = (string) $password;
return $this;
}
/**
* Get JabberID.
*
* @return string
*/
public function getFrom()
{
return $this->from;
}
/**
* Set abberID.
*
* @param string $nickname
* @return $this
*/
public function setFrom($from)
{
$this->from = (string) $from;
return $this;
}
public function setStep($step)
{
$this->step = (string) $step;
return $this;
}
public function setSID($sid)
{
$this->sid = (string) $sid;
return $this;
}
public function getSID()
{
return $this->sid;
}
}

View File

@ -0,0 +1,88 @@
<?php
/**
* Copyright 2014 Fabian Grutschus. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. 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.
*
* 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.
*
* The views and conclusions contained in the software and documentation are those
* of the authors and should not be interpreted as representing official policies,
* either expressed or implied, of the copyright holders.
*
* @author Fabian Grutschus <f.grutschus@lubyte.de>
* @copyright 2014 Fabian Grutschus. All rights reserved.
* @license BSD
* @link http://github.com/fabiang/xmpp
*/
namespace Fabiang\Xmpp\Protocol;
use Fabiang\Xmpp\Util\XML;
/**
* Protocol setting for Xmpp.
*
* @package Xmpp\Protocol
*/
class UnblockUser implements ProtocolImplementationInterface
{
// the jid of the user to block
protected $accountjid;
/**
* {@inheritDoc}
*/
public function toString()
{
return XML::quoteMessage(
'<iq type="set" id="%s">
<unblock xmlns="urn:xmpp:blocking">
<item jid="%s"/>
</unblock>
</iq>',
XML::generateId(),
$this->getJabberID()
);
}
/**
* Get JabberID.
*
* @return string
*/
public function getJabberID()
{
return $this->accountjid;
}
/**
* Set abberID.
*
* @param string $nickname
* @return $this
*/
public function setJabberID($accountjid)
{
$this->accountjid = (string) $accountjid;
return $this;
}
}

View File

@ -66,7 +66,7 @@ class User
*
* @var array
*/
protected $groups = array();
protected $groups = [];
public function getName()
{

View File

@ -0,0 +1,228 @@
<?php
namespace Fabiang\Xmpp\Protocol;
use Fabiang\Xmpp\Util\XML;
/**
* Protocol setting for Xmpp.
*
* @package Xmpp\Protocol
*/
class VCard implements ProtocolImplementationInterface
{
/**
* vCard to.
*
* @var string|null
*/
protected $to;
/**
* user firstname.
*
* @var string
*/
protected $firstname;
/**
* user lastname.
*
* @var string
*/
protected $lastname;
protected $jabberid;
protected $mime;
protected $image;
protected $ulr;
/**
* Constructor.
*
* @param integer $priority
* @param string $to
* @param string $nickname
*/
public function __construct($firstname = null, $lastname = null, $jabberid = null)
{
$this->setFirstname($firstname);
$this->setLastname($lastname);
$this->setJabberID($jabberid);
}
/**
* {@inheritDoc}
*/
public function toString()
{
return XML::quoteMessage(
'<iq id="' . XML::generateId() . '" type="set">
<vCard xmlns="vcard-temp">
<FN>%s</FN>
<N>
<FAMILY>%s</FAMILY>
<GIVEN>%s</GIVEN>
<MIDDLE/>
</N>
<NICKNAME>%s</NICKNAME>
<URL>%s</URL>
<PHOTO>
<TYPE>%s</TYPE>
<BINVAL>
%s
</BINVAL>
</PHOTO>
<JABBERID>%s</JABBERID>
<DESC/>
</vCard>
</iq>',
$this->getFirstname().' '.$this->getLastname(),
$this->getLastname(),
$this->getFirstname(),
$this->getFirstname().' '.$this->getLastname(),
$this->getUrl(),
$this->getMime(),
$this->getImage(),
$this->getJabberID()
);
}
/**
* Get nickname.
*
* @return string
*/
public function getFirstname()
{
return $this->firstname;
}
/**
* Set nickname.
*
* @param string $nickname
* @return $this
*/
public function setFirstname($firstname)
{
$this->firstname = (string) $firstname;
return $this;
}
/**
* Get nickname.
*
* @return string
*/
public function getLastname()
{
return $this->lastname;
}
/**
* Set nickname.
*
* @param string $nickname
* @return $this
*/
public function setLastname($lastname)
{
$this->lastname = (string) $lastname;
return $this;
}
/**
* Get JabberID.
*
* @return string
*/
public function getJabberID()
{
return $this->jabberid;
}
/**
* Set abberID.
*
* @param string $nickname
* @return $this
*/
public function setJabberID($jabberid)
{
$this->jabberid = (string) $jabberid;
return $this;
}
/**
* Get mime.
*
* @return string
*/
public function getMime()
{
return $this->mime;
}
/**
* Set mime.
*
* @param string $mime
* @return $this
*/
public function setMime($mime)
{
$this->mime = (string) $mime;
return $this;
}
/**
* Get image.
*
* @return string
*/
public function getImage()
{
return $this->image;
}
/**
* Set image.
*
* @param string $image base64
* @return $this
*/
public function setImage($image)
{
$this->image = (string) $image;
return $this;
}
/**
* Get url.
*
* @return string
*/
public function getUrl()
{
return $this->url;
}
/**
* Set url.
*
* @param string $image base64
* @return $this
*/
public function setUrl($url)
{
$this->url = (string) $url;
return $this;
}
}

View File

@ -1,26 +1,28 @@
# fabiang/xmpp
[![Latest Stable Version](https://poser.pugx.org/fabiang/xmpp/v/stable.svg)](https://packagist.org/packages/fabiang/xmpp) [![Total Downloads](https://poser.pugx.org/fabiang/xmpp/downloads.svg)](https://packagist.org/packages/fabiang/xmpp) [![Latest Unstable Version](https://poser.pugx.org/fabiang/xmpp/v/unstable.svg)](https://packagist.org/packages/fabiang/xmpp) [![License](https://poser.pugx.org/fabiang/xmpp/license.svg)](https://packagist.org/packages/fabiang/xmpp)
[![Build Status](https://travis-ci.org/fabiang/xmpp.png?branch=master)](https://travis-ci.org/fabiang/xmpp) [![Scrutinizer Quality Score](https://scrutinizer-ci.com/g/fabiang/xmpp/badges/quality-score.png?s=2605ad2bc987ff8501b8f749addff43ec1ac7098)](https://scrutinizer-ci.com/g/fabiang/xmpp/) [![Coverage Status](https://img.shields.io/coveralls/fabiang/xmpp.svg)](https://coveralls.io/r/fabiang/xmpp?branch=master) [![Dependency Status](https://gemnasium.com/fabiang/xmpp.png)](https://gemnasium.com/fabiang/xmpp) [![SensioLabsInsight](https://insight.sensiolabs.com/projects/a535cd82-788d-4506-803e-02ede44a9e74/mini.png)](https://insight.sensiolabs.com/projects/a535cd82-788d-4506-803e-02ede44a9e74)
Library for XMPP protocol connections (Jabber) for PHP.
[![License](https://poser.pugx.org/fabiang/xmpp/license.svg)](https://packagist.org/packages/fabiang/xmpp)
[![Latest Stable Version](https://poser.pugx.org/fabiang/xmpp/v/stable.svg)](https://packagist.org/packages/fabiang/xmpp)
[![Total Downloads](https://poser.pugx.org/fabiang/xmpp/downloads.svg)](https://packagist.org/packages/fabiang/xmpp)
[![Dependency Status](https://gemnasium.com/fabiang/xmpp.svg)](https://gemnasium.com/fabiang/xmpp)
[![Build Status](https://travis-ci.org/fabiang/xmpp.png?branch=master)](https://travis-ci.org/fabiang/xmpp)
[![Coverage Status](https://img.shields.io/coveralls/fabiang/xmpp.svg)](https://coveralls.io/r/fabiang/xmpp?branch=master)
[![Scrutinizer Quality Score](https://scrutinizer-ci.com/g/fabiang/xmpp/badges/quality-score.png?s=2605ad2bc987ff8501b8f749addff43ec1ac7098)](https://scrutinizer-ci.com/g/fabiang/xmpp/)
[![SensioLabsInsight](https://insight.sensiolabs.com/projects/a535cd82-788d-4506-803e-02ede44a9e74/mini.png)](https://insight.sensiolabs.com/projects/a535cd82-788d-4506-803e-02ede44a9e74)
## SYSTEM REQUIREMENTS
- PHP >= 5.3.3
- PHP minimum 5.6 or minimum 7.0
- psr/log
- psr/log-implementation - like monolog/monolog for logging (optional)
- (optional) psr/log-implementation - like monolog/monolog for logging
## INSTALLATION
New to Composer? Read the [introduction](https://getcomposer.org/doc/00-intro.md#introduction). Add the following to your composer file:
```json
{
"require": {
"fabiang/xmpp": "*"
}
}
```bash
composer require fabiang/xmpp
```
## DOCUMENTATION
@ -73,6 +75,7 @@ $client->send($message);
// join a channel
$channel = new Presence;
$channel->setTo('channelname@conference.myjabber.com')
->setPassword('channelpassword')
->setNickName('mynick');
$client->send($channel);
@ -95,14 +98,14 @@ $client->disconnect();
If you like this library and you want to contribute, make sure the unit-tests and integration tests are running.
Composer will help you to install the right version of PHPUnit and [Behat](http://behat.org/).
composer install --dev
composer install
After that:
./vendor/bin/phpunit -c tests
./vendor/bin/behat --config=tests/behat.yml --strict
./vendor/bin/phpunit
./vendor/bin/behat
New features should allways tested with Behat.
New features should always tested with Behat.
## LICENSE
@ -112,5 +115,4 @@ BSD-2-Clause. See the [LICENSE](LICENSE.md).
- Better integration of channels
- Factory method for server addresses
- Add support von vCard
- improve documentation

View File

@ -63,46 +63,57 @@ class SocketClient
*/
protected $address;
/**
* Options used to create a stream context
* @see http://php.net/manual/en/function.stream-context-create.php
*
* @var array
*/
protected $options;
/**
* Constructor takes address as argument.
*
* @param string $address
*/
public function __construct($address)
public function __construct($address, $options = null)
{
$this->address = $address;
$this->options = $options;
}
/**
* Connect.
*
* @param integer $timeout Timeout for connection
* @param integer $timeout Timeout for connection
* @param boolean $persistent Persitent connection
* @return void
*/
public function connect($timeout = 30, $persistent = false)
{
$flags = STREAM_CLIENT_CONNECT;
if (true === $persistent) {
$flags = STREAM_CLIENT_CONNECT | STREAM_CLIENT_PERSISTENT;
} else {
$flags = STREAM_CLIENT_CONNECT;
$flags |= STREAM_CLIENT_PERSISTENT;
}
// call stream_socket_client with custom error handler enabled
$handler = new ErrorHandler(
function ($address, $timeout, $flags) {
$options = [
'ssl' => [
'allow_self_signed' => true,
'verify_peer_name' => false,
],
];
$context = stream_context_create($options);
return stream_socket_client($address, $errno, $errstr, $timeout, $flags, $context);
function ($address, $timeout, $flags, array $options = null) {
$errno = null;
$errstr = null;
if (!empty($options)) {
$context = stream_context_create($options);
return stream_socket_client($address, $errno, $errstr, $timeout, $flags, $context);
}
return stream_socket_client($address, $errno, $errstr, $timeout, $flags);
},
$this->address,
$timeout,
$flags
$flags,
$this->options
);
$resource = $handler->execute(__FILE__, __LINE__);
@ -113,9 +124,9 @@ class SocketClient
/**
* Reconnect and optionally use different address.
*
* @param string $address
* @param string $address
* @param integer $timeout
* @param bool $persistent
* @param bool $persistent
*/
public function reconnect($address = null, $timeout = 30, $persistent = false)
{
@ -146,7 +157,7 @@ class SocketClient
*/
public function setBlocking($flag = true)
{
stream_set_blocking($this->resource, (int) $flag);
stream_set_blocking($this->resource, (int)$flag);
return $this;
}
@ -164,7 +175,7 @@ class SocketClient
/**
* Write to stream.
*
* @param string $string String
* @param string $string String
* @param integer $length Limit
* @return void
*/
@ -180,7 +191,7 @@ class SocketClient
/**
* Enable/disable cryptography on stream.
*
* @param boolean $enable Flag
* @param boolean $enable Flag
* @param integer $cryptoType One of the STREAM_CRYPTO_METHOD_* constants.
* @return void
* @throws InvalidArgumentException

View File

@ -85,21 +85,21 @@ class XMLStream implements EventManagerAwareInterface
*
* @var array
*/
protected $namespaces = array();
protected $namespaces = [];
/**
* Cache of namespace prefixes.
*
* @var array
*/
protected $namespacePrefixes = array();
protected $namespacePrefixes = [];
/**
* Element cache.
*
* @var array
*/
protected $elements = array();
protected $elements = [];
/**
* XML parser.
@ -120,7 +120,7 @@ class XMLStream implements EventManagerAwareInterface
*
* @var array
*/
protected $eventCache = array();
protected $eventCache = [];
/**
* Constructor.
@ -155,13 +155,13 @@ class XMLStream implements EventManagerAwareInterface
{
$this->clearDocument($source);
$this->eventCache = array();
$this->eventCache = [];
if (0 === xml_parse($this->parser, $source, false)) {
throw XMLParserException::create($this->parser);
}
// trigger collected events.
$this->trigger();
$this->eventCache = array();
$this->eventCache = [];
// </stream> was not there, so lets close the document
if ($this->depth > 0) {
@ -186,7 +186,7 @@ class XMLStream implements EventManagerAwareInterface
if ('<?xml' === substr($source, 0, 5)) {
$this->reset();
$matches = array();
$matches = [];
if (preg_match('/^<\?xml.*encoding=(\'|")([\w-]+)\1.*?>/i', $source, $matches)) {
$this->encoding = $matches[2];
xml_parser_set_option($this->parser, XML_OPTION_TARGET_ENCODING, $this->encoding);
@ -260,7 +260,7 @@ class XMLStream implements EventManagerAwareInterface
$this->depth++;
$event = '{' . $namespaceElement . '}' . $elementName;
$this->cacheEvent($event, true, array($element));
$this->cacheEvent($event, true, [$element]);
}
/**
@ -271,7 +271,7 @@ class XMLStream implements EventManagerAwareInterface
*/
protected function createAttributeNodes(array $attribs)
{
$attributesNodes = array();
$attributesNodes = [];
foreach ($attribs as $name => $value) {
// collect namespace prefixes
if ('xmlns:' === substr($name, 0, 6)) {
@ -316,7 +316,7 @@ class XMLStream implements EventManagerAwareInterface
}
$event = '{' . $namespaceURI . '}' . $localName;
$this->cacheEvent($event, false, array($element));
$this->cacheEvent($event, false, [$element]);
}
/**
@ -345,7 +345,7 @@ class XMLStream implements EventManagerAwareInterface
*/
protected function cacheEvent($event, $startTag, $params)
{
$this->eventCache[] = array($event, $startTag, $params);
$this->eventCache[] = [$event, $startTag, $params];
}
/**
@ -382,9 +382,9 @@ class XMLStream implements EventManagerAwareInterface
$this->parser = $parser;
$this->depth = 0;
$this->document = new \DOMDocument('1.0', $this->encoding);
$this->namespaces = array();
$this->namespacePrefixes = array();
$this->elements = array();
$this->namespaces = [];
$this->namespacePrefixes = [];
$this->elements = [];
}
/**

View File

@ -59,7 +59,7 @@ class ErrorHandler
*
* @var array
*/
protected $arguments = array();
protected $arguments = [];
public function __construct($method)
{

View File

@ -18,7 +18,7 @@
* @author Nick Ilyin <nick.ilyin@gmail.com>
* @author: Victor Stanciu <vic.stanciu@gmail.com> (original author)
*
* @version 2.8.41
* @version 2.8.45
*
* Auto-generated isXXXX() magic methods.
* php -a examples/dump_magic_methods.php
@ -255,7 +255,7 @@ class Mobile_Detect
/**
* Stores the version number of the current release.
*/
const VERSION = '2.8.41';
const VERSION = '2.8.45';
/**
* A type for the version() method indicating a string return value.
@ -416,7 +416,7 @@ class Mobile_Detect
'NexusTablet' => 'Android.*Nexus[\s]+(7|9|10)',
// https://en.wikipedia.org/wiki/Pixel_C
'GoogleTablet' => 'Android.*Pixel C',
'SamsungTablet' => 'SAMSUNG.*Tablet|Galaxy.*Tab|SC-01C|GT-P1000|GT-P1003|GT-P1010|GT-P3105|GT-P6210|GT-P6800|GT-P6810|GT-P7100|GT-P7300|GT-P7310|GT-P7500|GT-P7510|SCH-I800|SCH-I815|SCH-I905|SGH-I957|SGH-I987|SGH-T849|SGH-T859|SGH-T869|SPH-P100|GT-P3100|GT-P3108|GT-P3110|GT-P5100|GT-P5110|GT-P6200|GT-P7320|GT-P7511|GT-N8000|GT-P8510|SGH-I497|SPH-P500|SGH-T779|SCH-I705|SCH-I915|GT-N8013|GT-P3113|GT-P5113|GT-P8110|GT-N8010|GT-N8005|GT-N8020|GT-P1013|GT-P6201|GT-P7501|GT-N5100|GT-N5105|GT-N5110|SHV-E140K|SHV-E140L|SHV-E140S|SHV-E150S|SHV-E230K|SHV-E230L|SHV-E230S|SHW-M180K|SHW-M180L|SHW-M180S|SHW-M180W|SHW-M300W|SHW-M305W|SHW-M380K|SHW-M380S|SHW-M380W|SHW-M430W|SHW-M480K|SHW-M480S|SHW-M480W|SHW-M485W|SHW-M486W|SHW-M500W|GT-I9228|SCH-P739|SCH-I925|GT-I9200|GT-P5200|GT-P5210|GT-P5210X|SM-T311|SM-T310|SM-T310X|SM-T210|SM-T210R|SM-T211|SM-P600|SM-P601|SM-P605|SM-P900|SM-P901|SM-T217|SM-T217A|SM-T217S|SM-P6000|SM-T3100|SGH-I467|XE500|SM-T110|GT-P5220|GT-I9200X|GT-N5110X|GT-N5120|SM-P905|SM-T111|SM-T2105|SM-T315|SM-T320|SM-T320X|SM-T321|SM-T520|SM-T525|SM-T530NU|SM-T230NU|SM-T330NU|SM-T900|XE500T1C|SM-P605V|SM-P905V|SM-T337V|SM-T537V|SM-T707V|SM-T807V|SM-P600X|SM-P900X|SM-T210X|SM-T230|SM-T230X|SM-T325|GT-P7503|SM-T531|SM-T330|SM-T530|SM-T705|SM-T705C|SM-T535|SM-T331|SM-T800|SM-T700|SM-T537|SM-T807|SM-P907A|SM-T337A|SM-T537A|SM-T707A|SM-T807A|SM-T237|SM-T807P|SM-P607T|SM-T217T|SM-T337T|SM-T807T|SM-T116NQ|SM-T116BU|SM-P550|SM-T350|SM-T550|SM-T9000|SM-P9000|SM-T705Y|SM-T805|GT-P3113|SM-T710|SM-T810|SM-T815|SM-T360|SM-T533|SM-T113|SM-T335|SM-T715|SM-T560|SM-T670|SM-T677|SM-T377|SM-T567|SM-T357T|SM-T555|SM-T561|SM-T713|SM-T719|SM-T813|SM-T819|SM-T580|SM-T355Y?|SM-T280|SM-T817A|SM-T820|SM-W700|SM-P580|SM-T587|SM-P350|SM-P555M|SM-P355M|SM-T113NU|SM-T815Y|SM-T585|SM-T285|SM-T825|SM-W708|SM-T835|SM-T830|SM-T837V|SM-T720|SM-T510|SM-T387V|SM-P610|SM-T290|SM-T515|SM-T590|SM-T595|SM-T725|SM-T817P|SM-P585N0|SM-T395|SM-T295|SM-T865|SM-P610N|SM-P615|SM-T970|SM-T380|SM-T5950|SM-T905|SM-T231|SM-T500|SM-T860|SM-T536|SM-T837A|SM-X200|SM-T220|SM-T870|SM-X906C', // SCH-P709|SCH-P729|SM-T2558|GT-I9205 - Samsung Mega - treat them like a regular phone.
'SamsungTablet' => 'SAMSUNG.*Tablet|Galaxy.*Tab|SC-01C|GT-P1000|GT-P1003|GT-P1010|GT-P3105|GT-P6210|GT-P6800|GT-P6810|GT-P7100|GT-P7300|GT-P7310|GT-P7500|GT-P7510|SCH-I800|SCH-I815|SCH-I905|SGH-I957|SGH-I987|SGH-T849|SGH-T859|SGH-T869|SPH-P100|GT-P3100|GT-P3108|GT-P3110|GT-P5100|GT-P5110|GT-P6200|GT-P7320|GT-P7511|GT-N8000|GT-P8510|SGH-I497|SPH-P500|SGH-T779|SCH-I705|SCH-I915|GT-N8013|GT-P3113|GT-P5113|GT-P8110|GT-N8010|GT-N8005|GT-N8020|GT-P1013|GT-P6201|GT-P7501|GT-N5100|GT-N5105|GT-N5110|SHV-E140K|SHV-E140L|SHV-E140S|SHV-E150S|SHV-E230K|SHV-E230L|SHV-E230S|SHW-M180K|SHW-M180L|SHW-M180S|SHW-M180W|SHW-M300W|SHW-M305W|SHW-M380K|SHW-M380S|SHW-M380W|SHW-M430W|SHW-M480K|SHW-M480S|SHW-M480W|SHW-M485W|SHW-M486W|SHW-M500W|GT-I9228|SCH-P739|SCH-I925|GT-I9200|GT-P5200|GT-P5210|GT-P5210X|SM-T311|SM-T310|SM-T310X|SM-T210|SM-T210R|SM-T211|SM-P600|SM-P601|SM-P605|SM-P900|SM-P901|SM-T217|SM-T217A|SM-T217S|SM-P6000|SM-T3100|SGH-I467|XE500|SM-T110|GT-P5220|GT-I9200X|GT-N5110X|GT-N5120|SM-P905|SM-T111|SM-T2105|SM-T315|SM-T320|SM-T320X|SM-T321|SM-T520|SM-T525|SM-T530NU|SM-T230NU|SM-T330NU|SM-T900|XE500T1C|SM-P605V|SM-P905V|SM-T337V|SM-T537V|SM-T707V|SM-T807V|SM-P600X|SM-P900X|SM-T210X|SM-T230|SM-T230X|SM-T325|GT-P7503|SM-T531|SM-T330|SM-T530|SM-T705|SM-T705C|SM-T535|SM-T331|SM-T800|SM-T700|SM-T537|SM-T807|SM-P907A|SM-T337A|SM-T537A|SM-T707A|SM-T807A|SM-T237|SM-T807P|SM-P607T|SM-T217T|SM-T337T|SM-T807T|SM-T116NQ|SM-T116BU|SM-P550|SM-T350|SM-T550|SM-T9000|SM-P9000|SM-T705Y|SM-T805|GT-P3113|SM-T710|SM-T810|SM-T815|SM-T360|SM-T533|SM-T113|SM-T335|SM-T715|SM-T560|SM-T670|SM-T677|SM-T377|SM-T567|SM-T357T|SM-T555|SM-T561|SM-T713|SM-T719|SM-T813|SM-T819|SM-T580|SM-T355Y?|SM-T280|SM-T817A|SM-T820|SM-W700|SM-P580|SM-T587|SM-P350|SM-P555M|SM-P355M|SM-T113NU|SM-T815Y|SM-T585|SM-T285|SM-T825|SM-W708|SM-T835|SM-T830|SM-T837V|SM-T720|SM-T510|SM-T387V|SM-P610|SM-T290|SM-T515|SM-T590|SM-T595|SM-T725|SM-T817P|SM-P585N0|SM-T395|SM-T295|SM-T865|SM-P610N|SM-P615|SM-T970|SM-T380|SM-T5950|SM-T905|SM-T231|SM-T500|SM-T860|SM-T536|SM-T837A|SM-X200|SM-T220|SM-T870|SM-X906C|SM-X700|SM-X706|SM-X706B|SM-X706U|SM-X706N|SM-X800|SM-X806|SM-X806B|SM-X806U|SM-X806N|SM-X900|SM-X906|SM-X906B|SM-X906U|SM-X906N|SM-P613', // SCH-P709|SCH-P729|SM-T2558|GT-I9205 - Samsung Mega - treat them like a regular phone.
// http://docs.aws.amazon.com/silk/latest/developerguide/user-agent.html
'Kindle' => 'Kindle|Silk.*Accelerated|Android.*\b(KFOT|KFTT|KFJWI|KFJWA|KFOTE|KFSOWI|KFTHWI|KFTHWA|KFAPWI|KFAPWA|WFJWAE|KFSAWA|KFSAWI|KFASWI|KFARWI|KFFOWI|KFGIWI|KFMEWI)\b|Android.*Silk/[0-9.]+ like Chrome/[0-9.]+ (?!Mobile)',
// Only the Surface tablets with Windows RT are considered mobile.
@ -769,7 +769,7 @@ class Mobile_Detect
// http://scottcate.com/technology/windows-phone-8-ie10-desktop-or-mobile/
// https://github.com/serbanghita/Mobile-Detect/issues/57#issuecomment-15024011
// https://developers.facebook.com/docs/sharing/webmasters/crawler/
'Bot' => 'Googlebot|facebookexternalhit|Google-AMPHTML|s~amp-validator|AdsBot-Google|Google Keyword Suggestion|Facebot|YandexBot|YandexMobileBot|bingbot|ia_archiver|AhrefsBot|Ezooms|GSLFbot|WBSearchBot|Twitterbot|TweetmemeBot|Twikle|PaperLiBot|Wotbox|UnwindFetchor|Exabot|MJ12bot|YandexImages|TurnitinBot|Pingdom|contentkingapp|AspiegelBot',
'Bot' => 'Googlebot|facebookexternalhit|Google-AMPHTML|s~amp-validator|AdsBot-Google|Google Keyword Suggestion|Facebot|YandexBot|YandexMobileBot|bingbot|ia_archiver|AhrefsBot|Ezooms|GSLFbot|WBSearchBot|Twitterbot|TweetmemeBot|Twikle|PaperLiBot|Wotbox|UnwindFetchor|Exabot|MJ12bot|YandexImages|TurnitinBot|Pingdom|contentkingapp|AspiegelBot|Semrush|DotBot|PetalBot|MetadataScraper',
'MobileBot' => 'Googlebot-Mobile|AdsBot-Google-Mobile|YahooSeeker/M1A1-R2D2',
'DesktopMode' => 'WPDesktop',
'TV' => 'SonyDTV|HbbTV', // experimental
@ -1180,10 +1180,10 @@ class Mobile_Detect
if (!$rules) {
$rules = array_merge(
self::$phoneDevices,
self::$tabletDevices,
self::$operatingSystems,
self::$browsers
self::getPhoneDevices(),
self::getTabletDevices(),
self::getOperatingSystems(),
self::getBrowsers()
);
}
@ -1208,11 +1208,11 @@ class Mobile_Detect
if (!$rules) {
// Merge all rules together.
$rules = array_merge(
self::$phoneDevices,
self::$tabletDevices,
self::$operatingSystems,
self::$browsers,
self::$utilities
static::getPhoneDevices(),
static::getTabletDevices(),
static::getOperatingSystems(),
static::getBrowsers(),
static::getUtilities()
);
}
@ -1406,7 +1406,7 @@ class Mobile_Detect
$this->setDetectionType(self::DETECTION_TYPE_MOBILE);
foreach (self::$tabletDevices as $_regex) {
foreach (static::getTabletDevices() as $_regex) {
if ($this->match($_regex, $userAgent)) {
return true;
}
@ -1462,7 +1462,7 @@ class Mobile_Detect
return false;
}
$match = (bool) preg_match(sprintf('#%s#is', $regex), (false === empty($userAgent) ? $userAgent : $this->userAgent), $matches);
$match = (bool) preg_match(sprintf('#%s#is', $regex), (false === empty($userAgent) ? $userAgent : (is_string($this->userAgent) ? $this->userAgent : '')), $matches);
// If positive match is found, store the results for debug.
if ($match) {
$this->matchingRegex = $regex;
@ -1531,7 +1531,7 @@ class Mobile_Detect
$type = self::VERSION_TYPE_STRING;
}
$properties = self::getProperties();
$properties = static::getProperties();
// Check if the property exists in the properties array.
if (true === isset($properties[$propertyName])) {
@ -1545,7 +1545,7 @@ class Mobile_Detect
$propertyPattern = str_replace('[VER]', self::VER, $propertyMatchString);
// Identify and extract the version.
preg_match(sprintf('#%s#is', $propertyPattern), $this->userAgent, $match);
preg_match(sprintf('#%s#is', $propertyPattern), (is_string($this->userAgent) ? $this->userAgent : ''), $match);
if (false === empty($match[1])) {
$version = ($type == self::VERSION_TYPE_FLOAT ? $this->prepareVersionNo($match[1]) : $match[1]);

View File

@ -0,0 +1,160 @@
<?php
/**
* This file is part of the Tracy (https://tracy.nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Tracy\Bridges\Nette;
use Latte;
use Nette;
use Tracy;
use Tracy\BlueScreen;
use Tracy\Helpers;
/**
* Bridge for NEON & Latte.
*/
class Bridge
{
public static function initialize(): void
{
$blueScreen = Tracy\Debugger::getBlueScreen();
if (!class_exists(Latte\Bridges\Tracy\BlueScreenPanel::class)) {
$blueScreen->addPanel([self::class, 'renderLatteError']);
$blueScreen->addAction([self::class, 'renderLatteUnknownMacro']);
$blueScreen->addFileGenerator(function (string $file) {
return substr($file, -6) === '.latte'
? "{block content}\n\$END\$"
: null;
});
Tracy\Debugger::addSourceMapper([self::class, 'mapLatteSourceCode']);
}
$blueScreen->addAction([self::class, 'renderMemberAccessException']);
$blueScreen->addPanel([self::class, 'renderNeonError']);
}
public static function renderLatteError(?\Throwable $e): ?array
{
if ($e instanceof Latte\CompileException && $e->sourceName) {
return [
'tab' => 'Template',
'panel' => (preg_match('#\n|\?#', $e->sourceName)
? ''
: '<p>'
. (@is_file($e->sourceName) // @ - may trigger error
? '<b>File:</b> ' . Helpers::editorLink($e->sourceName, $e->sourceLine)
: '<b>' . htmlspecialchars($e->sourceName . ($e->sourceLine ? ':' . $e->sourceLine : '')) . '</b>')
. '</p>')
. BlueScreen::highlightFile($e->sourceCode, $e->sourceLine, 15, false),
];
}
return null;
}
public static function renderLatteUnknownMacro(?\Throwable $e): ?array
{
if (
$e instanceof Latte\CompileException
&& $e->sourceName
&& @is_file($e->sourceName) // @ - may trigger error
&& (preg_match('#Unknown macro (\{\w+)\}, did you mean (\{\w+)\}\?#A', $e->getMessage(), $m)
|| preg_match('#Unknown attribute (n:\w+), did you mean (n:\w+)\?#A', $e->getMessage(), $m))
) {
return [
'link' => Helpers::editorUri($e->sourceName, $e->sourceLine, 'fix', $m[1], $m[2]),
'label' => 'fix it',
];
}
return null;
}
/** @return array{file: string, line: int, label: string, active: bool} */
public static function mapLatteSourceCode(string $file, int $line): ?array
{
if (!strpos($file, '.latte--')) {
return null;
}
$lines = file($file);
if (
!preg_match('#^/(?:\*\*|/) source: (\S+\.latte)#m', implode('', array_slice($lines, 0, 10)), $m)
|| !@is_file($m[1]) // @ - may trigger error
) {
return null;
}
$file = $m[1];
$line = $line && preg_match('#/\* line (\d+) \*/#', $lines[$line - 1], $m) ? (int) $m[1] : 0;
return ['file' => $file, 'line' => $line, 'label' => 'Latte', 'active' => true];
}
public static function renderMemberAccessException(?\Throwable $e): ?array
{
if (!$e instanceof Nette\MemberAccessException && !$e instanceof \LogicException) {
return null;
}
$loc = $e->getTrace()[$e instanceof Nette\MemberAccessException ? 1 : 0];
if (!isset($loc['file'])) {
return null;
}
$loc = Tracy\Debugger::mapSource($loc['file'], $loc['line']) ?? $loc;
if (preg_match('#Cannot (?:read|write to) an undeclared property .+::\$(\w+), did you mean \$(\w+)\?#A', $e->getMessage(), $m)) {
return [
'link' => Helpers::editorUri($loc['file'], $loc['line'], 'fix', '->' . $m[1], '->' . $m[2]),
'label' => 'fix it',
];
} elseif (preg_match('#Call to undefined (static )?method .+::(\w+)\(\), did you mean (\w+)\(\)?#A', $e->getMessage(), $m)) {
$operator = $m[1] ? '::' : '->';
return [
'link' => Helpers::editorUri($loc['file'], $loc['line'], 'fix', $operator . $m[2] . '(', $operator . $m[3] . '('),
'label' => 'fix it',
];
}
return null;
}
public static function renderNeonError(?\Throwable $e): ?array
{
if (!$e instanceof Nette\Neon\Exception || !preg_match('#line (\d+)#', $e->getMessage(), $m)) {
return null;
} elseif ($trace = Helpers::findTrace($e->getTrace(), [Nette\Neon\Decoder::class, 'decodeFile'])
?? Helpers::findTrace($e->getTrace(), [Nette\DI\Config\Adapters\NeonAdapter::class, 'load'])
) {
$panel = '<p><b>File:</b> ' . Helpers::editorLink($trace['args'][0], (int) $m[1]) . '</p>'
. self::highlightNeon(file_get_contents($trace['args'][0]), (int) $m[1]);
} elseif ($trace = Helpers::findTrace($e->getTrace(), [Nette\Neon\Decoder::class, 'decode'])) {
$panel = self::highlightNeon($trace['args'][0], (int) $m[1]);
}
return isset($panel) ? ['tab' => 'NEON', 'panel' => $panel] : null;
}
private static function highlightNeon(string $code, int $line): string
{
$code = htmlspecialchars($code, ENT_IGNORE, 'UTF-8');
$code = str_replace(' ', "<span class='tracy-dump-whitespace'>·</span>", $code);
$code = str_replace("\t", "<span class='tracy-dump-whitespace'>→ </span>", $code);
return '<pre class=code><div>'
. BlueScreen::highlightLine($code, $line)
. '</div></pre>';
}
}

View File

@ -0,0 +1,59 @@
<?php
/**
* This file is part of the Tracy (https://tracy.nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Tracy\Bridges\Nette;
use Nette;
use Tracy;
/**
* Tracy logger bridge for Nette Mail.
*/
class MailSender
{
use Nette\SmartObject;
/** @var Nette\Mail\IMailer */
private $mailer;
/** @var string|null sender of email notifications */
private $fromEmail;
public function __construct(Nette\Mail\IMailer $mailer, ?string $fromEmail = null)
{
$this->mailer = $mailer;
$this->fromEmail = $fromEmail;
}
/**
* @param mixed $message
*/
public function send($message, string $email): void
{
$host = preg_replace('#[^\w.-]+#', '', $_SERVER['SERVER_NAME'] ?? php_uname('n'));
$mail = new Nette\Mail\Message;
$mail->setHeader('X-Mailer', 'Tracy');
if ($this->fromEmail || Nette\Utils\Validators::isEmail("noreply@$host")) {
$mail->setFrom($this->fromEmail ?: "noreply@$host");
}
foreach (explode(',', $email) as $item) {
$mail->addTo(trim($item));
}
$mail->setSubject('PHP: An error occurred on the server ' . $host);
$mail->setBody(Tracy\Logger::formatMessage($message) . "\n\nsource: " . Tracy\Helpers::getSource());
$this->mailer->send($mail);
}
}

View File

@ -0,0 +1,184 @@
<?php
/**
* This file is part of the Tracy (https://tracy.nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Tracy\Bridges\Nette;
use Nette;
use Nette\DI\Definitions\Statement;
use Nette\Schema\Expect;
use Tracy;
/**
* Tracy extension for Nette DI.
*/
class TracyExtension extends Nette\DI\CompilerExtension
{
private const ErrorSeverityPattern = 'E_(?:ALL|PARSE|STRICT|RECOVERABLE_ERROR|(?:CORE|COMPILE)_(?:ERROR|WARNING)|(?:USER_)?(?:ERROR|WARNING|NOTICE|DEPRECATED))';
/** @var bool */
private $debugMode;
/** @var bool */
private $cliMode;
public function __construct(bool $debugMode = false, bool $cliMode = false)
{
$this->debugMode = $debugMode;
$this->cliMode = $cliMode;
}
public function getConfigSchema(): Nette\Schema\Schema
{
$errorSeverity = Expect::string()->pattern(self::ErrorSeverityPattern);
$errorSeverityExpr = Expect::string()->pattern('(' . self::ErrorSeverityPattern . '|[ &|~()])+');
return Expect::structure([
'email' => Expect::anyOf(Expect::email(), Expect::listOf('email'))->dynamic(),
'fromEmail' => Expect::email()->dynamic(),
'emailSnooze' => Expect::string()->dynamic(),
'logSeverity' => Expect::anyOf(Expect::int(), $errorSeverityExpr, Expect::listOf($errorSeverity)),
'editor' => Expect::type('string|null')->dynamic(),
'browser' => Expect::string()->dynamic(),
'errorTemplate' => Expect::string()->dynamic(),
'strictMode' => Expect::anyOf(Expect::bool(), Expect::int(), $errorSeverityExpr, Expect::listOf($errorSeverity)),
'showBar' => Expect::bool()->dynamic(),
'maxLength' => Expect::int()->dynamic(),
'maxDepth' => Expect::int()->dynamic(),
'maxItems' => Expect::int()->dynamic(),
'keysToHide' => Expect::array(null)->dynamic(),
'dumpTheme' => Expect::string()->dynamic(),
'showLocation' => Expect::bool()->dynamic(),
'scream' => Expect::anyOf(Expect::bool(), Expect::int(), $errorSeverityExpr, Expect::listOf($errorSeverity)),
'bar' => Expect::listOf('string|Nette\DI\Definitions\Statement'),
'blueScreen' => Expect::listOf('callable'),
'editorMapping' => Expect::arrayOf('string')->dynamic()->default(null),
'netteMailer' => Expect::bool(true),
]);
}
public function loadConfiguration()
{
$builder = $this->getContainerBuilder();
$builder->addDefinition($this->prefix('logger'))
->setClass(Tracy\ILogger::class)
->setFactory([Tracy\Debugger::class, 'getLogger']);
$builder->addDefinition($this->prefix('blueScreen'))
->setFactory([Tracy\Debugger::class, 'getBlueScreen']);
$builder->addDefinition($this->prefix('bar'))
->setFactory([Tracy\Debugger::class, 'getBar']);
}
public function afterCompile(Nette\PhpGenerator\ClassType $class)
{
$initialize = $this->initialization ?? new Nette\PhpGenerator\Closure;
$initialize->addBody('if (!Tracy\Debugger::isEnabled()) { return; }');
$builder = $this->getContainerBuilder();
$logger = $builder->getDefinition($this->prefix('logger'));
$initialize->addBody($builder->formatPhp('$logger = ?;', [$logger]));
if (
!$logger instanceof Nette\DI\Definitions\ServiceDefinition
|| $logger->getFactory()->getEntity() !== [Tracy\Debugger::class, 'getLogger']
) {
$initialize->addBody('Tracy\Debugger::setLogger($logger);');
}
$options = (array) $this->config;
unset($options['bar'], $options['blueScreen'], $options['netteMailer']);
foreach (['logSeverity', 'strictMode', 'scream'] as $key) {
if (is_string($options[$key]) || is_array($options[$key])) {
$options[$key] = $this->parseErrorSeverity($options[$key]);
}
}
foreach ($options as $key => $value) {
if ($value !== null) {
$tbl = [
'keysToHide' => 'array_push(Tracy\Debugger::getBlueScreen()->keysToHide, ... ?)',
'fromEmail' => 'if ($logger instanceof Tracy\Logger) $logger->fromEmail = ?',
'emailSnooze' => 'if ($logger instanceof Tracy\Logger) $logger->emailSnooze = ?',
];
$initialize->addBody($builder->formatPhp(
($tbl[$key] ?? 'Tracy\Debugger::$' . $key . ' = ?') . ';',
Nette\DI\Helpers::filterArguments([$value])
));
}
}
if ($this->config->netteMailer && $builder->getByType(Nette\Mail\IMailer::class)) {
$initialize->addBody($builder->formatPhp('if ($logger instanceof Tracy\Logger) $logger->mailer = ?;', [
[new Statement(Tracy\Bridges\Nette\MailSender::class, ['fromEmail' => $this->config->fromEmail]), 'send'],
]));
}
if ($this->debugMode) {
foreach ($this->config->bar as $item) {
if (is_string($item) && substr($item, 0, 1) === '@') {
$item = new Statement(['@' . $builder::THIS_CONTAINER, 'getService'], [substr($item, 1)]);
} elseif (is_string($item)) {
$item = new Statement($item);
}
$initialize->addBody($builder->formatPhp(
'$this->getService(?)->addPanel(?);',
Nette\DI\Helpers::filterArguments([$this->prefix('bar'), $item])
));
}
if (
!$this->cliMode
&& Tracy\Debugger::getSessionStorage() instanceof Tracy\NativeSession
&& ($name = $builder->getByType(Nette\Http\Session::class))
) {
$initialize->addBody('$this->getService(?)->start();', [$name]);
$initialize->addBody('Tracy\Debugger::dispatch();');
}
}
foreach ($this->config->blueScreen as $item) {
$initialize->addBody($builder->formatPhp(
'$this->getService(?)->addPanel(?);',
Nette\DI\Helpers::filterArguments([$this->prefix('blueScreen'), $item])
));
}
if (empty($this->initialization)) {
$class->getMethod('initialize')->addBody("($initialize)();");
}
if (($dir = Tracy\Debugger::$logDirectory) && !is_writable($dir)) {
throw new Nette\InvalidStateException("Make directory '$dir' writable.");
}
}
/**
* @param string|string[] $value
*/
private function parseErrorSeverity($value): int
{
$value = implode('|', (array) $value);
$res = (int) @parse_ini_string('e = ' . $value)['e']; // @ may fail
if (!$res) {
throw new Nette\InvalidStateException("Syntax error in expression '$value'");
}
return $res;
}
}

View File

@ -0,0 +1,62 @@
<?php
/**
* This file is part of the Tracy (https://tracy.nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Tracy\Bridges\Psr;
use Psr;
use Tracy;
/**
* Psr\Log\LoggerInterface to Tracy\ILogger adapter.
*/
class PsrToTracyLoggerAdapter implements Tracy\ILogger
{
/** Tracy logger level to PSR-3 log level mapping */
private const LevelMap = [
Tracy\ILogger::DEBUG => Psr\Log\LogLevel::DEBUG,
Tracy\ILogger::INFO => Psr\Log\LogLevel::INFO,
Tracy\ILogger::WARNING => Psr\Log\LogLevel::WARNING,
Tracy\ILogger::ERROR => Psr\Log\LogLevel::ERROR,
Tracy\ILogger::EXCEPTION => Psr\Log\LogLevel::ERROR,
Tracy\ILogger::CRITICAL => Psr\Log\LogLevel::CRITICAL,
];
/** @var Psr\Log\LoggerInterface */
private $psrLogger;
public function __construct(Psr\Log\LoggerInterface $psrLogger)
{
$this->psrLogger = $psrLogger;
}
public function log($value, $level = self::INFO)
{
if ($value instanceof \Throwable) {
$message = Tracy\Helpers::getClass($value) . ': ' . $value->getMessage() . ($value->getCode() ? ' #' . $value->getCode() : '') . ' in ' . $value->getFile() . ':' . $value->getLine();
$context = ['exception' => $value];
} elseif (!is_string($value)) {
$message = trim(Tracy\Dumper::toText($value));
$context = [];
} else {
$message = $value;
$context = [];
}
$this->psrLogger->log(
self::LevelMap[$level] ?? Psr\Log\LogLevel::ERROR,
$message,
$context
);
}
}

View File

@ -0,0 +1,61 @@
<?php
/**
* This file is part of the Tracy (https://tracy.nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Tracy\Bridges\Psr;
use Psr;
use Tracy;
/**
* Tracy\ILogger to Psr\Log\LoggerInterface adapter.
*/
class TracyToPsrLoggerAdapter extends Psr\Log\AbstractLogger
{
/** PSR-3 log level to Tracy logger level mapping */
private const LevelMap = [
Psr\Log\LogLevel::EMERGENCY => Tracy\ILogger::CRITICAL,
Psr\Log\LogLevel::ALERT => Tracy\ILogger::CRITICAL,
Psr\Log\LogLevel::CRITICAL => Tracy\ILogger::CRITICAL,
Psr\Log\LogLevel::ERROR => Tracy\ILogger::ERROR,
Psr\Log\LogLevel::WARNING => Tracy\ILogger::WARNING,
Psr\Log\LogLevel::NOTICE => Tracy\ILogger::WARNING,
Psr\Log\LogLevel::INFO => Tracy\ILogger::INFO,
Psr\Log\LogLevel::DEBUG => Tracy\ILogger::DEBUG,
];
/** @var Tracy\ILogger */
private $tracyLogger;
public function __construct(Tracy\ILogger $tracyLogger)
{
$this->tracyLogger = $tracyLogger;
}
public function log($level, $message, array $context = []): void
{
$level = self::LevelMap[$level] ?? Tracy\ILogger::ERROR;
if (isset($context['exception']) && $context['exception'] instanceof \Throwable) {
$this->tracyLogger->log($context['exception'], $level);
unset($context['exception']);
}
if ($context) {
$message = [
'message' => $message,
'context' => $context,
];
}
$this->tracyLogger->log($message, $level);
}
}

View File

@ -0,0 +1,164 @@
<?php
/**
* This file is part of the Tracy (https://tracy.nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Tracy;
/**
* Debug Bar.
*/
class Bar
{
/** @var IBarPanel[] */
private $panels = [];
/** @var bool */
private $loaderRendered = false;
/**
* Add custom panel.
* @return static
*/
public function addPanel(IBarPanel $panel, ?string $id = null): self
{
if ($id === null) {
$c = 0;
do {
$id = get_class($panel) . ($c++ ? "-$c" : '');
} while (isset($this->panels[$id]));
}
$this->panels[$id] = $panel;
return $this;
}
/**
* Returns panel with given id
*/
public function getPanel(string $id): ?IBarPanel
{
return $this->panels[$id] ?? null;
}
/**
* Renders loading <script>
* @internal
*/
public function renderLoader(DeferredContent $defer): void
{
if (!$defer->isAvailable()) {
throw new \LogicException('Start session before Tracy is enabled.');
}
$this->loaderRendered = true;
$requestId = $defer->getRequestId();
$nonce = Helpers::getNonce();
$async = true;
require __DIR__ . '/assets/loader.phtml';
}
/**
* Renders debug bar.
*/
public function render(DeferredContent $defer): void
{
$redirectQueue = &$defer->getItems('redirect');
$requestId = $defer->getRequestId();
if (Helpers::isAjax()) {
if ($defer->isAvailable()) {
$defer->addSetup('Tracy.Debug.loadAjax', $this->renderPartial('ajax', '-ajax:' . $requestId));
}
} elseif (Helpers::isRedirect()) {
if ($defer->isAvailable()) {
$redirectQueue[] = ['content' => $this->renderPartial('redirect', '-r' . count($redirectQueue)), 'time' => time()];
}
} elseif (Helpers::isHtmlMode()) {
if (preg_match('#^Content-Length:#im', implode("\n", headers_list()))) {
Debugger::log(new \LogicException('Tracy cannot display the Bar because the Content-Length header is being sent'), Debugger::EXCEPTION);
}
$content = $this->renderPartial('main');
foreach (array_reverse($redirectQueue) as $item) {
$content['bar'] .= $item['content']['bar'];
$content['panels'] .= $item['content']['panels'];
}
$redirectQueue = null;
$content = '<div id=tracy-debug-bar>' . $content['bar'] . '</div>' . $content['panels'];
if ($this->loaderRendered) {
$defer->addSetup('Tracy.Debug.init', $content);
} else {
$nonce = Helpers::getNonce();
$async = false;
Debugger::removeOutputBuffers(false);
require __DIR__ . '/assets/loader.phtml';
}
}
}
private function renderPartial(string $type, string $suffix = ''): array
{
$panels = $this->renderPanels($suffix);
return [
'bar' => Helpers::capture(function () use ($type, $panels) {
require __DIR__ . '/assets/bar.phtml';
}),
'panels' => Helpers::capture(function () use ($type, $panels) {
require __DIR__ . '/assets/panels.phtml';
}),
];
}
private function renderPanels(string $suffix = ''): array
{
set_error_handler(function (int $severity, string $message, string $file, int $line) {
if (error_reporting() & $severity) {
throw new \ErrorException($message, 0, $severity, $file, $line);
}
});
$obLevel = ob_get_level();
$panels = [];
foreach ($this->panels as $id => $panel) {
$idHtml = preg_replace('#[^a-z0-9]+#i', '-', $id) . $suffix;
try {
$tab = (string) $panel->getTab();
$panelHtml = $tab ? $panel->getPanel() : null;
} catch (\Throwable $e) {
while (ob_get_level() > $obLevel) { // restore ob-level if broken
ob_end_clean();
}
$idHtml = "error-$idHtml";
$tab = "Error in $id";
$panelHtml = "<h1>Error: $id</h1><div class='tracy-inner'>" . nl2br(Helpers::escapeHtml($e)) . '</div>';
unset($e);
}
$panels[] = (object) ['id' => $idHtml, 'tab' => $tab, 'panel' => $panelHtml];
}
restore_error_handler();
return $panels;
}
}

View File

@ -0,0 +1,55 @@
<?php
/**
* This file is part of the Tracy (https://tracy.nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Tracy;
/**
* IBarPanel implementation helper.
* @internal
*/
#[\AllowDynamicProperties]
class DefaultBarPanel implements IBarPanel
{
public $data;
private $id;
public function __construct(string $id)
{
$this->id = $id;
}
/**
* Renders HTML code for custom tab.
*/
public function getTab(): string
{
return Helpers::capture(function () {
$data = $this->data;
require __DIR__ . "/panels/{$this->id}.tab.phtml";
});
}
/**
* Renders HTML code for custom panel.
*/
public function getPanel(): string
{
return Helpers::capture(function () {
if (is_file(__DIR__ . "/panels/{$this->id}.panel.phtml")) {
$data = $this->data;
require __DIR__ . "/panels/{$this->id}.panel.phtml";
}
});
}
}

View File

@ -0,0 +1,29 @@
<?php
/**
* This file is part of the Tracy (https://tracy.nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Tracy;
/**
* Custom output for Debugger.
*/
interface IBarPanel
{
/**
* Renders HTML code for custom tab.
* @return string
*/
function getTab();
/**
* Renders HTML code for custom panel.
* @return string
*/
function getPanel();
}

View File

@ -0,0 +1,291 @@
/**
* This file is part of the Tracy (https://tracy.nette.org)
*/
/* common styles */
#tracy-debug {
display: none;
direction: ltr;
}
body#tracy-debug { /* in popup window */
display: block;
}
#tracy-debug:not(body) {
position: absolute;
left: 0;
top: 0;
}
#tracy-debug a {
color: #125EAE;
text-decoration: none;
}
#tracy-debug a:hover,
#tracy-debug a:focus {
background-color: #125EAE;
color: white;
}
#tracy-debug h2,
#tracy-debug h3,
#tracy-debug p {
margin: .4em 0;
}
#tracy-debug table {
background: #FDF5CE;
width: 100%;
}
#tracy-debug tr:nth-child(2n) td {
background: rgba(0, 0, 0, 0.02);
}
#tracy-debug td,
#tracy-debug th {
border: 1px solid #E6DFBF;
padding: 2px 5px;
vertical-align: top;
text-align: left;
}
#tracy-debug th {
background: #F4F3F1;
color: #655E5E;
font-size: 90%;
font-weight: bold;
}
#tracy-debug pre,
#tracy-debug code {
font: 9pt/1.5 Consolas, monospace;
}
#tracy-debug table .tracy-right {
text-align: right;
}
#tracy-debug svg {
display: inline;
}
#tracy-debug .tracy-dump {
margin: 0;
padding: 2px 5px;
}
/* bar */
#tracy-debug-bar {
font: normal normal 13px/1.55 Tahoma, sans-serif;
color: #333;
border: 1px solid #c9c9c9;
background: #EDEAE0 url('data:image/png;base64,R0lGODlhAQAUALMAAOzq4e/t5e7s4/Dt5vDu5e3r4vDu5uvp4O/t5AAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAAAAAAALAAAAAABABQAAAQM0EgySEAYi1LA+UcEADs=') top;
background-size: 1em;
position: fixed;
min-width: 50px;
white-space: nowrap;
z-index: 30000;
opacity: .9;
transition: opacity 0.2s;
will-change: opacity, top, left;
border-radius: 3px;
box-shadow: 1px 1px 10px rgba(0, 0, 0, .15);
}
#tracy-debug-bar:hover {
opacity: 1;
transition: opacity 0.1s;
}
#tracy-debug-bar .tracy-row {
list-style: none none;
display: flex;
}
#tracy-debug-bar .tracy-row:not(:first-child) {
background: #d5d2c6;
opacity: .8;
}
#tracy-debug-bar .tracy-row[data-tracy-group="ajax"] {
animation: tracy-row-flash .2s ease;
}
@keyframes tracy-row-flash {
0% {
background: #c9c0a0;
}
}
#tracy-debug-bar .tracy-row:not(:first-child) li:first-child {
width: 4.1em;
text-align: center;
}
#tracy-debug-bar img {
vertical-align: bottom;
position: relative;
top: -2px;
}
#tracy-debug-bar svg {
vertical-align: bottom;
width: 1.23em;
height: 1.55em;
}
#tracy-debug-bar .tracy-label {
margin-left: .2em;
}
#tracy-debug-bar li > a,
#tracy-debug-bar li > span {
color: #000;
display: block;
padding: 0 .4em;
}
#tracy-debug-bar li > a:hover {
color: black;
background: #c3c1b8;
}
#tracy-debug-bar li:first-child {
cursor: move;
}
#tracy-debug-logo svg {
width: 3.4em;
margin: 0 .2em 0 .5em;
}
/* panels */
#tracy-debug .tracy-panel {
display: none;
font: normal normal 12px/1.5 sans-serif;
background: white;
color: #333;
text-align: left;
}
body#tracy-debug .tracy-panel { /* in popup window */
display: block;
}
#tracy-debug h1 {
font: normal normal 23px/1.4 Tahoma, sans-serif;
color: #575753;
margin: -5px -5px 5px;
padding: 0 5px 0 5px;
word-wrap: break-word;
}
#tracy-debug .tracy-inner {
overflow: auto;
flex: 1;
}
#tracy-debug .tracy-panel .tracy-icons {
display: none;
}
#tracy-debug .tracy-panel-ajax h1::after,
#tracy-debug .tracy-panel-redirect h1::after {
content: 'ajax';
float: right;
font-size: 65%;
margin: 0 .3em;
}
#tracy-debug .tracy-panel-redirect h1::after {
content: 'redirect';
}
#tracy-debug .tracy-mode-peek,
#tracy-debug .tracy-mode-float {
position: fixed;
flex-direction: column;
padding: 10px;
min-width: 200px;
min-height: 80px;
border-radius: 5px;
box-shadow: 1px 1px 20px rgba(102, 102, 102, 0.36);
border: 1px solid rgba(0, 0, 0, 0.1);
}
#tracy-debug .tracy-mode-peek,
#tracy-debug .tracy-mode-float:not(.tracy-panel-resized) {
max-width: 700px;
max-height: 500px;
}
@media (max-height: 555px) {
#tracy-debug .tracy-mode-peek,
#tracy-debug .tracy-mode-float:not(.tracy-panel-resized) {
max-height: 100vh;
}
}
#tracy-debug .tracy-mode-peek h1 {
cursor: move;
}
#tracy-debug .tracy-mode-float {
display: flex;
opacity: .95;
transition: opacity 0.2s;
will-change: opacity, top, left;
overflow: auto;
resize: both;
}
#tracy-debug .tracy-focused {
display: flex;
opacity: 1;
transition: opacity 0.1s;
}
#tracy-debug .tracy-mode-float h1 {
cursor: move;
padding-right: 25px;
}
#tracy-debug .tracy-mode-float .tracy-icons {
display: block;
position: absolute;
top: 0;
right: 5px;
font-size: 18px;
}
#tracy-debug .tracy-mode-window {
padding: 10px;
}
#tracy-debug .tracy-icons a {
color: #575753;
}
#tracy-debug .tracy-icons a:hover {
color: white;
}
#tracy-debug .tracy-inner-container {
min-width: 100%;
float: left;
}
@media print {
#tracy-debug * {
display: none;
}
}

View File

@ -0,0 +1,685 @@
/**
* This file is part of the Tracy (https://tracy.nette.org)
*/
let nonce = document.currentScript.getAttribute('nonce') || document.currentScript.nonce,
requestId = document.currentScript.dataset.id,
ajaxCounter = 1,
baseUrl = location.href.split('#')[0];
baseUrl += (baseUrl.indexOf('?') < 0 ? '?' : '&');
let defaults = {
PanelZIndex: 20000,
MaxAjaxRows: 3,
AutoRefresh: true,
};
function getOption(key)
{
let global = window['Tracy' + key];
return global === undefined ? defaults[key] : global;
}
class Panel
{
constructor(id) {
this.id = id;
this.elem = document.getElementById(this.id);
this.elem.Tracy = this.elem.Tracy || {};
}
init() {
let elem = this.elem;
this.init = function() {};
elem.innerHTML = elem.dataset.tracyContent;
Tracy.Dumper.init(Debug.layer);
evalScripts(elem);
draggable(elem, {
handles: elem.querySelectorAll('h1'),
start: () => {
if (!this.is(Panel.FLOAT)) {
this.toFloat();
}
this.focus();
this.peekPosition = false;
}
});
elem.addEventListener('mousedown', () => {
this.focus();
});
elem.addEventListener('mouseenter', () => {
clearTimeout(elem.Tracy.displayTimeout);
});
elem.addEventListener('mouseleave', () => {
this.blur();
});
elem.addEventListener('mousemove', (e) => {
if (e.buttons && !this.is(Panel.RESIZED) && (elem.style.width || elem.style.height)) {
elem.classList.add(Panel.RESIZED);
}
});
elem.addEventListener('tracy-toggle', () => {
this.reposition();
});
elem.querySelectorAll('.tracy-icons a').forEach((link) => {
link.addEventListener('click', (e) => {
if (link.dataset.tracyAction === 'close') {
this.toPeek();
} else if (link.dataset.tracyAction === 'window') {
this.toWindow();
}
e.preventDefault();
e.stopImmediatePropagation();
});
});
if (this.is('tracy-panel-persist')) {
Tracy.Toggle.persist(elem);
}
}
is(mode) {
return this.elem.classList.contains(mode);
}
focus() {
let elem = this.elem;
if (this.is(Panel.WINDOW)) {
elem.Tracy.window.focus();
} else if (!this.is(Panel.FOCUSED)) {
for (let id in Debug.panels) {
Debug.panels[id].elem.classList.remove(Panel.FOCUSED);
}
elem.classList.add(Panel.FOCUSED);
elem.style.zIndex = getOption('PanelZIndex') + Panel.zIndexCounter++;
}
}
blur() {
let elem = this.elem;
if (this.is(Panel.PEEK)) {
clearTimeout(elem.Tracy.displayTimeout);
elem.Tracy.displayTimeout = setTimeout(() => {
elem.classList.remove(Panel.FOCUSED);
}, 50);
}
}
toFloat() {
this.elem.classList.remove(Panel.WINDOW);
this.elem.classList.remove(Panel.PEEK);
this.elem.classList.add(Panel.FLOAT);
this.elem.classList.remove(Panel.RESIZED);
this.reposition();
}
toPeek() {
this.elem.classList.remove(Panel.WINDOW);
this.elem.classList.remove(Panel.FLOAT);
this.elem.classList.remove(Panel.FOCUSED);
this.elem.classList.add(Panel.PEEK);
this.elem.style.width = '';
this.elem.style.height = '';
this.elem.classList.remove(Panel.RESIZED);
}
toWindow() {
let offset = getOffset(this.elem);
offset.left += typeof window.screenLeft === 'number' ? window.screenLeft : (window.screenX + 10);
offset.top += typeof window.screenTop === 'number' ? window.screenTop : (window.screenY + 50);
let win = window.open('', this.id.replace(/-/g, '_'), 'left=' + offset.left + ',top=' + offset.top
+ ',width=' + this.elem.offsetWidth + ',height=' + this.elem.offsetHeight + ',resizable=yes,scrollbars=yes');
if (!win) {
return false;
}
let doc = win.document;
doc.write('<!DOCTYPE html><meta charset="utf-8">'
+ '<script src="' + (baseUrl.replace(/&/g, '&amp;').replace(/"/g, '&quot;')) + '_tracy_bar=js&amp;XDEBUG_SESSION_STOP=1" onload="Tracy.Dumper.init()" async></script>'
+ '<body id="tracy-debug">'
);
let meta = this.elem.parentElement.lastElementChild;
doc.body.innerHTML = '<tracy-div itemscope>'
+ '<div class="tracy-panel tracy-mode-window" id="' + this.elem.id + '">' + this.elem.dataset.tracyContent + '</div>'
+ meta.outerHTML
+ '</tracy-div>';
evalScripts(doc.body);
if (this.elem.querySelector('h1')) {
doc.title = this.elem.querySelector('h1').textContent;
}
win.addEventListener('beforeunload', () => {
this.toPeek();
win.close(); // forces closing, can be invoked by F5
});
doc.addEventListener('keyup', (e) => {
if (e.keyCode === 27 && !e.shiftKey && !e.altKey && !e.ctrlKey && !e.metaKey) {
win.close();
}
});
this.elem.classList.remove(Panel.FLOAT);
this.elem.classList.remove(Panel.PEEK);
this.elem.classList.remove(Panel.FOCUSED);
this.elem.classList.remove(Panel.RESIZED);
this.elem.classList.add(Panel.WINDOW);
this.elem.Tracy.window = win;
return true;
}
reposition(deltaX, deltaY) {
let pos = getPosition(this.elem);
if (pos.width) { // is visible?
setPosition(this.elem, {left: pos.left + (deltaX || 0), top: pos.top + (deltaY || 0)});
if (this.is(Panel.RESIZED)) {
let size = getWindowSize();
this.elem.style.width = Math.min(size.width, pos.width) + 'px';
this.elem.style.height = Math.min(size.height, pos.height) + 'px';
}
}
}
savePosition() {
let key = this.id.split(':')[0]; // remove :requestId part
let pos = getPosition(this.elem);
if (this.is(Panel.WINDOW)) {
localStorage.setItem(key, JSON.stringify({window: true}));
} else if (pos.width) { // is visible?
localStorage.setItem(key, JSON.stringify({right: pos.right, bottom: pos.bottom, width: pos.width, height: pos.height, zIndex: this.elem.style.zIndex - getOption('PanelZIndex'), resized: this.is(Panel.RESIZED)}));
} else {
localStorage.removeItem(key);
}
}
restorePosition() {
let key = this.id.split(':')[0];
let pos = JSON.parse(localStorage.getItem(key));
if (!pos) {
this.elem.classList.add(Panel.PEEK);
} else if (pos.window) {
this.init();
this.toWindow() || this.toFloat();
} else if (this.elem.dataset.tracyContent) {
this.init();
this.toFloat();
if (pos.resized) {
this.elem.classList.add(Panel.RESIZED);
this.elem.style.width = pos.width + 'px';
this.elem.style.height = pos.height + 'px';
}
setPosition(this.elem, pos);
this.elem.style.zIndex = getOption('PanelZIndex') + (pos.zIndex || 1);
Panel.zIndexCounter = Math.max(Panel.zIndexCounter, (pos.zIndex || 1)) + 1;
}
}
}
Panel.PEEK = 'tracy-mode-peek';
Panel.FLOAT = 'tracy-mode-float';
Panel.WINDOW = 'tracy-mode-window';
Panel.FOCUSED = 'tracy-focused';
Panel.RESIZED = 'tracy-panel-resized';
Panel.zIndexCounter = 1;
class Bar
{
init() {
this.id = 'tracy-debug-bar';
this.elem = document.getElementById(this.id);
draggable(this.elem, {
handles: this.elem.querySelectorAll('li:first-child'),
draggedClass: 'tracy-dragged',
stop: () => {
this.savePosition();
}
});
this.elem.addEventListener('mousedown', (e) => {
e.preventDefault();
});
this.initTabs(this.elem);
this.restorePosition();
(new MutationObserver(() => {
this.restorePosition();
})).observe(this.elem, {childList: true, characterData: true, subtree: true});
}
initTabs(elem) {
elem.querySelectorAll('a').forEach((link) => {
link.addEventListener('click', (e) => {
if (link.dataset.tracyAction === 'close') {
this.close();
} else if (link.rel) {
let panel = Debug.panels[link.rel];
panel.init();
if (e.shiftKey) {
panel.toFloat();
panel.toWindow();
} else if (panel.is(Panel.FLOAT)) {
panel.toPeek();
} else {
panel.toFloat();
if (panel.peekPosition) {
panel.reposition(-Math.round(Math.random() * 100) - 20, (Math.round(Math.random() * 100) + 20) * (this.isAtTop() ? 1 : -1));
panel.peekPosition = false;
}
}
}
e.preventDefault();
e.stopImmediatePropagation();
});
link.addEventListener('mouseenter', (e) => {
if (e.buttons || !link.rel || elem.classList.contains('tracy-dragged')) {
return;
}
clearTimeout(this.displayTimeout);
this.displayTimeout = setTimeout(() => {
let panel = Debug.panels[link.rel];
panel.focus();
if (panel.is(Panel.PEEK)) {
panel.init();
let pos = getPosition(panel.elem);
setPosition(panel.elem, {
left: getOffset(link).left + getPosition(link).width + 4 - pos.width,
top: this.isAtTop()
? getOffset(this.elem).top + getPosition(this.elem).height + 4
: getOffset(this.elem).top - pos.height - 4
});
panel.peekPosition = true;
}
}, 50);
});
link.addEventListener('mouseleave', () => {
clearTimeout(this.displayTimeout);
if (link.rel && !elem.classList.contains('tracy-dragged')) {
Debug.panels[link.rel].blur();
}
});
});
this.autoHideLabels();
}
autoHideLabels() {
let width = getWindowSize().width;
this.elem.querySelectorAll('.tracy-row').forEach((row) => {
let i, labels = row.querySelectorAll('.tracy-label');
for (i = 0; i < labels.length && row.clientWidth < width; i++) {
labels.item(i).hidden = false;
}
for (i = labels.length - 1; i >= 0 && row.clientWidth >= width; i--) {
labels.item(i).hidden = true;
}
});
}
close() {
document.getElementById('tracy-debug').style.display = 'none';
}
reposition(deltaX, deltaY) {
let pos = getPosition(this.elem);
if (pos.width) { // is visible?
setPosition(this.elem, {left: pos.left + (deltaX || 0), top: pos.top + (deltaY || 0)});
this.savePosition();
}
}
savePosition() {
let pos = getPosition(this.elem);
if (pos.width) { // is visible?
localStorage.setItem(this.id, JSON.stringify(this.isAtTop() ? {right: pos.right, top: pos.top} : {right: pos.right, bottom: pos.bottom}));
}
}
restorePosition() {
let pos = JSON.parse(localStorage.getItem(this.id));
setPosition(this.elem, pos || {right: 0, bottom: 0});
this.savePosition();
}
isAtTop() {
let pos = getPosition(this.elem);
return pos.top < 100 && pos.bottom > pos.top;
}
}
class Debug
{
static init(content) {
Debug.bar = new Bar;
Debug.panels = {};
Debug.layer = document.createElement('tracy-div');
Debug.layer.setAttribute('id', 'tracy-debug');
Debug.layer.innerHTML = content;
(document.body || document.documentElement).appendChild(Debug.layer);
evalScripts(Debug.layer);
Debug.layer.style.display = 'block';
Debug.bar.init();
Debug.layer.querySelectorAll('.tracy-panel').forEach((panel) => {
Debug.panels[panel.id] = new Panel(panel.id);
Debug.panels[panel.id].restorePosition();
});
Debug.captureWindow();
Debug.captureAjax();
Tracy.TableSort.init();
}
static loadAjax(content) {
let rows = Debug.bar.elem.querySelectorAll('.tracy-row[data-tracy-group=ajax]');
rows = Array.from(rows).reverse();
let max = getOption('MaxAjaxRows');
rows.forEach((row) => {
if (--max > 0) {
return;
}
row.querySelectorAll('a[rel]').forEach((tab) => {
let panel = Debug.panels[tab.rel];
if (panel.is(Panel.PEEK)) {
delete Debug.panels[tab.rel];
panel.elem.remove();
}
});
row.remove();
});
if (rows[0]) { // update content in first-row panels
rows[0].querySelectorAll('a[rel]').forEach((tab) => {
Debug.panels[tab.rel].savePosition();
Debug.panels[tab.rel].toPeek();
});
}
Debug.layer.insertAdjacentHTML('beforeend', content.panels);
evalScripts(Debug.layer);
Debug.bar.elem.insertAdjacentHTML('beforeend', content.bar);
let ajaxBar = Debug.bar.elem.querySelector('.tracy-row:last-child');
Debug.layer.querySelectorAll('.tracy-panel').forEach((panel) => {
if (!Debug.panels[panel.id]) {
Debug.panels[panel.id] = new Panel(panel.id);
Debug.panels[panel.id].restorePosition();
}
});
Debug.bar.initTabs(ajaxBar);
}
static captureWindow() {
let size = getWindowSize();
window.addEventListener('resize', () => {
let newSize = getWindowSize();
Debug.bar.reposition(newSize.width - size.width, newSize.height - size.height);
Debug.bar.autoHideLabels();
for (let id in Debug.panels) {
Debug.panels[id].reposition(newSize.width - size.width, newSize.height - size.height);
}
size = newSize;
});
window.addEventListener('unload', () => {
for (let id in Debug.panels) {
Debug.panels[id].savePosition();
}
});
}
static captureAjax() {
if (!requestId) {
return;
}
let oldOpen = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function() {
oldOpen.apply(this, arguments);
if (getOption('AutoRefresh') && new URL(arguments[1], location.origin).host === location.host) {
let reqId = Tracy.getAjaxHeader();
this.setRequestHeader('X-Tracy-Ajax', reqId);
this.addEventListener('load', function() {
if (this.getAllResponseHeaders().match(/^X-Tracy-Ajax: 1/mi)) {
Debug.loadScript(baseUrl + '_tracy_bar=content-ajax.' + reqId + '&XDEBUG_SESSION_STOP=1&v=' + Math.random());
}
});
}
};
let oldFetch = window.fetch;
window.fetch = function(request, options) {
request = request instanceof Request ? request : new Request(request, options || {});
let reqId = request.headers.get('X-Tracy-Ajax');
if (getOption('AutoRefresh') && !reqId && new URL(request.url, location.origin).host === location.host) {
reqId = Tracy.getAjaxHeader();
request.headers.set('X-Tracy-Ajax', reqId);
}
return oldFetch(request).then((response) => {
if (response instanceof Response && response.headers.has('X-Tracy-Ajax') && response.headers.get('X-Tracy-Ajax')[0] === '1') {
Debug.loadScript(baseUrl + '_tracy_bar=content-ajax.' + reqId + '&XDEBUG_SESSION_STOP=1&v=' + Math.random());
}
return response;
});
};
}
static loadScript(url) {
if (Debug.scriptElem) {
Debug.scriptElem.remove();
}
Debug.scriptElem = document.createElement('script');
Debug.scriptElem.src = url;
Debug.scriptElem.setAttribute('nonce', nonce);
(document.body || document.documentElement).appendChild(Debug.scriptElem);
}
}
function evalScripts(elem) {
elem.querySelectorAll('script').forEach((script) => {
if ((!script.hasAttribute('type') || script.type === 'text/javascript' || script.type === 'application/javascript') && !script.tracyEvaluated) {
let document = script.ownerDocument;
let dolly = document.createElement('script');
dolly.textContent = script.textContent;
dolly.setAttribute('nonce', nonce);
(document.body || document.documentElement).appendChild(dolly);
script.tracyEvaluated = true;
}
});
}
let dragging;
function draggable(elem, options) {
let dE = document.documentElement, started, deltaX, deltaY, clientX, clientY;
options = options || {};
let redraw = function () {
if (dragging) {
setPosition(elem, {left: clientX + deltaX, top: clientY + deltaY});
requestAnimationFrame(redraw);
}
};
let onMove = function(e) {
if (e.buttons === 0) {
return onEnd(e);
}
if (!started) {
if (options.draggedClass) {
elem.classList.add(options.draggedClass);
}
if (options.start) {
options.start(e, elem);
}
started = true;
}
clientX = e.touches ? e.touches[0].clientX : e.clientX;
clientY = e.touches ? e.touches[0].clientY : e.clientY;
return false;
};
let onEnd = function(e) {
if (started) {
if (options.draggedClass) {
elem.classList.remove(options.draggedClass);
}
if (options.stop) {
options.stop(e, elem);
}
}
dragging = null;
dE.removeEventListener('mousemove', onMove);
dE.removeEventListener('mouseup', onEnd);
dE.removeEventListener('touchmove', onMove);
dE.removeEventListener('touchend', onEnd);
return false;
};
let onStart = function(e) {
e.preventDefault();
e.stopPropagation();
if (dragging) { // missed mouseup out of window?
return onEnd(e);
}
let pos = getPosition(elem);
clientX = e.touches ? e.touches[0].clientX : e.clientX;
clientY = e.touches ? e.touches[0].clientY : e.clientY;
deltaX = pos.left - clientX;
deltaY = pos.top - clientY;
dragging = true;
started = false;
dE.addEventListener('mousemove', onMove);
dE.addEventListener('mouseup', onEnd);
dE.addEventListener('touchmove', onMove);
dE.addEventListener('touchend', onEnd);
requestAnimationFrame(redraw);
if (options.start) {
options.start(e, elem);
}
};
options.handles.forEach((handle) => {
handle.addEventListener('mousedown', onStart);
handle.addEventListener('touchstart', onStart);
handle.addEventListener('click', (e) => {
if (started) {
e.stopImmediatePropagation();
}
});
});
}
// returns total offset for element
function getOffset(elem) {
let res = {left: elem.offsetLeft, top: elem.offsetTop};
while (elem = elem.offsetParent) { // eslint-disable-line no-cond-assign
res.left += elem.offsetLeft; res.top += elem.offsetTop;
}
return res;
}
function getWindowSize() {
return {
width: document.documentElement.clientWidth,
height: document.compatMode === 'BackCompat' ? window.innerHeight : document.documentElement.clientHeight
};
}
// move to new position
function setPosition(elem, coords) {
let win = getWindowSize();
if (typeof coords.right !== 'undefined') {
coords.left = win.width - elem.offsetWidth - coords.right;
}
if (typeof coords.bottom !== 'undefined') {
coords.top = win.height - elem.offsetHeight - coords.bottom;
}
elem.style.left = Math.max(0, Math.min(coords.left, win.width - elem.offsetWidth)) + 'px';
elem.style.top = Math.max(0, Math.min(coords.top, win.height - elem.offsetHeight)) + 'px';
}
// returns current position
function getPosition(elem) {
let win = getWindowSize();
return {
left: elem.offsetLeft,
top: elem.offsetTop,
right: win.width - elem.offsetWidth - elem.offsetLeft,
bottom: win.height - elem.offsetHeight - elem.offsetTop,
width: elem.offsetWidth,
height: elem.offsetHeight
};
}
let Tracy = window.Tracy = window.Tracy || {};
Tracy.DebugPanel = Panel;
Tracy.DebugBar = Bar;
Tracy.Debug = Debug;
Tracy.getAjaxHeader = () => requestId + '_' + ajaxCounter++;

View File

@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace Tracy;
/**
* @var string $type
* @var \stdClass[] $panels
*/
?>
<ul class="tracy-row" data-tracy-group="<?= Helpers::escapeHtml($type) ?>">
<?php if ($type === 'main'): ?>
<li id="tracy-debug-logo" title="Tracy Debugger <?= Debugger::VERSION, " \nhttps://tracy.nette.org" ?>">
<svg viewBox="0 -10 1561 333"><path fill="#585755" d="m176 327h-57v-269h-119v-57h291v57h-115v269zm208-191h114c50 0 47-78 0-78h-114v78zm106-135c17 0 33 2 46 7 75 30 75 144 1 175-13 6-29 8-47 8h-27l132 74v68l-211-128v122h-57v-326h163zm300 57c-5 0-9 3-11 9l-56 156h135l-55-155c-2-7-6-10-13-10zm-86 222l-17 47h-61l102-285c20-56 107-56 126 0l102 285h-61l-17-47h-174zm410 47c-98 0-148-55-148-163v-2c0-107 50-161 149-161h118v57h-133c-26 0-45 8-58 25-12 17-19 44-19 81 0 71 26 106 77 106h133v57h-119zm270-145l-121-181h68l81 130 81-130h68l-121 178v148h-56v-145z"/></svg>
</li>
<?php endif; if ($type === 'redirect'): ?>
<li><span title="Previous request before redirect">redirect</span></li>
<?php endif; if ($type === 'ajax'): ?>
<li>AJAX</li>
<?php endif ?>
<?php foreach ($panels as $panel): if ($panel->tab) { ?>
<li><?php if ($panel->panel): ?><a href="#" rel="tracy-debug-panel-<?= $panel->id ?>"><?= trim($panel->tab) ?></a><?php else: echo '<span>', trim($panel->tab), '</span>'; endif ?></li>
<?php } endforeach ?>
<?php if ($type === 'main'): ?>
<li><a href="#" data-tracy-action="close" title="close debug bar">&times;</a></li>
<?php endif ?>
</ul>

View File

@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Tracy;
/**
* @var ?string $nonce
* @var bool $async
* @var string $requestId
*/
$baseUrl = $_SERVER['REQUEST_URI'] ?? '';
$baseUrl .= strpos($baseUrl, '?') === false ? '?' : '&';
$nonceAttr = $nonce ? ' nonce="' . Helpers::escapeHtml($nonce) . '"' : '';
$asyncAttr = $async ? ' async' : '';
?>
<?php if (empty($content)): ?>
<script src="<?= Helpers::escapeHtml($baseUrl) ?>_tracy_bar=<?= urlencode("content.$requestId") ?>&amp;XDEBUG_SESSION_STOP=1" data-id="<?= Helpers::escapeHtml($requestId) ?>"<?= $asyncAttr, $nonceAttr ?>></script>
<?php else: ?>
<!-- Tracy Debug Bar -->
<script src="<?= Helpers::escapeHtml($baseUrl) ?>_tracy_bar=js&amp;v=<?= urlencode(Debugger::VERSION) ?>&amp;XDEBUG_SESSION_STOP=1" data-id="<?= Helpers::escapeHtml($requestId) ?>"<?= $nonceAttr ?>></script>
<script<?= $nonceAttr ?>>
Tracy.Debug.init(<?= str_replace(['<!--', '</s'], ['<\!--', '<\/s'], json_encode($content, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE)) ?>);
</script>
<?php endif ?>

View File

@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Tracy;
use Tracy\Helpers;
/**
* @var string $type
* @var \stdClass[] $panels
*/
$icons = '
<div class="tracy-icons">
<a href="#" data-tracy-action="window" title="open in window">&curren;</a>
<a href="#" data-tracy-action="close" title="close window">&times;</a>
</div>
';
echo '<div itemscope>';
foreach ($panels as $panel) {
$content = $panel->panel ? ($panel->panel . "\n" . $icons) : '';
$class = 'tracy-panel ' . ($type === 'ajax' ? '' : 'tracy-panel-persist') . ' tracy-panel-' . $type; ?>
<div class="<?= $class ?>" id="tracy-debug-panel-<?= $panel->id ?>" data-tracy-content='<?= str_replace(['&', "'"], ['&amp;', '&#039;'], $content) ?>'></div><?php
}
echo '<meta itemprop=tracy-snapshot content=', Dumper::formatSnapshotAttribute(Dumper::$liveSnapshot), '>';
echo '</div>';

View File

@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace Tracy;
/** @var array[] $data */
?>
<style class="tracy-debug">
#tracy-debug .tracy-DumpPanel h2 {
font: 11pt/1.5 sans-serif;
margin: 0;
padding: 2px 8px;
background: #3484d2;
color: white;
}
</style>
<h1>Dumps</h1>
<div class="tracy-inner tracy-DumpPanel">
<?php foreach ($data as $item): ?>
<?php if ($item['title']):?>
<h2><?= Helpers::escapeHtml($item['title']) ?></h2>
<?php endif ?>
<?= $item['dump'] ?>
<?php endforeach ?>
</div>

View File

@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
namespace Tracy;
/** @var array[] $data */
if (empty($data)) {
return;
}
?>
<svg viewBox="0 0 2048 2048"><path fill="#154ABD" d="m1084 540c-110-1-228-2-325 58-54 35-87 94-126 143-94 162-71 383 59 519 83 94 207 151 333 149 132 3 261-60 344-160 122-138 139-355 44-511-73-66-133-158-234-183-31-9-65-9-95-14zm-60 116c73 0 53 115-16 97-105 5-195 102-192 207-2 78-122 48-95-23 8-153 151-285 304-280l-1-1zM1021 511"/><path fill="#4B6193" d="m1021 511c-284-2-560 131-746 344-53 64-118 125-145 206-16 86 59 152 103 217 219 267 575 428 921 377 312-44 600-241 755-515 39-81-30-156-74-217-145-187-355-327-581-384-77-19-156-29-234-28zm0 128c263-4 512 132 679 330 33 52 132 110 58 168-170 237-449 409-747 399-309 0-590-193-752-447 121-192 305-346 526-407 75-25 170-38 237-43z"/>
</svg><span class="tracy-label">dumps</span>

View File

@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace Tracy;
/** @var int[] $data */
?>
<h1>Errors</h1>
<div class="tracy-inner">
<table class="tracy-sortable">
<tr><th>Count</th><th>Error</th></tr>
<?php foreach ($data as $item => $count): [$file, $line, $message] = explode('|', $item, 3) ?>
<tr>
<td class="tracy-right"><?= $count ? "$count\xC3\x97" : '' ?></td>
<td><pre><?= Helpers::escapeHtml($message), ' in ', Helpers::editorLink($file, (int) $line) ?></pre></td>
</tr>
<?php endforeach ?>
</table>
</div>

View File

@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace Tracy;
/** @var int[] $data */
if (empty($data)) {
return;
}
?>
<style class="tracy-debug">
#tracy-debug .tracy-ErrorTab {
display: block;
background: #D51616;
color: white;
font-weight: bold;
margin: -1px -.4em;
padding: 1px .4em;
}
</style>
<span class="tracy-ErrorTab">
<svg viewBox="0 0 2048 2048"><path fill="#fff" d="M1152 1503v-190q0-14-9.5-23.5t-22.5-9.5h-192q-13 0-22.5 9.5t-9.5 23.5v190q0 14 9.5 23.5t22.5 9.5h192q13 0 22.5-9.5t9.5-23.5zm-2-374l18-459q0-12-10-19-13-11-24-11h-220q-11 0-24 11-10 7-10 21l17 457q0 10 10 16.5t24 6.5h185q14 0 23.5-6.5t10.5-16.5zm-14-934l768 1408q35 63-2 126-17 29-46.5 46t-63.5 17h-1536q-34 0-63.5-17t-46.5-46q-37-63-2-126l768-1408q17-31 47-49t65-18 65 18 47 49z"/>
</svg><span class="tracy-label"><?= $sum = array_sum($data), $sum > 1 ? ' errors' : ' error' ?></span>
</span>

View File

@ -0,0 +1,125 @@
<?php
declare(strict_types=1);
namespace Tracy;
/** @var DefaultBarPanel $this */
if (isset($this->cpuUsage) && $this->time) {
foreach (getrusage() as $key => $val) {
$this->cpuUsage[$key] -= $val;
}
$userUsage = -round(($this->cpuUsage['ru_utime.tv_sec'] * 1e6 + $this->cpuUsage['ru_utime.tv_usec']) / $this->time / 10000);
$systemUsage = -round(($this->cpuUsage['ru_stime.tv_sec'] * 1e6 + $this->cpuUsage['ru_stime.tv_usec']) / $this->time / 10000);
}
$countClasses = function (array $list): int {
return count(array_filter($list, function (string $name): bool {
return (new \ReflectionClass($name))->isUserDefined();
}));
};
$ipFormatter = static function (?string $ip): ?string {
if ($ip === '127.0.0.1' || $ip === '::1') {
$ip .= ' (localhost)';
}
return $ip;
};
$opcache = function_exists('opcache_get_status') ? @opcache_get_status() : null; // @ can be restricted
$cachedFiles = isset($opcache['scripts']) ? array_intersect(array_keys($opcache['scripts']), get_included_files()) : [];
$jit = $opcache['jit'] ?? null;
$info = [
'Execution time' => number_format($this->time * 1000, 1, '.', "\u{202f}") . "\u{202f}ms",
'CPU usage user + system' => isset($userUsage) ? (int) $userUsage . "\u{202f}% + " . (int) $systemUsage . "\u{202f}%" : null,
'Peak of allocated memory' => number_format(memory_get_peak_usage() / 1000000, 2, '.', "\u{202f}") . "\u{202f}MB",
'Included files' => count(get_included_files()),
'Classes + interfaces + traits' => $countClasses(get_declared_classes()) . ' + '
. $countClasses(get_declared_interfaces()) . ' + ' . $countClasses(get_declared_traits()),
'OPcache' => $opcache ? round(count($cachedFiles) * 100 / count(get_included_files())) . "\u{202f}% cached" : '',
'JIT' => empty($jit['buffer_size']) ? '' : round(($jit['buffer_size'] - $jit['buffer_free']) / $jit['buffer_size'] * 100) . "\u{202f}% used",
'Your IP' => $ipFormatter($_SERVER['REMOTE_ADDR'] ?? null),
'Server IP' => $ipFormatter($_SERVER['SERVER_ADDR'] ?? null),
'HTTP method / response code' => isset($_SERVER['REQUEST_METHOD']) ? $_SERVER['REQUEST_METHOD'] . ' / ' . http_response_code() : null,
'PHP' => PHP_VERSION,
'Xdebug' => extension_loaded('xdebug') ? phpversion('xdebug') : null,
'Tracy' => Debugger::VERSION,
'Server' => $_SERVER['SERVER_SOFTWARE'] ?? null,
];
$info = array_map('strval', array_filter($info + (array) $this->data));
$packages = $devPackages = [];
if (class_exists('Composer\Autoload\ClassLoader', false)) {
$baseDir = (function () {
@include dirname((new \ReflectionClass('Composer\Autoload\ClassLoader'))->getFileName()) . '/autoload_psr4.php'; // @ may not exist
return $baseDir;
})();
$composer = @json_decode((string) file_get_contents($baseDir . '/composer.lock')); // @ may not exist or be valid
[$packages, $devPackages] = [(array) @$composer->packages, (array) @$composer->{'packages-dev'}]; // @ keys may not exist
foreach ([&$packages, &$devPackages] as &$items) {
array_walk($items, function ($package) {
$package->hash = $package->source->reference ?? $package->dist->reference ?? null;
}, $items);
usort($items, function ($a, $b): int { return $a->name <=> $b->name; });
}
}
?>
<style class="tracy-debug">
#tracy-debug .tracy-InfoPanel td {
white-space: nowrap;
}
#tracy-debug .tracy-InfoPanel td:nth-child(2) {
font-weight: bold;
width: 60%;
}
#tracy-debug .tracy-InfoPanel td[colspan='2'] b {
float: right;
margin-left: 2em;
white-space: normal;
}
</style>
<h1>System info</h1>
<div class="tracy-inner tracy-InfoPanel">
<div class="tracy-inner-container">
<table class="tracy-sortable">
<?php foreach ($info as $key => $val): ?>
<tr>
<?php if (strlen($val) > 25): ?>
<td colspan=2><?= Helpers::escapeHtml($key) ?> <b><?= Helpers::escapeHtml($val) ?></b></td>
<?php else: ?>
<td><?= Helpers::escapeHtml($key) ?></td><td><?= Helpers::escapeHtml($val) ?></td>
<?php endif ?>
</tr>
<?php endforeach ?>
</table>
<?php if ($packages || $devPackages): ?>
<h2><a class="tracy-toggle tracy-collapsed" data-tracy-ref="^div .tracy-InfoPanel-packages">Composer Packages (<?= count($packages), $devPackages ? ' + ' . count($devPackages) . ' dev' : '' ?>)</a></h2>
<div class="tracy-InfoPanel-packages tracy-collapsed">
<?php if ($packages): ?>
<table class="tracy-sortable">
<?php foreach ($packages as $package): ?>
<tr><td><?= Helpers::escapeHtml($package->name) ?></td><td><?= Helpers::escapeHtml($package->version . (strpos($package->version, 'dev') !== false && $package->hash ? ' #' . substr($package->hash, 0, 4) : '')) ?></td></tr>
<?php endforeach ?>
</table>
<?php endif ?>
<?php if ($devPackages): ?>
<h2>Dev Packages</h2>
<table class="tracy-sortable">
<?php foreach ($devPackages as $package): ?>
<tr><td><?= Helpers::escapeHtml($package->name) ?></td><td><?= Helpers::escapeHtml($package->version . (strpos($package->version, 'dev') !== false && $package->hash ? ' #' . substr($package->hash, 0, 4) : '')) ?></td></tr>
<?php endforeach ?>
</table>
<?php endif ?>
</div>
<?php endif ?>
</div>
</div>

View File

@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace Tracy;
/** @var DefaultBarPanel $this */
$this->time = microtime(true) - Debugger::$time;
?>
<span title="Execution time">
<svg viewBox="0 0 2048 2048"><path fill="#86bbf0" d="m640 1153.6v639.3h-256v-639.3z"/><path fill="#6ba9e6" d="m1024 254.68v1538.2h-256v-1538.2z"/><path fill="#4f96dc" d="m1408 897.57v894.3h-256v-894.3z"/><path fill="#3987d4" d="m1792 513.08v1279.8h-256v-1279.8z"/>
</svg><span class="tracy-label"><?= number_format($this->time * 1000, 1, '.', "\u{202f}") ?> ms</span>
</span>

View File

@ -0,0 +1,626 @@
<?php
/**
* This file is part of the Tracy (https://tracy.nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Tracy;
/**
* Red BlueScreen.
*/
class BlueScreen
{
private const MaxMessageLength = 2000;
/** @var string[] */
public $info = [];
/** @var string[] paths to be collapsed in stack trace (e.g. core libraries) */
public $collapsePaths = [];
/** @var int */
public $maxDepth = 5;
/** @var int */
public $maxLength = 150;
/** @var int */
public $maxItems = 100;
/** @var callable|null a callable returning true for sensitive data; fn(string $key, mixed $val): bool */
public $scrubber;
/** @var string[] */
public $keysToHide = [
'password', 'passwd', 'pass', 'pwd', 'creditcard', 'credit card', 'cc', 'pin', 'authorization',
self::class . '::$snapshot',
];
/** @var bool */
public $showEnvironment = true;
/** @var callable[] */
private $panels = [];
/** @var callable[] functions that returns action for exceptions */
private $actions = [];
/** @var callable[] */
private $fileGenerators = [];
/** @var array */
private $snapshot;
/** @var \WeakMap<\Fiber|\Generator> */
private $fibers;
public function __construct()
{
$this->collapsePaths = preg_match('#(.+/vendor)/tracy/tracy/src/Tracy/BlueScreen$#', strtr(__DIR__, '\\', '/'), $m)
? [$m[1] . '/tracy', $m[1] . '/nette', $m[1] . '/latte']
: [dirname(__DIR__)];
$this->fileGenerators[] = [self::class, 'generateNewPhpFileContents'];
$this->fibers = PHP_VERSION_ID < 80000 ? new \SplObjectStorage : new \WeakMap;
}
/**
* Add custom panel as function (?\Throwable $e): ?array
* @return static
*/
public function addPanel(callable $panel): self
{
if (!in_array($panel, $this->panels, true)) {
$this->panels[] = $panel;
}
return $this;
}
/**
* Add action.
* @return static
*/
public function addAction(callable $action): self
{
$this->actions[] = $action;
return $this;
}
/**
* Add new file generator.
* @param callable(string): ?string $generator
* @return static
*/
public function addFileGenerator(callable $generator): self
{
$this->fileGenerators[] = $generator;
return $this;
}
/**
* @param \Fiber|\Generator $fiber
* @return static
*/
public function addFiber($fiber): self
{
$this->fibers[$fiber] = true;
return $this;
}
/**
* Renders blue screen.
*/
public function render(\Throwable $exception): void
{
if (!headers_sent()) {
header('Content-Type: text/html; charset=UTF-8');
}
$this->renderTemplate($exception, __DIR__ . '/assets/page.phtml');
}
/** @internal */
public function renderToAjax(\Throwable $exception, DeferredContent $defer): void
{
$defer->addSetup('Tracy.BlueScreen.loadAjax', Helpers::capture(function () use ($exception) {
$this->renderTemplate($exception, __DIR__ . '/assets/content.phtml');
}));
}
/**
* Renders blue screen to file (if file exists, it will not be overwritten).
*/
public function renderToFile(\Throwable $exception, string $file): bool
{
if ($handle = @fopen($file, 'x')) {
ob_start(); // double buffer prevents sending HTTP headers in some PHP
ob_start(function ($buffer) use ($handle): void { fwrite($handle, $buffer); }, 4096);
$this->renderTemplate($exception, __DIR__ . '/assets/page.phtml', false);
ob_end_flush();
ob_end_clean();
fclose($handle);
return true;
}
return false;
}
private function renderTemplate(\Throwable $exception, string $template, $toScreen = true): void
{
[$generators, $fibers] = $this->findGeneratorsAndFibers($exception);
$headersSent = headers_sent($headersFile, $headersLine);
$obStatus = Debugger::$obStatus;
$showEnvironment = $this->showEnvironment && (strpos($exception->getMessage(), 'Allowed memory size') === false);
$info = array_filter($this->info);
$source = Helpers::getSource();
$title = $exception instanceof \ErrorException
? Helpers::errorTypeToString($exception->getSeverity())
: Helpers::getClass($exception);
$lastError = $exception instanceof \ErrorException || $exception instanceof \Error
? null
: error_get_last();
if (function_exists('apache_request_headers')) {
$httpHeaders = apache_request_headers();
} else {
$httpHeaders = array_filter($_SERVER, function ($k) { return strncmp($k, 'HTTP_', 5) === 0; }, ARRAY_FILTER_USE_KEY);
$httpHeaders = array_combine(array_map(function ($k) { return strtolower(strtr(substr($k, 5), '_', '-')); }, array_keys($httpHeaders)), $httpHeaders);
}
$snapshot = &$this->snapshot;
$snapshot = [];
$dump = $this->getDumper();
$css = array_map('file_get_contents', array_merge([
__DIR__ . '/../assets/reset.css',
__DIR__ . '/assets/bluescreen.css',
__DIR__ . '/../assets/toggle.css',
__DIR__ . '/../assets/table-sort.css',
__DIR__ . '/../assets/tabs.css',
__DIR__ . '/../Dumper/assets/dumper-light.css',
], Debugger::$customCssFiles));
$css = Helpers::minifyCss(implode('', $css));
$nonce = $toScreen ? Helpers::getNonce() : null;
$actions = $toScreen ? $this->renderActions($exception) : [];
require $template;
}
/**
* @return \stdClass[]
*/
private function renderPanels(?\Throwable $ex): array
{
$obLevel = ob_get_level();
$res = [];
foreach ($this->panels as $callback) {
try {
$panel = $callback($ex);
if (empty($panel['tab']) || empty($panel['panel'])) {
continue;
}
$res[] = (object) $panel;
continue;
} catch (\Throwable $e) {
}
while (ob_get_level() > $obLevel) { // restore ob-level if broken
ob_end_clean();
}
is_callable($callback, true, $name);
$res[] = (object) [
'tab' => "Error in panel $name",
'panel' => nl2br(Helpers::escapeHtml($e)),
];
}
return $res;
}
/**
* @return array[]
*/
private function renderActions(\Throwable $ex): array
{
$actions = [];
foreach ($this->actions as $callback) {
$action = $callback($ex);
if (!empty($action['link']) && !empty($action['label'])) {
$actions[] = $action;
}
}
if (
property_exists($ex, 'tracyAction')
&& !empty($ex->tracyAction['link'])
&& !empty($ex->tracyAction['label'])
) {
$actions[] = $ex->tracyAction;
}
if (preg_match('# ([\'"])(\w{3,}(?:\\\\\w{3,})+)\1#i', $ex->getMessage(), $m)) {
$class = $m[2];
if (
!class_exists($class, false) && !interface_exists($class, false) && !trait_exists($class, false)
&& ($file = Helpers::guessClassFile($class)) && !@is_file($file) // @ - may trigger error
) {
[$content, $line] = $this->generateNewFileContents($file, $class);
$actions[] = [
'link' => Helpers::editorUri($file, $line, 'create', '', $content),
'label' => 'create class',
];
}
}
if (preg_match('# ([\'"])((?:/|[a-z]:[/\\\\])\w[^\'"]+\.\w{2,5})\1#i', $ex->getMessage(), $m)) {
$file = $m[2];
if (@is_file($file)) { // @ - may trigger error
$label = 'open';
$content = '';
$line = 1;
} else {
$label = 'create';
[$content, $line] = $this->generateNewFileContents($file);
}
$actions[] = [
'link' => Helpers::editorUri($file, $line, $label, '', $content),
'label' => $label . ' file',
];
}
$query = ($ex instanceof \ErrorException ? '' : Helpers::getClass($ex) . ' ')
. preg_replace('#\'.*\'|".*"#Us', '', $ex->getMessage());
$actions[] = [
'link' => 'https://www.google.com/search?sourceid=tracy&q=' . urlencode($query),
'label' => 'search',
'external' => true,
];
if (
$ex instanceof \ErrorException
&& !empty($ex->skippable)
&& preg_match('#^https?://#', $source = Helpers::getSource())
) {
$actions[] = [
'link' => $source . (strpos($source, '?') ? '&' : '?') . '_tracy_skip_error',
'label' => 'skip error',
];
}
return $actions;
}
/**
* Returns syntax highlighted source code.
*/
public static function highlightFile(
string $file,
int $line,
int $lines = 15,
bool $php = true,
int $column = 0
): ?string
{
$source = @file_get_contents($file); // @ file may not exist
if ($source === false) {
return null;
}
$source = $php
? static::highlightPhp($source, $line, $lines, $column)
: '<pre class=tracy-code><div>' . static::highlightLine(htmlspecialchars($source, ENT_IGNORE, 'UTF-8'), $line, $lines, $column) . '</div></pre>';
if ($editor = Helpers::editorUri($file, $line)) {
$source = substr_replace($source, ' title="Ctrl-Click to open in editor" data-tracy-href="' . Helpers::escapeHtml($editor) . '"', 4, 0);
}
return $source;
}
/**
* Returns syntax highlighted source code.
*/
public static function highlightPhp(string $source, int $line, int $lines = 15, int $column = 0): string
{
if (function_exists('ini_set')) {
ini_set('highlight.comment', '#998; font-style: italic');
ini_set('highlight.default', '#000');
ini_set('highlight.html', '#06B');
ini_set('highlight.keyword', '#D24; font-weight: bold');
ini_set('highlight.string', '#080');
}
$source = preg_replace('#(__halt_compiler\s*\(\)\s*;).*#is', '$1', $source);
$source = str_replace(["\r\n", "\r"], "\n", $source);
$source = explode("\n", highlight_string($source, true));
$out = $source[0]; // <code><span color=highlight.html>
$source = str_replace('<br />', "\n", $source[1]);
$out .= static::highlightLine($source, $line, $lines, $column);
$out = str_replace('&nbsp;', ' ', $out) . '</code>';
return "<pre class='tracy-code'><div>$out</div></pre>";
}
/**
* Returns highlighted line in HTML code.
*/
public static function highlightLine(string $html, int $line, int $lines = 15, int $column = 0): string
{
$source = explode("\n", "\n" . str_replace("\r\n", "\n", $html));
$out = '';
$spans = 1;
$start = $i = max(1, min($line, count($source) - 1) - (int) floor($lines * 2 / 3));
while (--$i >= 1) { // find last highlighted block
if (preg_match('#.*(</?span[^>]*>)#', $source[$i], $m)) {
if ($m[1] !== '</span>') {
$spans++;
$out .= $m[1];
}
break;
}
}
$source = array_slice($source, $start, $lines, true);
end($source);
$numWidth = strlen((string) key($source));
foreach ($source as $n => $s) {
$spans += substr_count($s, '<span') - substr_count($s, '</span');
$s = str_replace(["\r", "\n"], ['', ''], $s);
preg_match_all('#<[^>]+>#', $s, $tags);
if ($n == $line) {
$s = strip_tags($s);
if ($column) {
$s = preg_replace(
'#((?:&.*?;|[^&]){' . ($column - 1) . '})(&.*?;|.)#u',
'\1<span class="tracy-column-highlight">\2</span>',
$s . ' ',
1
);
}
$out .= sprintf(
"<span class='tracy-line-highlight'>%{$numWidth}s: %s\n</span>%s",
$n,
$s,
implode('', $tags[0])
);
} else {
$out .= sprintf("<span class='tracy-line'>%{$numWidth}s:</span> %s\n", $n, $s);
}
}
$out .= str_repeat('</span>', $spans);
return $out;
}
/**
* Returns syntax highlighted source code to Terminal.
*/
public static function highlightPhpCli(string $file, int $line, int $lines = 15, int $column = 0): ?string
{
$source = @file_get_contents($file); // @ file may not exist
if ($source === false) {
return null;
}
$s = self::highlightPhp($source, $line, $lines);
$colors = [
'color: ' . ini_get('highlight.comment') => '1;30',
'color: ' . ini_get('highlight.default') => '1;36',
'color: ' . ini_get('highlight.html') => '1;35',
'color: ' . ini_get('highlight.keyword') => '1;37',
'color: ' . ini_get('highlight.string') => '1;32',
'tracy-line' => '1;30',
'tracy-line-highlight' => "1;37m\e[41",
];
$stack = ['0'];
$s = preg_replace_callback(
'#<\w+(?: (class|style)=["\'](.*?)["\'])?[^>]*>|</\w+>#',
function ($m) use ($colors, &$stack): string {
if ($m[0][1] === '/') {
array_pop($stack);
} else {
$stack[] = isset($m[2], $colors[$m[2]]) ? $colors[$m[2]] : '0';
}
return "\e[0m\e[" . end($stack) . 'm';
},
$s
);
$s = htmlspecialchars_decode(strip_tags($s), ENT_QUOTES | ENT_HTML5);
return $s;
}
/**
* Should a file be collapsed in stack trace?
* @internal
*/
public function isCollapsed(string $file): bool
{
$file = strtr($file, '\\', '/') . '/';
foreach ($this->collapsePaths as $path) {
$path = strtr($path, '\\', '/') . '/';
if (strncmp($file, $path, strlen($path)) === 0) {
return true;
}
}
return false;
}
/** @internal */
public function getDumper(): \Closure
{
return function ($v, $k = null): string {
return Dumper::toHtml($v, [
Dumper::DEPTH => $this->maxDepth,
Dumper::TRUNCATE => $this->maxLength,
Dumper::ITEMS => $this->maxItems,
Dumper::SNAPSHOT => &$this->snapshot,
Dumper::LOCATION => Dumper::LOCATION_CLASS,
Dumper::SCRUBBER => $this->scrubber,
Dumper::KEYS_TO_HIDE => $this->keysToHide,
], $k);
};
}
public function formatMessage(\Throwable $exception): string
{
$msg = Helpers::encodeString(trim((string) $exception->getMessage()), self::MaxMessageLength, false);
// highlight 'string'
$msg = preg_replace(
'#\'\S(?:[^\']|\\\\\')*\S\'|"\S(?:[^"]|\\\\")*\S"#',
'<i>$0</i>',
$msg
);
// clickable class & methods
$msg = preg_replace_callback(
'#(\w+\\\\[\w\\\\]+\w)(?:::(\w+))?#',
function ($m) {
if (isset($m[2]) && method_exists($m[1], $m[2])) {
$r = new \ReflectionMethod($m[1], $m[2]);
} elseif (class_exists($m[1], false) || interface_exists($m[1], false)) {
$r = new \ReflectionClass($m[1]);
}
if (empty($r) || !$r->getFileName()) {
return $m[0];
}
return '<a href="' . Helpers::escapeHtml(Helpers::editorUri($r->getFileName(), $r->getStartLine())) . '" class="tracy-editor">' . $m[0] . '</a>';
},
$msg
);
// clickable file name
$msg = preg_replace_callback(
'#([\w\\\\/.:-]+\.(?:php|phpt|phtml|latte|neon))(?|:(\d+)| on line (\d+))?#',
function ($m) {
return @is_file($m[1])
? '<a href="' . Helpers::escapeHtml(Helpers::editorUri($m[1], isset($m[2]) ? (int) $m[2] : null)) . '" class="tracy-editor">' . $m[0] . '</a>'
: $m[0];
},
$msg
);
return $msg;
}
private function renderPhpInfo(): void
{
ob_start();
@phpinfo(INFO_LICENSE); // @ phpinfo may be disabled
$license = ob_get_clean();
ob_start();
@phpinfo(INFO_CONFIGURATION | INFO_MODULES); // @ phpinfo may be disabled
$info = ob_get_clean();
if (strpos($license, '<body') === false) {
echo '<pre class="tracy-dump tracy-light">', Helpers::escapeHtml($info), '</pre>';
} else {
$info = str_replace('<table', '<table class="tracy-sortable"', $info);
echo preg_replace('#^.+<body>|</body>.+\z|<hr />|<h1>Configuration</h1>#s', '', $info);
}
}
/** @internal */
private function generateNewFileContents(string $file, ?string $class = null): array
{
foreach (array_reverse($this->fileGenerators) as $generator) {
$content = $generator($file, $class);
if ($content !== null) {
$line = 1;
$pos = strpos($content, '$END$');
if ($pos !== false) {
$content = substr_replace($content, '', $pos, 5);
$line = substr_count($content, "\n", 0, $pos) + 1;
}
return [$content, $line];
}
}
return ['', 1];
}
/** @internal */
public static function generateNewPhpFileContents(string $file, ?string $class = null): ?string
{
if (substr($file, -4) !== '.php') {
return null;
}
$res = "<?php\n\ndeclare(strict_types=1);\n\n";
if (!$class) {
return $res . '$END$';
}
if ($pos = strrpos($class, '\\')) {
$res .= 'namespace ' . substr($class, 0, $pos) . ";\n\n";
$class = substr($class, $pos + 1);
}
return $res . "class $class\n{\n\$END\$\n}\n";
}
private function findGeneratorsAndFibers(object $object): array
{
$generators = $fibers = [];
$add = function ($obj) use (&$generators, &$fibers) {
if ($obj instanceof \Generator) {
try {
new \ReflectionGenerator($obj);
$generators[spl_object_id($obj)] = $obj;
} catch (\ReflectionException $e) {
}
} elseif ($obj instanceof \Fiber && $obj->isStarted() && !$obj->isTerminated()) {
$fibers[spl_object_id($obj)] = $obj;
}
};
foreach ($this->fibers as $k => $v) {
$add($this->fibers instanceof \WeakMap ? $k : $v);
}
if (PHP_VERSION_ID >= 80000) {
Helpers::traverseValue($object, $add);
}
return [$generators, $fibers];
}
}

View File

@ -0,0 +1,418 @@
/**
* This file is part of the Tracy (https://tracy.nette.org)
*/
:root {
--tracy-space: 16px;
}
@media (max-width: 600px) {
:root {
--tracy-space: 8px;
}
}
html.tracy-bs-visible,
html.tracy-bs-visible body {
display: block;
overflow: auto;
}
#tracy-bs {
font: 9pt/1.5 Verdana, sans-serif;
background: white;
color: #333;
position: absolute;
z-index: 20000;
left: 0;
top: 0;
width: 100%;
text-align: left;
}
#tracy-bs a {
text-decoration: none;
color: #328ADC;
padding: 0 4px;
margin: 0 -4px;
}
#tracy-bs a + a {
margin-left: 0;
}
#tracy-bs a:hover,
#tracy-bs a:focus {
color: #085AA3;
}
#tracy-bs-toggle {
position: absolute;
right: .5em;
top: .5em;
text-decoration: none;
background: #CD1818;
color: white !important;
padding: 3px;
}
#tracy-bs-toggle.tracy-collapsed {
position: fixed;
}
.tracy-bs-main {
display: flex;
flex-direction: column;
padding-bottom: 80vh;
}
.tracy-bs-main.tracy-collapsed {
display: none;
}
#tracy-bs p,
#tracy-bs table,
#tracy-bs pre,
#tracy-bs h1,
#tracy-bs h2,
#tracy-bs h3 {
margin: 0 0 var(--tracy-space);
}
#tracy-bs h1 {
font-size: 15pt;
font-weight: normal;
text-shadow: 1px 1px 2px rgba(0, 0, 0, .3);
}
#tracy-bs h1 span {
white-space: pre-wrap;
}
#tracy-bs h2 {
font-size: 14pt;
font-weight: normal;
margin-top: var(--tracy-space);
}
#tracy-bs h3 {
font-size: 10pt;
font-weight: bold;
}
#tracy-bs pre,
#tracy-bs code,
#tracy-bs table {
font: 9pt/1.5 Consolas, monospace !important;
}
#tracy-bs pre,
#tracy-bs table {
background: #FDF5CE;
padding: .4em .7em;
border: 2px solid #ffffffa6;
box-shadow: 1px 2px 6px #00000005;
overflow: auto;
}
#tracy-bs table pre {
padding: 0;
margin: 0;
border: none;
box-shadow: none;
}
#tracy-bs table {
border-collapse: collapse;
width: 100%;
}
#tracy-bs td,
#tracy-bs th {
vertical-align: top;
text-align: left;
padding: 2px 6px;
border: 1px solid #e6dfbf;
}
#tracy-bs th {
font-weight: bold;
}
#tracy-bs tr > :first-child {
width: 20%;
}
#tracy-bs tr:nth-child(2n),
#tracy-bs tr:nth-child(2n) pre {
background-color: #F7F0CB;
}
#tracy-bs .tracy-footer--sticky {
position: fixed;
width: 100%;
bottom: 0;
}
#tracy-bs footer ul {
font-size: 7pt;
padding: var(--tracy-space);
margin: var(--tracy-space) 0 0;
color: #777;
background: #F6F5F3;
border-top: 1px solid #DDD;
list-style: none;
}
#tracy-bs .tracy-footer-logo {
position: relative;
}
#tracy-bs .tracy-footer-logo a {
position: absolute;
bottom: 0;
right: 0;
width: 100px;
height: 50px;
background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFoAAAAUBAMAAAD/1DctAAAAMFBMVEWupZzj39rEvbTy8O3X0sz9/PvGwLu8tavQysHq6OS0rKP5+Pbd2dT29fPMxbzPx8DKErMJAAAACXBIWXMAAAsTAAALEwEAmpwYAAACGUlEQVQoFX3TQWgTQRQA0MWLIJJDYehBTykhG5ERTx56K1u8eEhCYtomE7x5L4iLh0ViF7egewuFFqSIYE6hIHsIYQ6CQSg9CDKn4QsNCRlB59C74J/ZNHW1+An5+bOPyf6/s46oz2P+A0yIeZZ2ieEHi6TOnLKTxvWq+b52mxlVO3xnM1s7xLX1504XQH65OnW2dBqn7cCkYsFsfYsWpyY/2salmFTpEyzeR8zosYqMdiPDXdyU52K1wgEa/SjGpdEwUAxqvRfckQCDOyFearsEHe2grvkh/cFAHKvdtI3lcVceKQIOFpv+FOZaNPQBwJZLPp+hfrvT5JZXaUFsR8zqQc9qSgAharkfS5M/5F6nGJJAtXq/eLr3ucZpHccSxOOIPaQhtHohpCH2Xu6rLmQ0djnr4/+J3C6v+AW8/XWYxwYNdlhWj/P5fPSTQwVr0T9lGxdaBCqErNZaqYnEwbkjEB3NasGF3lPdrHa1nnxNOMgj0+neePUPjd2v/qVvUv29ifvc19huQ48qwXShy/9o8o3OSk0cs37mOFd0Ydgvsf/oZEnPVtggfd66lORn9mDyyzXU13SRtH2L6aR5T/snGAcZPfAXz5J1YlJWBEuxdMYqQecpBrlM49xAbmqyHA+xlA1FxBtqT2xmJoNXZlIt74ZBLeJ9ZGDqByNI7p543idzJ23vXEv7IgnsxiS+eNtwNbFdLq7+Bi4wQ0I4SVb9AAAAAElFTkSuQmCC') no-repeat;
opacity: .6;
padding: 0;
margin: 0;
}
#tracy-bs .tracy-footer-logo a:hover,
#tracy-bs .tracy-footer-logo a:focus {
opacity: 1;
transition: opacity 0.1s;
}
#tracy-bs .tracy-section {
padding-left: calc(1.5 * var(--tracy-space));
padding-right: calc(1.5 * var(--tracy-space));
}
#tracy-bs .tracy-section-panel {
background: #F4F3F1;
padding: var(--tracy-space) var(--tracy-space) 0;
margin: 0 0 var(--tracy-space);
border-radius: 8px;
box-shadow: inset 1px 1px 0px 0 #00000005;
overflow: hidden;
}
#tracy-bs .outer, /* deprecated */
#tracy-bs .tracy-pane {
overflow: auto;
}
#tracy-bs.tracy-mac .tracy-pane {
padding-bottom: 12px;
}
/* header */
#tracy-bs .tracy-section--error {
background: #CD1818;
color: white;
font-size: 13pt;
padding-top: var(--tracy-space);
}
#tracy-bs .tracy-section--error h1 {
color: white;
}
#tracy-bs .tracy-section--error::selection,
#tracy-bs .tracy-section--error ::selection {
color: black !important;
background: #FDF5CE !important;
}
#tracy-bs .tracy-section--error a {
color: #ffefa1 !important;
}
#tracy-bs .tracy-section--error span span {
font-size: 80%;
color: rgba(255, 255, 255, 0.5);
text-shadow: none;
}
#tracy-bs .tracy-section--error a.tracy-action {
color: white !important;
opacity: 0;
font-size: .7em;
border-bottom: none !important;
}
#tracy-bs .tracy-section--error:hover a.tracy-action {
opacity: .6;
}
#tracy-bs .tracy-section--error a.tracy-action:hover {
opacity: 1;
}
#tracy-bs .tracy-section--error i {
color: #ffefa1;
font-style: normal;
}
/* source code */
#tracy-bs pre.tracy-code > div {
min-width: 100%;
float: left;
white-space: pre;
}
#tracy-bs .tracy-line-highlight {
background: #CD1818;
color: white;
font-weight: bold;
font-style: normal;
display: block;
padding: 0 1ch;
margin: 0 -1ch;
}
#tracy-bs .tracy-column-highlight {
display: inline-block;
backdrop-filter: grayscale(1);
margin: 0 -1px;
padding: 0 1px;
}
#tracy-bs .tracy-line {
color: #9F9C7F;
font-weight: normal;
font-style: normal;
}
#tracy-bs a.tracy-editor {
color: inherit;
border-bottom: 1px dotted rgba(0, 0, 0, .3);
border-radius: 3px;
}
#tracy-bs a.tracy-editor:hover {
background: #0001;
}
#tracy-bs span[data-tracy-href] {
border-bottom: 1px dotted rgba(0, 0, 0, .3);
}
#tracy-bs .tracy-dump-whitespace {
color: #0003;
}
#tracy-bs .tracy-caused {
float: right;
padding: .3em calc(1.5 * var(--tracy-space));
background: #df8075;
border-radius: 0 0 0 8px;
white-space: nowrap;
}
#tracy-bs .tracy-caused a {
color: white;
}
#tracy-bs .tracy-callstack {
display: grid;
grid-template-columns: max-content 1fr;
margin-bottom: calc(.5 * var(--tracy-space));
}
#tracy-bs .tracy-callstack-file {
text-align: right;
padding-right: var(--tracy-space);
white-space: nowrap;
height: calc(1.5 * var(--tracy-space));
}
#tracy-bs .tracy-callstack-callee {
white-space: nowrap;
height: calc(1.5 * var(--tracy-space));
}
#tracy-bs .tracy-callstack-additional {
grid-column-start: 1;
grid-column-end: 3;
}
#tracy-bs .tracy-callstack-args tr:first-child > * {
position: relative;
}
#tracy-bs .tracy-callstack-args tr:first-child td:before {
position: absolute;
right: .3em;
content: 'may not be true';
opacity: .4;
}
#tracy-bs .tracy-panel-fadein {
animation: tracy-panel-fadein .12s ease;
}
@keyframes tracy-panel-fadein {
0% {
opacity: 0;
}
}
#tracy-bs .tracy-section--causedby {
flex-direction: column;
padding: 0;
}
#tracy-bs .tracy-section--causedby:not(.tracy-collapsed) {
display: flex;
}
#tracy-bs .tracy-section--causedby .tracy-section--error {
background: #cd1818a6;
}
#tracy-bs .tracy-section--error + .tracy-section--stack {
margin-top: calc(1.5 * var(--tracy-space));
}
/* tabs */
#tracy-bs .tracy-tab-bar {
display: flex;
list-style: none;
padding-left: 0;
margin: 0;
width: 100%;
font-size: 110%;
}
#tracy-bs .tracy-tab-bar > *:not(:first-child) {
margin-left: var(--tracy-space);
}
#tracy-bs .tracy-tab-bar a {
display: block;
padding: calc(.5 * var(--tracy-space)) var(--tracy-space);
margin: 0;
height: 100%;
box-sizing: border-box;
border-radius: 5px 5px 0 0;
text-decoration: none;
transition: all 0.1s;
}
#tracy-bs .tracy-tab-bar > .tracy-active a {
background: white;
}
#tracy-bs .tracy-tab-panel {
border-top: 2px solid white;
padding-top: var(--tracy-space);
overflow: auto;
}

View File

@ -0,0 +1,77 @@
/**
* This file is part of the Tracy (https://tracy.nette.org)
*/
class BlueScreen
{
static init(ajax) {
BlueScreen.globalInit();
let blueScreen = document.getElementById('tracy-bs');
document.documentElement.classList.add('tracy-bs-visible');
if (navigator.platform.indexOf('Mac') > -1) {
blueScreen.classList.add('tracy-mac');
}
blueScreen.addEventListener('tracy-toggle', (e) => {
if (e.target.matches('#tracy-bs-toggle')) { // blue screen toggle
document.documentElement.classList.toggle('tracy-bs-visible', !e.detail.collapsed);
} else if (!e.target.matches('.tracy-dump *') && e.detail.originalEvent) { // panel toggle
e.detail.relatedTarget.classList.toggle('tracy-panel-fadein', !e.detail.collapsed);
}
});
if (!ajax) {
document.body.appendChild(blueScreen);
let id = location.href + document.querySelector('.tracy-section--error').textContent;
Tracy.Toggle.persist(blueScreen, sessionStorage.getItem('tracy-toggles-bskey') === id);
sessionStorage.setItem('tracy-toggles-bskey', id);
}
(new ResizeObserver(stickyFooter)).observe(blueScreen);
if (document.documentElement.classList.contains('tracy-bs-visible')) {
window.scrollTo(0, 0);
}
}
static globalInit() {
// enables toggling via ESC
document.addEventListener('keyup', (e) => {
if (e.keyCode === 27 && !e.shiftKey && !e.altKey && !e.ctrlKey && !e.metaKey) { // ESC
Tracy.Toggle.toggle(document.getElementById('tracy-bs-toggle'));
}
});
Tracy.TableSort.init();
Tracy.Tabs.init();
window.addEventListener('scroll', stickyFooter);
BlueScreen.globalInit = function() {};
}
static loadAjax(content) {
let ajaxBs = document.getElementById('tracy-bs');
if (ajaxBs) {
ajaxBs.remove();
}
document.body.insertAdjacentHTML('beforeend', content);
ajaxBs = document.getElementById('tracy-bs');
Tracy.Dumper.init(ajaxBs);
BlueScreen.init(true);
}
}
function stickyFooter() {
let footer = document.querySelector('#tracy-bs footer');
footer.classList.toggle('tracy-footer--sticky', false); // to measure footer.offsetTop
footer.classList.toggle('tracy-footer--sticky', footer.offsetHeight + footer.offsetTop - window.innerHeight - document.documentElement.scrollTop < 0);
}
let Tracy = window.Tracy = window.Tracy || {};
Tracy.BlueScreen = Tracy.BlueScreen || BlueScreen;

View File

@ -0,0 +1,73 @@
<?php
declare(strict_types=1);
namespace Tracy;
/**
* @var \Throwable $exception
* @var array[] $actions
* @var string[] $info
* @var string $source
* @var ?array $lastError
* @var string[] $httpHeaders
* @var callable $dump
* @var array $snapshot
* @var bool $showEnvironment
* @var BlueScreen $this
* @var bool $headersSent
* @var ?string $headersFile
* @var ?int $headersLine
* @var ?array $obStatus
* @var \Generator[] $generators
* @var \Fiber[] $fibers
*/
?>
<tracy-div id="tracy-bs" itemscope>
<a id="tracy-bs-toggle" href="#" class="tracy-toggle">&#xfeff;</a>
<div class="tracy-bs-main">
<?php $ex = $exception; $exceptions = []; ?>
<?php require __DIR__ . '/section-exception.phtml' ?>
<?php require __DIR__ . '/section-lastMutedError.phtml' ?>
<?php $bottomPanels = [] ?>
<?php foreach ($this->renderPanels(null) as $panel): ?>
<?php if (!empty($panel->bottom)) { $bottomPanels[] = $panel; continue; } ?>
<?php $collapsedClass = !isset($panel->collapsed) || $panel->collapsed ? ' tracy-collapsed' : ''; ?>
<section class="tracy-section">
<h2 class="tracy-section-label"><a href="#" data-tracy-ref="^+" class="tracy-toggle<?= $collapsedClass ?>"><?= Helpers::escapeHtml($panel->tab) ?></a></h2>
<div class="tracy-section-panel<?= $collapsedClass ?>">
<?= $panel->panel ?>
</div>
</section>
<?php endforeach ?>
<?php require __DIR__ . '/section-environment.phtml' ?>
<?php require __DIR__ . '/section-cli.phtml' ?>
<?php require __DIR__ . '/section-http.phtml' ?>
<?php foreach ($bottomPanels as $panel): ?>
<section class="tracy-section">
<h2 class="tracy-section-label"><a href="#" data-tracy-ref="^+" class="tracy-toggle"><?= Helpers::escapeHtml($panel->tab) ?></a></h2>
<div class="tracy-section-panel">
<?= $panel->panel ?>
</div>
</section>
<?php endforeach ?>
<footer>
<ul>
<li><b><a href="https://github.com/sponsors/dg" target="_blank" rel="noreferrer noopener">Please support Tracy via a donation 💙️</a></b></li>
<li>Report generated at <?= date('Y/m/d H:i:s') ?></li>
<?php foreach ($info as $item): ?><li><?= Helpers::escapeHtml($item) ?></li><?php endforeach ?>
</ul>
<div class="tracy-footer-logo"><a href="https://tracy.nette.org" rel="noreferrer"></a></div>
</footer>
</div>
<meta itemprop=tracy-snapshot content=<?= Dumper::formatSnapshotAttribute($snapshot) ?>>
</tracy-div>

View File

@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
namespace Tracy;
/**
* @var \Throwable $exception
* @var string $title
* @var ?string $nonce
* @var string $css
*/
$code = $exception->getCode() ? ' #' . $exception->getCode() : '';
$nonceAttr = $nonce ? ' nonce="' . Helpers::escapeHtml($nonce) . '"' : '';
$chain = Helpers::getExceptionChain($exception);
?><!DOCTYPE html><!-- "' --></textarea></script></style></pre></xmp></a></iframe></noembed></noframes></noscript></option></select></template></title></table></p></code>
<html>
<head>
<meta charset="utf-8">
<meta name="robots" content="noindex">
<meta name="generator" content="Tracy by Nette Framework">
<title><?= Helpers::escapeHtml($title . ': ' . $exception->getMessage() . $code) ?></title>
<!-- in <?= str_replace('--', '- ', Helpers::escapeHtml($exception->getFile() . ':' . $exception->getLine())) ?> -->
<?php if (count($chain) > 1): ?>
<!--<?php foreach (array_slice($chain, 1) as $ex) {
echo str_replace('--', '- ', Helpers::escapeHtml("\n\tcaused by " . Helpers::getClass($ex) . ': ' . $ex->getMessage() . ($ex->getCode() ? ' #' . $ex->getCode() : '')));
} ?> -->
<?php endif ?>
<style class="tracy-debug">
<?= str_replace('</', '<\/', $css) ?>
</style>
</head>
<body>
<?php require __DIR__ . '/content.phtml' ?>
<script<?= $nonceAttr ?>>
'use strict';
<?php
array_map(function ($file) { echo '(function(){', str_replace(['<!--', '</s'], ['<\!--', '<\/s'], Helpers::minifyJs(file_get_contents($file))), '})();'; }, [
__DIR__ . '/../../assets/toggle.js',
__DIR__ . '/../../assets/table-sort.js',
__DIR__ . '/../../assets/tabs.js',
__DIR__ . '/../../Dumper/assets/dumper.js',
__DIR__ . '/bluescreen.js',
]);
?>
Tracy.BlueScreen.init();
</script>
</body>
</html>

View File

@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace Tracy;
/**
* @var string $source
* @var callable $dump
*/
if (!Helpers::isCli()) {
return;
}
?>
<section class="tracy-section">
<h2 class="tracy-section-label"><a href="#" data-tracy-ref="^+" class="tracy-toggle tracy-collapsed">CLI request</a></h2>
<div class="tracy-section-panel tracy-collapsed">
<h3>Process ID <?= Helpers::escapeHtml(getmypid()) ?></h3>
<?php if (count($tmp = explode('):', $source, 2)) === 2): ?>
<pre>php<?= Helpers::escapeHtml($tmp[1]) ?></pre>
<?php endif; ?>
<?php if (isset($_SERVER['argv'])): ?>
<h3>Arguments</h3>
<div class="tracy-pane">
<table>
<?php foreach ($_SERVER['argv'] as $k => $v): ?>
<tr><th><?= Helpers::escapeHtml($k) ?></th><td><?= $dump($v, $k) ?></td></tr>
<?php endforeach ?>
</table>
</div>
<?php endif; ?>
</div>
</section>

View File

@ -0,0 +1,103 @@
<?php
declare(strict_types=1);
namespace Tracy;
/**
* @var callable $dump
* @var bool $showEnvironment
* @var array $obStatus
* @var BlueScreen $this
*/
if (!$showEnvironment) {
return;
}
$constants = get_defined_constants(true)['user'] ?? [];
?>
<section class="tracy-section">
<h2 class="tracy-section-label"><a href="#" data-tracy-ref="^+" class="tracy-toggle tracy-collapsed">Environment</a></h2>
<div class="tracy-section-panel tracy-collapsed">
<div class="tracy-tabs">
<ul class="tracy-tab-bar">
<li class="tracy-tab-label tracy-active"><a href="#">$_SERVER</a></li>
<?php if ($_SESSION ?? null): ?>
<li class="tracy-tab-label"><a href="#">$_SESSION</a></li>
<?php endif ?>
<?php if ($constants): ?>
<li class="tracy-tab-label"><a href="#">Constants</a></li>
<?php endif ?>
<li class="tracy-tab-label"><a href="#">Configuration</a></li>
<?php if ($obStatus): ?>
<li class="tracy-tab-label"><a href="#">Output buffers</a></li>
<?php endif ?>
</ul>
<div>
<div class="tracy-tab-panel tracy-pane tracy-active">
<table class="tracy-sortable">
<?php foreach ($_SERVER as $k => $v): ?>
<tr><th><?= Helpers::escapeHtml($k) ?></th><td><?= $dump($v, $k) ?></td></tr>
<?php endforeach ?>
</table>
</div>
<?php if ($_SESSION ?? null): ?>
<div class="tracy-tab-panel">
<div class="tracy-pane">
<table class="tracy-sortable">
<?php foreach ($_SESSION as $k => $v): ?>
<tr><th><?= Helpers::escapeHtml($k) ?></th><td><?= $k === '__NF' ? '<i>Nette Session</i>' : $dump($v, $k) ?></td></tr>
<?php endforeach ?>
</table>
</div>
<?php if (!empty($_SESSION['__NF']['DATA'])):?>
<h3>Nette Session</h3>
<div class="tracy-pane">
<table class="tracy-sortable">
<?php foreach ($_SESSION['__NF']['DATA'] as $k => $v): ?>
<tr><th><?= Helpers::escapeHtml($k) ?></th><td><?= $dump($v, $k) ?></td></tr>
<?php endforeach ?>
</table>
</div>
<?php endif ?>
</div>
<?php endif ?>
<?php if ($constants): ?>
<div class="tracy-tab-panel tracy-pane">
<table class="tracy-sortable">
<?php foreach ($constants as $k => $v): ?>
<tr><th><?= Helpers::escapeHtml($k) ?></th><td><?= $dump($v, $k) ?></td></tr>
<?php endforeach ?>
</table>
</div>
<?php endif ?>
<div class="tracy-tab-panel">
<?php $this->renderPhpInfo() ?>
</div>
<?php if ($obStatus): ?>
<div class="tracy-tab-panel tracy-pane">
<?= Dumper::toHtml($obStatus, [Dumper::COLLAPSE_COUNT => 10]) ?>
</div>
<?php endif ?>
</div>
</div>
</div>
</section>

View File

@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace Tracy;
/**
* @var \Throwable $ex
* @var \Throwable[] $exceptions
* @var BlueScreen $this
* @var array[] $actions
* @var callable $dump
*/
$ex = $ex->getPrevious();
if (!$ex || in_array($ex, $exceptions, true)) {
return;
}
$exceptions[] = $ex;
?>
<section class="tracy-section" id="tracyCaused<?= count($exceptions) ?>">
<h2 class="tracy-section-label"><a href="#" data-tracy-ref="^+" class="tracy-toggle<?= ($collapsed = count($exceptions) > 1) ? ' tracy-collapsed' : '' ?>">Caused by</a></h2>
<div class="tracy-section-panel tracy-section--causedby<?= $collapsed ? ' tracy-collapsed' : '' ?>">
<?php require __DIR__ . '/section-exception.phtml' ?>
</div>
</section>

View File

@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace Tracy;
/**
* @var \Throwable $ex
* @var callable $dump
*/
if (count((array) $ex) <= count((array) new \Exception)) {
return;
}
?>
<section class="tracy-section">
<h2 class="tracy-section-label"><a href="#" data-tracy-ref="^+" class="tracy-toggle tracy-collapsed">Exception</a></h2>
<div class="tracy-section-panel tracy-collapsed">
<?= $dump($ex) ?>
</div>
</section>

View File

@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace Tracy;
/**
* @var \Throwable $ex
* @var callable $dump
*/
if (!$ex instanceof \ErrorException || empty($ex->context) || !is_array($ex->context)) {
return;
}
?>
<section class="tracy-section">
<h2 class="tracy-section-label"><a href="#" data-tracy-ref="^+" class="tracy-toggle tracy-collapsed">Variables</a></h2>
<div class="tracy-section-panel tracy-collapsed">
<div class="tracy-pane">
<table class="tracy-sortable">
<?php foreach ($ex->context as $k => $v): ?>
<tr><th><?= Helpers::escapeHtml($k) ?></th><td><?= $dump($v, $k) ?></td></tr>
<?php endforeach ?>
</table>
</div>
</div>
</section>

View File

@ -0,0 +1,74 @@
<?php
declare(strict_types=1);
namespace Tracy;
/**
* @var \Throwable $ex
* @var \Throwable[] $exceptions
* @var array[] $actions
* @var callable $dump
* @var BlueScreen $this
* @var \Generator[] $generators
* @var \Fiber[] $fibers
*/
?>
<?php require __DIR__ . '/section-header.phtml' ?>
<?php foreach ($this->renderPanels($ex) as $panel): ?>
<section class="tracy-section">
<h2 class="tracy-section-label"><a href="#" data-tracy-ref="^+" class="tracy-toggle"><?= Helpers::escapeHtml($panel->tab) ?></a></h2>
<div class="tracy-section-panel">
<?= $panel->panel ?>
</div>
</section>
<?php endforeach ?>
<?php if (!$exceptions && ($generators || $fibers)): ?>
<section class="tracy-section tracy-section--stack">
<div class="tracy-section-panel">
<div class="tracy-tabs">
<ul class="tracy-tab-bar">
<li class="tracy-tab-label tracy-active"><a href="#">Main thread</a></li>
<?php foreach ($generators as $id => $generator): ?>
<li class="tracy-tab-label"><a href="#">Generator #<?= $id ?></a></li>
<?php endforeach ?>
<?php foreach ($fibers as $id => $fiber): ?>
<li class="tracy-tab-label"><a href="#">Fiber #<?= $id ?></a></li>
<?php endforeach ?>
</ul>
<div>
<div class="tracy-tab-panel tracy-active">
<?php require __DIR__ . '/section-stack-exception.phtml' ?>
</div>
<?php foreach ($generators as $generator): ?>
<div class="tracy-tab-panel">
<?php require __DIR__ . '/section-stack-generator.phtml' ?>
</div>
<?php endforeach ?>
<?php foreach ($fibers as $fiber): ?>
<div class="tracy-tab-panel">
<?php require __DIR__ . '/section-stack-fiber.phtml' ?>
</div>
<?php endforeach ?>
</div>
</div>
</div>
</section>
<?php else: ?>
<?php require __DIR__ . '/section-stack-exception.phtml' ?>
<?php endif ?>
<?php require __DIR__ . '/section-exception-variables.phtml' ?>
<?php require __DIR__ . '/section-exception-exception.phtml' ?>
<?php require __DIR__ . '/section-exception-causedBy.phtml' ?>

View File

@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace Tracy;
/**
* @var \Throwable $ex
* @var \Throwable[] $exceptions
* @var array[] $actions
* @var BlueScreen $this
*/
$title = $ex instanceof \ErrorException
? Helpers::errorTypeToString($ex->getSeverity())
: Helpers::getClass($ex);
$code = $ex->getCode() ? ' #' . $ex->getCode() : '';
?>
<section class="tracy-section tracy-section--error">
<?php if ($ex->getMessage()): ?><p><?= Helpers::escapeHtml($title . $code) ?></p><?php endif ?>
<h1><span><?= $this->formatMessage($ex) ?: Helpers::escapeHtml($title . $code) ?></span>
<?php foreach ($actions as $item): ?>
<a href="<?= Helpers::escapeHtml($item['link']) ?>" class="tracy-action"<?= empty($item['external']) ? '' : ' target="_blank" rel="noreferrer noopener"'?>><?= Helpers::escapeHtml($item['label']) ?>&#x25ba;</a>
<?php endforeach ?>
</h1>
</section>
<?php if ($ex->getPrevious()): ?>
<div class="tracy-caused">
<a href="#tracyCaused<?= count($exceptions) + 1 ?>">Caused by <?= Helpers::escapeHtml(Helpers::getClass($ex->getPrevious())) ?></a>
</div>
<?php endif ?>

View File

@ -0,0 +1,91 @@
<?php
declare(strict_types=1);
namespace Tracy;
/**
* @var string $source
* @var string[] $httpHeaders
* @var callable $dump
* @var bool $headersSent
* @var ?string $headersFile
* @var ?int $headersLine
*/
if (Helpers::isCli()) {
return;
}
?>
<section class="tracy-section">
<h2 class="tracy-section-label"><a href="#" data-tracy-ref="^+" class="tracy-toggle tracy-collapsed">HTTP</a></h2>
<div class="tracy-section-panel tracy-collapsed">
<div class="tracy-tabs">
<ul class="tracy-tab-bar">
<li class="tracy-tab-label tracy-active"><a href="#">Request</a></li>
<li class="tracy-tab-label"><a href="#">Response</a></li>
</ul>
<div>
<div class="tracy-tab-panel tracy-active">
<h3><?= Helpers::escapeHtml($_SERVER['REQUEST_METHOD'] ?? 'URL') ?> <a href="<?= Helpers::escapeHtml($source) ?>" target="_blank" rel="noreferrer noopener" style="font-weight: normal"><?= Helpers::escapeHtml($source) ?></a></h3>
<?php if ($httpHeaders): ?>
<div class="tracy-pane">
<table class="tracy-sortable">
<?php foreach ($httpHeaders as $k => $v): ?>
<tr><th><?= Helpers::escapeHtml($k) ?></th><td><?= $dump($v, $k) ?></td></tr>
<?php endforeach ?>
</table>
</div>
<?php endif ?>
<?php foreach (['_GET', '_POST', '_COOKIE'] as $name): ?>
<h3>$<?= Helpers::escapeHtml($name) ?></h3>
<?php if (empty($GLOBALS[$name])):?>
<p><i>empty</i></p>
<?php else: ?>
<div class="tracy-pane">
<table class="tracy-sortable">
<?php foreach ($GLOBALS[$name] as $k => $v): ?>
<tr><th><?= Helpers::escapeHtml($k) ?></th><td><?= $dump($v, $k) ?></td></tr>
<?php endforeach ?>
</table>
</div>
<?php endif ?>
<?php endforeach ?>
</div>
<div class="tracy-tab-panel">
<h3>Code: <?= Helpers::escapeHtml(http_response_code()) ?></h3>
<?php if (headers_list()): ?>
<div class="tracy-pane">
<table class="tracy-sortable">
<?php foreach (headers_list() as $s): $s = explode(':', $s, 2); ?>
<tr><th><?= Helpers::escapeHtml($s[0]) ?></th><td><?= $dump(trim($s[1]), $s[0]) ?></td></tr>
<?php endforeach ?>
</table>
</div>
<?php else: ?>
<p><i>no headers</i></p>
<?php endif ?>
<?php if ($headersSent && $headersFile && @is_file($headersFile)): ?>
<p>Headers have been sent, output started at <?= Helpers::editorLink($headersFile, $headersLine) ?> <a href="#" data-tracy-ref="^p + div" class="tracy-toggle tracy-collapsed">source</a></p>
<div class="tracy-collapsed"><?= BlueScreen::highlightFile($headersFile, $headersLine) ?></div>
<?php elseif ($headersSent): ?>
<p>Headers have been sent</p>
<?php else: ?>
<p>Headers were not sent at the time the exception was thrown</p>
<?php endif ?>
</div>
</div>
</div>
</div>
</section>

View File

@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace Tracy;
/**
* @var ?array $lastError
*/
if (!$lastError) {
return;
}
?>
<section class="tracy-section">
<h2 class="tracy-section-label"><a href="#" data-tracy-ref="^+" class="tracy-toggle tracy-collapsed">Last muted error</a></h2>
<div class="tracy-section-panel tracy-collapsed">
<h3><?= Helpers::errorTypeToString($lastError['type']) ?>: <?= Helpers::escapeHtml($lastError['message']) ?></h3>
<p><i>Note: the last muted error may have nothing to do with the thrown exception.</i></p>
<?php if (isset($lastError['file']) && @is_file($lastError['file'])): // @ - may trigger error ?>
<p><?= Helpers::editorLink($lastError['file'], $lastError['line']) ?></p>
<div><?= BlueScreen::highlightFile($lastError['file'], $lastError['line']) ?></div>
<?php else: ?>
<p><i>inner-code</i><?php if (isset($lastError['line'])) echo ':', $lastError['line'] ?></p>
<?php endif ?>
</div>
</section>

View File

@ -0,0 +1,92 @@
<?php
declare(strict_types=1);
namespace Tracy;
/**
* @var callable $dump
* @var int $expanded
* @var array $stack
*/
if (!$stack) {
return;
}
?>
<section class="tracy-section">
<h2 class="tracy-section-label"><a href="#" data-tracy-ref="^+" class="tracy-toggle">Call stack</a></h2>
<div class="tracy-section-panel">
<div class="tracy-callstack">
<?php foreach ($stack as $key => $row): ?>
<?php $clickable = !empty($row['args']) || (isset($row['file']) && @is_file($row['file'])) // @ - may trigger error ?>
<div class="tracy-callstack-file">
<?php if (isset($row['file']) && @is_file($row['file'])): // @ - may trigger error ?>
<?= Helpers::editorLink($row['file'], $row['line']) ?>
<?php else: ?>
<i>inner-code</i><?php if (isset($row['line'])) echo ':', $row['line'] ?>
<?php endif ?>
</div>
<div class="tracy-callstack-callee">
<?php if ($clickable): ?>
<a href="#" data-tracy-ref="^div + div" class="tracy-toggle<?php if ($expanded !== $key) echo ' tracy-collapsed' ?>"><?php endif ?>
<?php if (isset($row['class'])) echo Helpers::escapeHtml($row['class']), '::' ?><b><?= Helpers::escapeHtml($row['function']) ?></b> <?= empty($row['args']) ? '()' : '(...)' ?>
<?php if ($clickable): ?></a><?php endif ?>
</div>
<?php if ($clickable): ?>
<div class="tracy-callstack-additional<?php if ($expanded !== $key) echo ' tracy-collapsed' ?>">
<?php $sourceOriginal = isset($row['file']) && @is_file($row['file']) ? [$row['file'], $row['line']] : null // @ - may trigger error ?>
<?php $sourceMapped = $sourceOriginal ? Debugger::mapSource(...$sourceOriginal) : null ?>
<?php if ($sourceOriginal && $sourceMapped): ?>
<div class="tracy-tabs">
<ul class="tracy-tab-bar">
<li class="tracy-tab-label<?= $sourceMapped['active'] ? '' : ' tracy-active' ?>"><a href="#">PHP</a></li>
<li class="tracy-tab-label<?= $sourceMapped['active'] ? ' tracy-active' : '' ?>"><a href="#"><?= Helpers::escapeHtml($sourceMapped['label']) ?></a></li>
</ul>
<div>
<div class="tracy-tab-panel<?= $sourceMapped['active'] ? '' : ' tracy-active' ?>">
<?= BlueScreen::highlightFile(...$sourceOriginal) ?>
</div>
<div class="tracy-tab-panel<?= $sourceMapped['active'] ? ' tracy-active' : '' ?>">
<?= BlueScreen::highlightFile($sourceMapped['file'], $sourceMapped['line'], 15, false) ?>
</div>
</div>
</div>
<?php elseif ($sourceOriginal): ?>
<?= BlueScreen::highlightFile(...$sourceOriginal) ?>
<?php endif ?>
<?php if (!empty($row['args'])): ?>
<table class="tracy-callstack-args">
<?php
try {
$r = isset($row['class']) ? new \ReflectionMethod($row['class'], $row['function']) : new \ReflectionFunction($row['function']);
$params = $r->getParameters();
} catch (\Exception $e) {
$params = [];
}
foreach ($row['args'] as $k => $v) {
$argName = isset($params[$k]) && !$params[$k]->isVariadic() ? $params[$k]->name : $k;
echo '<tr><th>', Helpers::escapeHtml((is_string($argName) ? '$' : '#') . $argName), '</th><td>';
echo $dump($v, (string) $argName);
echo "</td></tr>\n";
}
?>
</table>
<?php endif ?>
</div>
<?php endif ?>
<?php endforeach ?>
</div>
</div>
</section>

View File

@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace Tracy;
/**
* @var \Throwable $ex
* @var callable $dump
* @var BlueScreen $this
*/
$stack = $ex->getTrace();
$expanded = null;
if (
(!$ex instanceof \ErrorException
|| in_array($ex->getSeverity(), [E_USER_NOTICE, E_USER_WARNING, E_USER_DEPRECATED], true))
&& $this->isCollapsed($ex->getFile())
) {
foreach ($stack as $key => $row) {
if (isset($row['file']) && !$this->isCollapsed($row['file'])) {
$expanded = $key;
break;
}
}
}
if (in_array($stack[0]['class'] ?? null, [DevelopmentStrategy::class, ProductionStrategy::class], true)) {
array_shift($stack);
}
if (($stack[0]['class'] ?? null) === Debugger::class && in_array($stack[0]['function'], ['shutdownHandler', 'errorHandler'], true)) {
array_shift($stack);
}
$file = $ex->getFile();
$line = $ex->getLine();
require __DIR__ . '/section-stack-sourceFile.phtml';
require __DIR__ . '/section-stack-callStack.phtml';

View File

@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace Tracy;
/**
* @var \Fiber $fiber
* @var callable $dump
*/
$ref = new \ReflectionFiber($fiber);
$stack = $ref->getTrace();
$expanded = 0;
require __DIR__ . '/section-stack-callStack.phtml';

View File

@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace Tracy;
/**
* @var \Generator $generator
* @var callable $dump
*/
$ref = new \ReflectionGenerator($generator);
$stack = $ref->getTrace();
$expanded = null;
$execGenerator = $ref->getExecutingGenerator();
$refExec = new \ReflectionGenerator($execGenerator);
$file = $refExec->getExecutingFile();
$line = $refExec->getExecutingLine();
require __DIR__ . '/section-stack-sourceFile.phtml';
require __DIR__ . '/section-stack-callStack.phtml';

View File

@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace Tracy;
/**
* @var string $file
* @var int $line
* @var int $expanded
*/
$sourceOriginal = $file && @is_file($file) ? [$file, $line] : null; // @ - may trigger error
$sourceMapped = $sourceOriginal ? Debugger::mapSource($file, $line) : null;
?>
<section class="tracy-section">
<h2 class="tracy-section-label"><a href="#" data-tracy-ref="^+" class="tracy-toggle<?= ($collapsed = $expanded !== null) ? ' tracy-collapsed' : '' ?>">Source file</a></h2>
<div class="tracy-section-panel<?= $collapsed ? ' tracy-collapsed' : '' ?>">
<?php if ($sourceOriginal && $sourceMapped): ?>
<div class="tracy-tabs">
<ul class="tracy-tab-bar">
<li class="tracy-tab-label<?= $sourceMapped['active'] ? '' : ' tracy-active' ?>"><a href="#">PHP</a></li>
<li class="tracy-tab-label<?= $sourceMapped['active'] ? ' tracy-active' : '' ?>"><a href="#"><?= Helpers::escapeHtml($sourceMapped['label']) ?></a></li>
</ul>
<div>
<div class="tracy-tab-panel<?= $sourceMapped['active'] ? '' : ' tracy-active' ?>">
<p><b>File:</b> <?= Helpers::editorLink(...$sourceOriginal) ?></p>
<?= BlueScreen::highlightFile(...$sourceOriginal) ?>
</div>
<div class="tracy-tab-panel<?= $sourceMapped['active'] ? ' tracy-active' : '' ?>">
<p><b>File:</b> <?= Helpers::editorLink($sourceMapped['file'], $sourceMapped['line']) ?></p>
<?= BlueScreen::highlightFile($sourceMapped['file'], $sourceMapped['line'], 15, false) ?>
</div>
</div>
</div>
<?php else: ?>
<p><b>File:</b> <?= Helpers::editorLink($file, $line) ?></p>
<?php if ($sourceOriginal) echo BlueScreen::highlightFile(...$sourceOriginal) ?>
<?php endif ?>
</div>
</section>

View File

@ -0,0 +1,658 @@
<?php
/**
* This file is part of the Tracy (https://tracy.nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Tracy;
use ErrorException;
/**
* Debugger: displays and logs errors.
*/
class Debugger
{
public const VERSION = '2.9.8';
/** server modes for Debugger::enable() */
public const
Development = false,
Production = true,
Detect = null;
public const
DEVELOPMENT = self::Development,
PRODUCTION = self::Production,
DETECT = self::Detect;
public const CookieSecret = 'tracy-debug';
public const COOKIE_SECRET = self::CookieSecret;
/** @var bool in production mode is suppressed any debugging output */
public static $productionMode = self::Detect;
/** @var bool whether to display debug bar in development mode */
public static $showBar = true;
/** @var bool whether to send data to FireLogger in development mode */
public static $showFireLogger = true;
/** @var int size of reserved memory */
public static $reservedMemorySize = 500000;
/** @var bool */
private static $enabled = false;
/** @var string|null reserved memory; also prevents double rendering */
private static $reserved;
/** @var int initial output buffer level */
private static $obLevel;
/** @var ?array output buffer status @internal */
public static $obStatus;
/********************* errors and exceptions reporting ****************d*g**/
/** @var bool|int determines whether any error will cause immediate death in development mode; if integer that it's matched against error severity */
public static $strictMode = false;
/** @var bool|int disables the @ (shut-up) operator so that notices and warnings are no longer hidden; if integer than it's matched against error severity */
public static $scream = false;
/** @var callable[] functions that are automatically called after fatal error */
public static $onFatalError = [];
/********************* Debugger::dump() ****************d*g**/
/** @var int how many nested levels of array/object properties display by dump() */
public static $maxDepth = 15;
/** @var int how long strings display by dump() */
public static $maxLength = 150;
/** @var int how many items in array/object display by dump() */
public static $maxItems = 100;
/** @var bool display location by dump()? */
public static $showLocation;
/** @var string[] sensitive keys not displayed by dump() */
public static $keysToHide = [];
/** @var string theme for dump() */
public static $dumpTheme = 'light';
/** @deprecated */
public static $maxLen;
/********************* logging ****************d*g**/
/** @var string|null name of the directory where errors should be logged */
public static $logDirectory;
/** @var int log bluescreen in production mode for this error severity */
public static $logSeverity = 0;
/** @var string|array email(s) to which send error notifications */
public static $email;
/** for Debugger::log() and Debugger::fireLog() */
public const
DEBUG = ILogger::DEBUG,
INFO = ILogger::INFO,
WARNING = ILogger::WARNING,
ERROR = ILogger::ERROR,
EXCEPTION = ILogger::EXCEPTION,
CRITICAL = ILogger::CRITICAL;
/********************* misc ****************d*g**/
/** @var float timestamp with microseconds of the start of the request */
public static $time;
/** @var string URI pattern mask to open editor */
public static $editor = 'editor://%action/?file=%file&line=%line&search=%search&replace=%replace';
/** @var array replacements in path */
public static $editorMapping = [];
/** @var string command to open browser (use 'start ""' in Windows) */
public static $browser;
/** @var string custom static error template */
public static $errorTemplate;
/** @var string[] */
public static $customCssFiles = [];
/** @var string[] */
public static $customJsFiles = [];
/** @var callable[] */
private static $sourceMappers = [];
/** @var array|null */
private static $cpuUsage;
/********************* services ****************d*g**/
/** @var BlueScreen */
private static $blueScreen;
/** @var Bar */
private static $bar;
/** @var ILogger */
private static $logger;
/** @var ILogger */
private static $fireLogger;
/** @var array{DevelopmentStrategy, ProductionStrategy} */
private static $strategy;
/** @var SessionStorage */
private static $sessionStorage;
/**
* Static class - cannot be instantiated.
*/
final public function __construct()
{
throw new \LogicException;
}
/**
* Enables displaying or logging errors and exceptions.
* @param bool|string|string[] $mode use constant Debugger::Production, Development, Detect (autodetection) or IP address(es) whitelist.
* @param string $logDirectory error log directory
* @param string|array $email administrator email; enables email sending in production mode
*/
public static function enable($mode = null, ?string $logDirectory = null, $email = null): void
{
if ($mode !== null || self::$productionMode === null) {
self::$productionMode = is_bool($mode)
? $mode
: !self::detectDebugMode($mode);
}
self::$reserved = str_repeat('t', self::$reservedMemorySize);
self::$time = $_SERVER['REQUEST_TIME_FLOAT'] ?? microtime(true);
self::$obLevel = ob_get_level();
self::$cpuUsage = !self::$productionMode && function_exists('getrusage') ? getrusage() : null;
// logging configuration
if ($email !== null) {
self::$email = $email;
}
if ($logDirectory !== null) {
self::$logDirectory = $logDirectory;
}
if (self::$logDirectory) {
if (!preg_match('#([a-z]+:)?[/\\\\]#Ai', self::$logDirectory)) {
self::exceptionHandler(new \RuntimeException('Logging directory must be absolute path.'));
exit(255);
} elseif (!is_dir(self::$logDirectory)) {
self::exceptionHandler(new \RuntimeException("Logging directory '" . self::$logDirectory . "' is not found."));
exit(255);
}
}
// php configuration
if (function_exists('ini_set')) {
ini_set('display_errors', '0'); // or 'stderr'
ini_set('html_errors', '0');
ini_set('log_errors', '0');
ini_set('zend.exception_ignore_args', '0');
}
error_reporting(E_ALL);
$strategy = self::getStrategy();
$strategy->initialize();
self::dispatch();
if (self::$enabled) {
return;
}
register_shutdown_function([self::class, 'shutdownHandler']);
set_exception_handler(function (\Throwable $e) {
self::exceptionHandler($e);
exit(255);
});
set_error_handler([self::class, 'errorHandler']);
foreach ([
'Bar/Bar',
'Bar/DefaultBarPanel',
'BlueScreen/BlueScreen',
'Dumper/Describer',
'Dumper/Dumper',
'Dumper/Exposer',
'Dumper/Renderer',
'Dumper/Value',
'Logger/FireLogger',
'Logger/Logger',
'Session/SessionStorage',
'Session/FileSession',
'Session/NativeSession',
'Helpers',
] as $path) {
require_once dirname(__DIR__) . "/$path.php";
}
self::$enabled = true;
}
public static function dispatch(): void
{
if (
!Helpers::isCli()
&& self::getStrategy()->sendAssets()
) {
self::$showBar = false;
exit;
}
}
/**
* Renders loading <script>
*/
public static function renderLoader(): void
{
self::getStrategy()->renderLoader();
}
public static function isEnabled(): bool
{
return self::$enabled;
}
/**
* Shutdown handler to catch fatal errors and execute of the planned activities.
* @internal
*/
public static function shutdownHandler(): void
{
$error = error_get_last();
if (in_array($error['type'] ?? null, [E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR, E_PARSE, E_RECOVERABLE_ERROR, E_USER_ERROR], true)) {
self::exceptionHandler(Helpers::fixStack(new ErrorException($error['message'], 0, $error['type'], $error['file'], $error['line'])));
} elseif (($error['type'] ?? null) === E_COMPILE_WARNING) {
error_clear_last();
self::errorHandler($error['type'], $error['message'], $error['file'], $error['line']);
}
self::$reserved = null;
if (self::$showBar && !Helpers::isCli()) {
try {
self::getStrategy()->renderBar();
} catch (\Throwable $e) {
self::exceptionHandler($e);
}
}
}
/**
* Handler to catch uncaught exception.
* @internal
*/
public static function exceptionHandler(\Throwable $exception): void
{
$firstTime = (bool) self::$reserved;
self::$reserved = null;
self::$obStatus = ob_get_status(true);
if (!headers_sent()) {
http_response_code(isset($_SERVER['HTTP_USER_AGENT']) && strpos($_SERVER['HTTP_USER_AGENT'], 'MSIE ') !== false ? 503 : 500);
}
Helpers::improveException($exception);
self::removeOutputBuffers(true);
self::getStrategy()->handleException($exception, $firstTime);
try {
foreach ($firstTime ? self::$onFatalError : [] as $handler) {
$handler($exception);
}
} catch (\Throwable $e) {
try {
self::log($e, self::EXCEPTION);
} catch (\Throwable $e) {
}
}
}
/**
* Handler to catch warnings and notices.
* @return bool|null false to call normal error handler, null otherwise
* @throws ErrorException
* @internal
*/
public static function errorHandler(
int $severity,
string $message,
string $file,
int $line,
?array $context = null
): bool
{
$error = error_get_last();
if (($error['type'] ?? null) === E_COMPILE_WARNING) {
error_clear_last();
self::errorHandler($error['type'], $error['message'], $error['file'], $error['line']);
}
if ($context) {
$context = (array) (object) $context; // workaround for PHP bug #80234
}
if ($severity === E_RECOVERABLE_ERROR || $severity === E_USER_ERROR) {
if (Helpers::findTrace(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS), '*::__toString')) { // workaround for PHP < 7.4
$previous = isset($context['e']) && $context['e'] instanceof \Throwable
? $context['e']
: null;
$e = new ErrorException($message, 0, $severity, $file, $line, $previous);
@$e->context = $context; // dynamic properties are deprecated since PHP 8.2
self::exceptionHandler($e);
exit(255);
}
$e = new ErrorException($message, 0, $severity, $file, $line);
@$e->context = $context; // dynamic properties are deprecated since PHP 8.2
throw $e;
} elseif (
($severity & error_reporting())
|| (is_int(self::$scream) ? $severity & self::$scream : self::$scream)
) {
self::getStrategy()->handleError($severity, $message, $file, $line, $context);
}
return false; // calls normal error handler to fill-in error_get_last()
}
/** @internal */
public static function removeOutputBuffers(bool $errorOccurred): void
{
while (ob_get_level() > self::$obLevel) {
$status = ob_get_status();
if (in_array($status['name'], ['ob_gzhandler', 'zlib output compression'], true)) {
break;
}
$fnc = $status['chunk_size'] || !$errorOccurred
? 'ob_end_flush'
: 'ob_end_clean';
if (!@$fnc()) { // @ may be not removable
break;
}
}
}
/********************* services ****************d*g**/
public static function getBlueScreen(): BlueScreen
{
if (!self::$blueScreen) {
self::$blueScreen = new BlueScreen;
self::$blueScreen->info = [
'PHP ' . PHP_VERSION,
$_SERVER['SERVER_SOFTWARE'] ?? null,
'Tracy ' . self::VERSION,
];
}
return self::$blueScreen;
}
public static function getBar(): Bar
{
if (!self::$bar) {
self::$bar = new Bar;
self::$bar->addPanel($info = new DefaultBarPanel('info'), 'Tracy:info');
$info->cpuUsage = self::$cpuUsage;
self::$bar->addPanel(new DefaultBarPanel('errors'), 'Tracy:errors'); // filled by errorHandler()
}
return self::$bar;
}
public static function setLogger(ILogger $logger): void
{
self::$logger = $logger;
}
public static function getLogger(): ILogger
{
if (!self::$logger) {
self::$logger = new Logger(self::$logDirectory, self::$email, self::getBlueScreen());
self::$logger->directory = &self::$logDirectory; // back compatiblity
self::$logger->email = &self::$email;
}
return self::$logger;
}
public static function getFireLogger(): ILogger
{
if (!self::$fireLogger) {
self::$fireLogger = new FireLogger;
}
return self::$fireLogger;
}
/** @return ProductionStrategy|DevelopmentStrategy @internal */
public static function getStrategy()
{
if (empty(self::$strategy[self::$productionMode])) {
self::$strategy[self::$productionMode] = self::$productionMode
? new ProductionStrategy
: new DevelopmentStrategy(self::getBar(), self::getBlueScreen(), new DeferredContent(self::getSessionStorage()));
}
return self::$strategy[self::$productionMode];
}
public static function setSessionStorage(SessionStorage $storage): void
{
if (self::$sessionStorage) {
throw new \Exception('Storage is already set.');
}
self::$sessionStorage = $storage;
}
/** @internal */
public static function getSessionStorage(): SessionStorage
{
if (!self::$sessionStorage) {
self::$sessionStorage = @is_dir($dir = session_save_path())
|| @is_dir($dir = ini_get('upload_tmp_dir'))
|| @is_dir($dir = sys_get_temp_dir())
|| ($dir = self::$logDirectory)
? new FileSession($dir)
: new NativeSession;
}
return self::$sessionStorage;
}
/********************* useful tools ****************d*g**/
/**
* Dumps information about a variable in readable format.
* @tracySkipLocation
* @param mixed $var variable to dump
* @param bool $return return output instead of printing it? (bypasses $productionMode)
* @return mixed variable itself or dump
*/
public static function dump($var, bool $return = false)
{
if ($return) {
$options = [
Dumper::DEPTH => self::$maxDepth,
Dumper::TRUNCATE => self::$maxLength,
Dumper::ITEMS => self::$maxItems,
];
return Helpers::isCli()
? Dumper::toText($var)
: Helpers::capture(function () use ($var, $options) {
Dumper::dump($var, $options);
});
} elseif (!self::$productionMode) {
$html = Helpers::isHtmlMode();
echo $html ? '<tracy-div>' : '';
Dumper::dump($var, [
Dumper::DEPTH => self::$maxDepth,
Dumper::TRUNCATE => self::$maxLength,
Dumper::ITEMS => self::$maxItems,
Dumper::LOCATION => self::$showLocation,
Dumper::THEME => self::$dumpTheme,
Dumper::KEYS_TO_HIDE => self::$keysToHide,
]);
echo $html ? '</tracy-div>' : '';
}
return $var;
}
/**
* Starts/stops stopwatch.
* @return float elapsed seconds
*/
public static function timer(?string $name = null): float
{
static $time = [];
$now = microtime(true);
$delta = isset($time[$name]) ? $now - $time[$name] : 0;
$time[$name] = $now;
return $delta;
}
/**
* Dumps information about a variable in Tracy Debug Bar.
* @tracySkipLocation
* @param mixed $var
* @return mixed variable itself
*/
public static function barDump($var, ?string $title = null, array $options = [])
{
if (!self::$productionMode) {
static $panel;
if (!$panel) {
self::getBar()->addPanel($panel = new DefaultBarPanel('dumps'), 'Tracy:dumps');
}
$panel->data[] = ['title' => $title, 'dump' => Dumper::toHtml($var, $options + [
Dumper::DEPTH => self::$maxDepth,
Dumper::ITEMS => self::$maxItems,
Dumper::TRUNCATE => self::$maxLength,
Dumper::LOCATION => self::$showLocation ?: Dumper::LOCATION_CLASS | Dumper::LOCATION_SOURCE,
Dumper::LAZY => true,
])];
}
return $var;
}
/**
* Logs message or exception.
* @param mixed $message
* @return mixed
*/
public static function log($message, string $level = ILogger::INFO)
{
return self::getLogger()->log($message, $level);
}
/**
* Sends message to FireLogger console.
* @param mixed $message
*/
public static function fireLog($message): bool
{
return !self::$productionMode && self::$showFireLogger
? self::getFireLogger()->log($message)
: false;
}
/** @internal */
public static function addSourceMapper(callable $mapper): void
{
self::$sourceMappers[] = $mapper;
}
/** @return array{file: string, line: int, label: string, active: bool} */
public static function mapSource(string $file, int $line): ?array
{
foreach (self::$sourceMappers as $mapper) {
if ($res = $mapper($file, $line)) {
return $res;
}
}
return null;
}
/**
* Detects debug mode by IP address.
* @param string|array $list IP addresses or computer names whitelist detection
*/
public static function detectDebugMode($list = null): bool
{
$addr = $_SERVER['REMOTE_ADDR'] ?? php_uname('n');
$secret = isset($_COOKIE[self::CookieSecret]) && is_string($_COOKIE[self::CookieSecret])
? $_COOKIE[self::CookieSecret]
: null;
$list = is_string($list)
? preg_split('#[,\s]+#', $list)
: (array) $list;
if (!isset($_SERVER['HTTP_X_FORWARDED_FOR']) && !isset($_SERVER['HTTP_FORWARDED'])) {
$list[] = '127.0.0.1';
$list[] = '::1';
$list[] = '[::1]'; // workaround for PHP < 7.3.4
}
return in_array($addr, $list, true) || in_array("$secret@$addr", $list, true);
}
}

View File

@ -0,0 +1,161 @@
<?php
/**
* This file is part of the Tracy (https://tracy.nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Tracy;
/**
* @internal
*/
final class DeferredContent
{
/** @var SessionStorage */
private $sessionStorage;
/** @var string */
private $requestId;
/** @var bool */
private $useSession = false;
public function __construct(SessionStorage $sessionStorage)
{
$this->sessionStorage = $sessionStorage;
$this->requestId = $_SERVER['HTTP_X_TRACY_AJAX'] ?? Helpers::createId();
}
public function isAvailable(): bool
{
return $this->useSession && $this->sessionStorage->isAvailable();
}
public function getRequestId(): string
{
return $this->requestId;
}
public function &getItems(string $key): array
{
$items = &$this->sessionStorage->getData()[$key];
$items = (array) $items;
return $items;
}
public function addSetup(string $method, $argument): void
{
$argument = json_encode($argument, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE);
$item = &$this->getItems('setup')[$this->requestId];
$item['code'] = ($item['code'] ?? '') . "$method($argument);\n";
$item['time'] = time();
}
public function sendAssets(): bool
{
if (headers_sent($file, $line) || ob_get_length()) {
throw new \LogicException(
__METHOD__ . '() called after some output has been sent. '
. ($file ? "Output started at $file:$line." : 'Try Tracy\OutputDebugger to find where output started.')
);
}
$asset = $_GET['_tracy_bar'] ?? null;
if ($asset === 'js') {
header('Content-Type: application/javascript; charset=UTF-8');
header('Cache-Control: max-age=864000');
header_remove('Pragma');
header_remove('Set-Cookie');
$str = $this->buildJsCss();
header('Content-Length: ' . strlen($str));
echo $str;
flush();
return true;
}
$this->useSession = $this->sessionStorage->isAvailable();
if (!$this->useSession) {
return false;
}
$this->clean();
if (is_string($asset) && preg_match('#^content(-ajax)?\.(\w+)$#', $asset, $m)) {
[, $ajax, $requestId] = $m;
header('Content-Type: application/javascript; charset=UTF-8');
header('Cache-Control: max-age=60');
header_remove('Set-Cookie');
$str = $ajax ? '' : $this->buildJsCss();
$data = &$this->getItems('setup');
$str .= $data[$requestId]['code'] ?? '';
unset($data[$requestId]);
header('Content-Length: ' . strlen($str));
echo $str;
flush();
return true;
}
if (Helpers::isAjax()) {
header('X-Tracy-Ajax: 1'); // session must be already locked
}
return false;
}
private function buildJsCss(): string
{
$css = array_map('file_get_contents', array_merge([
__DIR__ . '/../assets/reset.css',
__DIR__ . '/../Bar/assets/bar.css',
__DIR__ . '/../assets/toggle.css',
__DIR__ . '/../assets/table-sort.css',
__DIR__ . '/../assets/tabs.css',
__DIR__ . '/../Dumper/assets/dumper-light.css',
__DIR__ . '/../Dumper/assets/dumper-dark.css',
__DIR__ . '/../BlueScreen/assets/bluescreen.css',
], Debugger::$customCssFiles));
$js1 = array_map(function ($file) { return '(function() {' . file_get_contents($file) . '})();'; }, [
__DIR__ . '/../Bar/assets/bar.js',
__DIR__ . '/../assets/toggle.js',
__DIR__ . '/../assets/table-sort.js',
__DIR__ . '/../assets/tabs.js',
__DIR__ . '/../Dumper/assets/dumper.js',
__DIR__ . '/../BlueScreen/assets/bluescreen.js',
]);
$js2 = array_map('file_get_contents', Debugger::$customJsFiles);
$str = "'use strict';
(function(){
var el = document.createElement('style');
el.setAttribute('nonce', document.currentScript.getAttribute('nonce') || document.currentScript.nonce);
el.className='tracy-debug';
el.textContent=" . json_encode(Helpers::minifyCss(implode('', $css))) . ";
document.head.appendChild(el);})
();\n" . implode('', $js1) . implode('', $js2);
return $str;
}
public function clean(): void
{
foreach ($this->sessionStorage->getData() as &$items) {
$items = array_slice((array) $items, -10, null, true);
$items = array_filter($items, function ($item) {
return isset($item['time']) && $item['time'] > time() - 60;
});
}
}
}

View File

@ -0,0 +1,141 @@
<?php
/**
* This file is part of the Tracy (https://tracy.nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Tracy;
use ErrorException;
/**
* @internal
*/
final class DevelopmentStrategy
{
/** @var Bar */
private $bar;
/** @var BlueScreen */
private $blueScreen;
/** @var DeferredContent */
private $defer;
public function __construct(Bar $bar, BlueScreen $blueScreen, DeferredContent $defer)
{
$this->bar = $bar;
$this->blueScreen = $blueScreen;
$this->defer = $defer;
}
public function initialize(): void
{
}
public function handleException(\Throwable $exception, bool $firstTime): void
{
if (Helpers::isAjax() && $this->defer->isAvailable()) {
$this->blueScreen->renderToAjax($exception, $this->defer);
} elseif ($firstTime && Helpers::isHtmlMode()) {
$this->blueScreen->render($exception);
} else {
Debugger::fireLog($exception);
$this->renderExceptionCli($exception);
}
}
private function renderExceptionCli(\Throwable $exception): void
{
try {
$logFile = Debugger::log($exception, Debugger::EXCEPTION);
} catch (\Throwable $e) {
echo "$exception\nTracy is unable to log error: {$e->getMessage()}\n";
return;
}
if ($logFile && !headers_sent()) {
header("X-Tracy-Error-Log: $logFile", false);
}
if (Helpers::detectColors()) {
echo "\n\n" . $this->blueScreen->highlightPhpCli($exception->getFile(), $exception->getLine()) . "\n";
}
echo "$exception\n" . ($logFile ? "\n(stored in $logFile)\n" : '');
if ($logFile && Debugger::$browser) {
exec(Debugger::$browser . ' ' . escapeshellarg(strtr($logFile, Debugger::$editorMapping)));
}
}
public function handleError(
int $severity,
string $message,
string $file,
int $line,
array $context = null
): void
{
if (function_exists('ini_set')) {
$oldDisplay = ini_set('display_errors', '1');
}
if (
(is_bool(Debugger::$strictMode) ? Debugger::$strictMode : (Debugger::$strictMode & $severity)) // $strictMode
&& !isset($_GET['_tracy_skip_error'])
) {
$e = new ErrorException($message, 0, $severity, $file, $line);
@$e->context = $context; // dynamic properties are deprecated since PHP 8.2
@$e->skippable = true;
Debugger::exceptionHandler($e);
exit(255);
}
$message = 'PHP ' . Helpers::errorTypeToString($severity) . ': ' . Helpers::improveError($message, (array) $context);
$count = &$this->bar->getPanel('Tracy:errors')->data["$file|$line|$message"];
if (!$count++) { // not repeated error
Debugger::fireLog(new ErrorException($message, 0, $severity, $file, $line));
if (!Helpers::isHtmlMode() && !Helpers::isAjax()) {
echo "\n$message in $file on line $line\n";
}
}
if (function_exists('ini_set')) {
ini_set('display_errors', $oldDisplay);
}
}
public function sendAssets(): bool
{
return $this->defer->sendAssets();
}
public function renderLoader(): void
{
$this->bar->renderLoader($this->defer);
}
public function renderBar(): void
{
if (function_exists('ini_set')) {
ini_set('display_errors', '1');
}
$this->bar->render($this->defer);
}
}

View File

@ -0,0 +1,95 @@
<?php
/**
* This file is part of the Tracy (https://tracy.nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Tracy;
use ErrorException;
/**
* @internal
*/
final class ProductionStrategy
{
public function initialize(): void
{
if (!function_exists('ini_set') && (ini_get('display_errors') && ini_get('display_errors') !== 'stderr')) {
Debugger::exceptionHandler(new \RuntimeException("Unable to set 'display_errors' because function ini_set() is disabled."));
}
}
public function handleException(\Throwable $exception, bool $firstTime): void
{
try {
Debugger::log($exception, Debugger::EXCEPTION);
} catch (\Throwable $e) {
}
if (!$firstTime) {
// nothing
} elseif (Helpers::isHtmlMode()) {
if (!headers_sent()) {
header('Content-Type: text/html; charset=UTF-8');
}
(function ($logged) use ($exception) {
require Debugger::$errorTemplate ?: __DIR__ . '/assets/error.500.phtml';
})(empty($e));
} elseif (Helpers::isCli()) {
// @ triggers E_NOTICE when strerr is closed since PHP 7.4
@fwrite(STDERR, "ERROR: {$exception->getMessage()}\n"
. (isset($e)
? 'Unable to log error. You may try enable debug mode to inspect the problem.'
: 'Check log to see more info.')
. "\n");
}
}
public function handleError(
int $severity,
string $message,
string $file,
int $line,
array $context = null
): void
{
if ($severity & Debugger::$logSeverity) {
$err = new ErrorException($message, 0, $severity, $file, $line);
@$err->context = $context; // dynamic properties are deprecated since PHP 8.2
Helpers::improveException($err);
} else {
$err = 'PHP ' . Helpers::errorTypeToString($severity) . ': ' . Helpers::improveError($message, (array) $context) . " in $file:$line";
}
try {
Debugger::log($err, Debugger::ERROR);
} catch (\Throwable $e) {
}
}
public function sendAssets(): bool
{
return false;
}
public function renderLoader(): void
{
}
public function renderBar(): void
{
}
}

View File

@ -0,0 +1,43 @@
<?php
/**
* Default error page.
*/
declare(strict_types=1);
namespace Tracy;
/**
* @var bool $logged
*/
?>
<!DOCTYPE html><!-- "' --></textarea></script></style></pre></xmp></a></audio></button></canvas></datalist></details></dialog></iframe></listing></meter></noembed></noframes></noscript></optgroup></option></progress></rp></select></table></template></title></video>
<meta charset="utf-8">
<meta name=robots content=noindex>
<meta name=generator content="Tracy">
<title>Server Error</title>
<style>
#tracy-error { all: initial; position: absolute; top: 0; left: 0; right: 0; height: 70vh; min-height: 400px; display: flex; align-items: center; justify-content: center; z-index: 1000 }
#tracy-error div { all: initial; max-width: 550px; background: white; color: #333; display: block }
#tracy-error h1 { all: initial; font: bold 50px/1.1 sans-serif; display: block; margin: 40px }
#tracy-error p { all: initial; font: 20px/1.4 sans-serif; margin: 40px; display: block }
#tracy-error small { color: gray }
#tracy-error small span { color: silver }
</style>
<div id=tracy-error>
<div>
<h1>Server Error</h1>
<p>We're sorry! The server encountered an internal error and
was unable to complete your request. Please try again later.</p>
<p><small>error 500 <span> | <?php echo date('j. n. Y H:i') ?></span><?php if (!$logged): ?><br>Tracy is unable to log error.<?php endif ?></small></p>
</div>
</div>
<script>
document.body.insertBefore(document.getElementById('tracy-error'), document.body.firstChild);
</script>

Some files were not shown because too many files have changed in this diff Show More