laravel-6 support

This commit is contained in:
RafficMohammed
2023-01-08 01:17:22 +05:30
parent 1a5c16ae4b
commit 774eed8b0e
4962 changed files with 279380 additions and 297961 deletions

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,44 @@
<?php
$finder = Symfony\Component\Finder\Finder::create()
->notPath('bootstrap/*')
->notPath('storage/*')
->notPath('resources/view/mail/*')
->in([
__DIR__ . '/src',
__DIR__ . '/tests',
])
->name('*.php')
->notName('*.blade.php')
->notName('GitConflictController.php')
->ignoreDotFiles(true)
->ignoreVCS(true);
return (new PhpCsFixer\Config())
->setRules([
'@PSR12' => true,
'array_syntax' => ['syntax' => 'short'],
'ordered_imports' => ['sort_algorithm' => 'alpha'],
'no_unused_imports' => true,
'not_operator_with_successor_space' => true,
'trailing_comma_in_multiline' => true,
'phpdoc_scalar' => true,
'unary_operator_spaces' => true,
'binary_operator_spaces' => true,
'blank_line_before_statement' => [
'statements' => ['break', 'continue', 'declare', 'return', 'throw', 'try'],
],
'phpdoc_single_line_var_spacing' => true,
'phpdoc_var_without_name' => true,
'class_attributes_separation' => [
'elements' => [
'method' => 'one',
],
],
'method_argument_space' => [
'on_multiline' => 'ensure_fully_multiline',
'keep_multiple_spaces_after_comma' => true,
],
'single_trait_insert_per_statement' => true,
])
->setFinder($finder);

View File

@@ -0,0 +1,114 @@
# Changelog
All notable changes to `flare-client-php` will be documented in this file
## 1.9.1 - 2021-09-13
- let `report` return the created report
## 1.9.0 - 2021-09-13
- add report tracking uuid
## 1.8.1 - 2021-05-31
- improve compatibility with Symfony 5.3
## 1.8.0 - 2021-04-30
- add ability to ignore errors and exceptions (#23)
- fix curl parameters
## 1.7.0 - 2021-04-12
- use new Flare endpoint and allow 1 redirect to it
## 1.6.1 - 2021-04-08
- make `censorRequestBodyFields` chainable
## 1.6.0 - 2021-04-08
- add ability to censor request body fields (#18)
## 1.5.0 - 2021-03-31
- add `determineVersionUsing`
## 1.4.0 - 2021-02-16
- remove custom grouping
## 1.3.7 - 2020-10-21
- allow PHP 8
## 1.3.6 - 2020-09-18
- remove `larapack/dd` (#15)
## 1.3.5 - 2020-08-26
- allow Laravel 8 (#13)
## 1.3.4 - 2020-07-14
- use directory separator constant
## 1.3.3 - 2020-07-14
- fix tests by requiring symfony/mime
- display real exception class for view errors (see https://github.com/facade/ignition/discussions/237)
## 1.3.2 - 2020-03-02
- allow L7
## 1.3.1 - 2019-12-15
- allow var-dumper v5.0
## 1.3.0 - 2019-11-27
- Allow custom grouping types
## 1.2.1 - 2019-11-19
- Let `registerFlareHandlers` return $this
## 1.2.0 - 2019-11-19
- Add `registerFlareHandlers` method to register error and exception handlers in non-Laravel applications
- Fix get requests with query parameters (#4)
## 1.1.2 - 2019-11-08
- Ignore invalid mime type detection issues
## 1.1.1 - 2019-10-07
- Wrap filesize detection in try-catch block
## 1.1.0 - 2019-09-27
- Add ability to log messages
## 1.0.4 - 2019-09-11
- Fixes an issue when sending exceptions inside a queue worker
## 1.0.3 - 2019-09-05
- Ensure valid session data
## 1.0.2 - 2019-09-05
- Fix error when uploading multiple files using an array name
## 1.0.1 - 2019-09-02
- Fix issue with uploaded files in request context
## 1.0.0 - 2019-08-30
- initial release

View File

@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) Facade <info@facade.company>
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,36 @@
# Send PHP errors to Flare
[![Latest Version on Packagist](https://img.shields.io/packagist/v/facade/flare-client-php.svg?style=flat-square)](https://packagist.org/packages/facade/flare-client-php)
![Tests](https://github.com/facade/flare-client-php/workflows/Run%20tests/badge.svg)
[![Total Downloads](https://img.shields.io/packagist/dt/facade/flare-client-php.svg?style=flat-square)](https://packagist.org/packages/facade/flare-client-php)
This repository contains a PHP client to send PHP errors to [Flare](https://flareapp.io).
![Screenshot of error in Flare](https://facade.github.io/flare-client-php/screenshot.png)
## Documentation
You can find the documentation of this package at [the docs of Flare](https://flareapp.io/docs/general/projects).
## Changelog
Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently.
## Testing
``` bash
composer test
```
## Contributing
Please see [CONTRIBUTING](CONTRIBUTING.md) for details.
## Security
If you discover any security related issues, please email support@flareapp.io instead of using the issue tracker.
## License
The MIT License (MIT). Please see [License File](LICENSE.md) for more information.

View File

@@ -0,0 +1,52 @@
{
"name": "facade/flare-client-php",
"description": "Send PHP errors to Flare",
"keywords": [
"facade",
"flare",
"exception",
"reporting"
],
"homepage": "https://github.com/facade/flare-client-php",
"license": "MIT",
"require": {
"php": "^7.1|^8.0",
"facade/ignition-contracts": "~1.0",
"illuminate/pipeline": "^5.5|^6.0|^7.0|^8.0",
"symfony/http-foundation": "^3.3|^4.1|^5.0",
"symfony/mime": "^3.4|^4.0|^5.1",
"symfony/var-dumper": "^3.4|^4.0|^5.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^2.14",
"spatie/phpunit-snapshot-assertions": "^2.0",
"phpunit/phpunit": "^7.5"
},
"autoload": {
"psr-4": {
"Facade\\FlareClient\\": "src"
},
"files": [
"src/helpers.php"
]
},
"autoload-dev": {
"psr-4": {
"Facade\\FlareClient\\Tests\\": "tests"
}
},
"scripts": {
"format": "vendor/bin/php-cs-fixer fix --allow-risky=yes",
"test": "vendor/bin/phpunit",
"test-coverage": "vendor/bin/phpunit --coverage-html coverage"
},
"config": {
"sort-packages": true
},
"extra": {
"branch-alias": {
"dev-master": "1.0-dev"
}
}
}

View File

@@ -0,0 +1,77 @@
<?php
namespace Facade\FlareClient;
use Exception;
use Facade\FlareClient\Http\Client;
use Facade\FlareClient\Truncation\ReportTrimmer;
class Api
{
/** @var \Facade\FlareClient\Http\Client */
protected $client;
/** @var bool */
public static $sendInBatches = true;
/** @var array */
protected $queue = [];
public function __construct(Client $client)
{
$this->client = $client;
register_shutdown_function([$this, 'sendQueuedReports']);
}
public static function sendReportsInBatches(bool $batchSending = true)
{
static::$sendInBatches = $batchSending;
}
public function report(Report $report)
{
try {
if (static::$sendInBatches) {
$this->addReportToQueue($report);
} else {
$this->sendReportToApi($report);
}
} catch (Exception $e) {
//
}
}
public function sendTestReport(Report $report)
{
$this->sendReportToApi($report);
}
protected function addReportToQueue(Report $report)
{
$this->queue[] = $report;
}
public function sendQueuedReports()
{
try {
foreach ($this->queue as $report) {
$this->sendReportToApi($report);
}
} catch (Exception $e) {
//
} finally {
$this->queue = [];
}
}
protected function sendReportToApi(Report $report)
{
$this->client->post('reports', $this->truncateReport($report->toArray()));
}
protected function truncateReport(array $payload): array
{
return (new ReportTrimmer())->trim($payload);
}
}

View File

@@ -0,0 +1,51 @@
<?php
namespace Facade\FlareClient\Concerns;
trait HasContext
{
/** @var string|null */
private $messageLevel;
/** @var string|null */
private $stage;
/** @var array */
private $userProvidedContext = [];
public function stage(?string $stage)
{
$this->stage = $stage;
return $this;
}
public function messageLevel(?string $messageLevel)
{
$this->messageLevel = $messageLevel;
return $this;
}
public function getGroup(string $groupName = 'context', $default = []): array
{
return $this->userProvidedContext[$groupName] ?? $default;
}
public function context($key, $value)
{
return $this->group('context', [$key => $value]);
}
public function group(string $groupName, array $properties)
{
$group = $this->userProvidedContext[$groupName] ?? [];
$this->userProvidedContext[$groupName] = array_merge_recursive_distinct(
$group,
$properties
);
return $this;
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace Facade\FlareClient\Concerns;
use Facade\FlareClient\Time\SystemTime;
use Facade\FlareClient\Time\Time;
trait UsesTime
{
/** @var \Facade\FlareClient\Time\Time */
public static $time;
public static function useTime(Time $time)
{
self::$time = $time;
}
public function getCurrentTime(): int
{
$time = self::$time ?? new SystemTime();
return $time->getCurrentTime();
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace Facade\FlareClient\Context;
class ConsoleContext implements ContextInterface
{
/** @var array */
private $arguments = [];
public function __construct(array $arguments = [])
{
$this->arguments = $arguments;
}
public function toArray(): array
{
return [
'arguments' => $this->arguments,
];
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace Facade\FlareClient\Context;
class ContextContextDetector implements ContextDetectorInterface
{
public function detectCurrentContext(): ContextInterface
{
if ($this->runningInConsole()) {
return new ConsoleContext($_SERVER['argv'] ?? []);
}
return new RequestContext();
}
private function runningInConsole(): bool
{
if (isset($_ENV['APP_RUNNING_IN_CONSOLE'])) {
return $_ENV['APP_RUNNING_IN_CONSOLE'] === 'true';
}
if (isset($_ENV['FLARE_FAKE_WEB_REQUEST'])) {
return false;
}
return in_array(php_sapi_name(), ['cli', 'phpdb']);
}
}

View File

@@ -0,0 +1,8 @@
<?php
namespace Facade\FlareClient\Context;
interface ContextDetectorInterface
{
public function detectCurrentContext(): ContextInterface;
}

View File

@@ -0,0 +1,8 @@
<?php
namespace Facade\FlareClient\Context;
interface ContextInterface
{
public function toArray(): array;
}

View File

@@ -0,0 +1,126 @@
<?php
namespace Facade\FlareClient\Context;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\Mime\Exception\InvalidArgumentException;
use Throwable;
class RequestContext implements ContextInterface
{
/** @var \Symfony\Component\HttpFoundation\Request|null */
protected $request;
public function __construct(Request $request = null)
{
$this->request = $request ?? Request::createFromGlobals();
}
public function getRequest(): array
{
return [
'url' => $this->request->getUri(),
'ip' => $this->request->getClientIp(),
'method' => $this->request->getMethod(),
'useragent' => $this->request->headers->get('User-Agent'),
];
}
private function getFiles(): array
{
if (is_null($this->request->files)) {
return [];
}
return $this->mapFiles($this->request->files->all());
}
protected function mapFiles(array $files)
{
return array_map(function ($file) {
if (is_array($file)) {
return $this->mapFiles($file);
}
if (! $file instanceof UploadedFile) {
return;
}
try {
$fileSize = $file->getSize();
} catch (\RuntimeException $e) {
$fileSize = 0;
}
try {
$mimeType = $file->getMimeType();
} catch (InvalidArgumentException $e) {
$mimeType = 'undefined';
}
return [
'pathname' => $file->getPathname(),
'size' => $fileSize,
'mimeType' => $mimeType,
];
}, $files);
}
public function getSession(): array
{
try {
$session = $this->request->getSession();
} catch (\Exception $exception) {
$session = [];
}
return $session ? $this->getValidSessionData($session) : [];
}
/**
* @param SessionInterface $session
* @return array
*/
protected function getValidSessionData($session): array
{
try {
json_encode($session->all());
} catch (Throwable $e) {
return [];
}
return $session->all();
}
public function getCookies(): array
{
return $this->request->cookies->all();
}
public function getHeaders(): array
{
return $this->request->headers->all();
}
public function getRequestData(): array
{
return [
'queryString' => $this->request->query->all(),
'body' => $this->request->request->all(),
'files' => $this->getFiles(),
];
}
public function toArray(): array
{
return [
'request' => $this->getRequest(),
'request_data' => $this->getRequestData(),
'headers' => $this->getHeaders(),
'cookies' => $this->getCookies(),
'session' => $this->getSession(),
];
}
}

View File

@@ -0,0 +1,8 @@
<?php
namespace Facade\FlareClient\Contracts;
interface ProvidesFlareContext
{
public function context(): array;
}

View File

@@ -0,0 +1,11 @@
<?php
namespace Facade\FlareClient\Enums;
/** @deprecated */
class GroupingTypes
{
public const TOP_FRAME = 'topFrame';
public const EXCEPTION = 'exceptionClass';
}

View File

@@ -0,0 +1,16 @@
<?php
namespace Facade\FlareClient\Enums;
class MessageLevels
{
public const INFO = 'info';
public const DEBUG = 'debug';
public const WARNING = 'warning';
public const ERROR = 'error';
public const CRITICAL = 'critical';
}

333
vendor/facade/flare-client-php/src/Flare.php vendored Executable file
View File

@@ -0,0 +1,333 @@
<?php
namespace Facade\FlareClient;
use Error;
use ErrorException;
use Exception;
use Facade\FlareClient\Concerns\HasContext;
use Facade\FlareClient\Context\ContextContextDetector;
use Facade\FlareClient\Context\ContextDetectorInterface;
use Facade\FlareClient\Enums\MessageLevels;
use Facade\FlareClient\Glows\Glow;
use Facade\FlareClient\Glows\Recorder;
use Facade\FlareClient\Http\Client;
use Facade\FlareClient\Middleware\AddGlows;
use Facade\FlareClient\Middleware\AnonymizeIp;
use Facade\FlareClient\Middleware\CensorRequestBodyFields;
use Illuminate\Contracts\Container\Container;
use Illuminate\Pipeline\Pipeline;
use Throwable;
class Flare
{
use HasContext;
/** @var \Facade\FlareClient\Http\Client */
protected $client;
/** @var \Facade\FlareClient\Api */
protected $api;
/** @var array */
protected $middleware = [];
/** @var \Facade\FlareClient\Glows\Recorder */
protected $recorder;
/** @var string */
protected $applicationPath;
/** @var \Illuminate\Contracts\Container\Container|null */
protected $container;
/** @var ContextDetectorInterface */
protected $contextDetector;
/** @var callable|null */
protected $previousExceptionHandler;
/** @var callable|null */
protected $previousErrorHandler;
/** @var callable|null */
protected $determineVersionCallable;
/** @var int|null */
protected $reportErrorLevels;
/** @var callable|null */
protected $filterExceptionsCallable;
/** @var callable|null */
protected $filterReportsCallable;
public static function register(string $apiKey, string $apiSecret = null, ContextDetectorInterface $contextDetector = null, Container $container = null)
{
$client = new Client($apiKey, $apiSecret);
return new static($client, $contextDetector, $container);
}
public function determineVersionUsing($determineVersionCallable)
{
$this->determineVersionCallable = $determineVersionCallable;
}
public function reportErrorLevels(int $reportErrorLevels)
{
$this->reportErrorLevels = $reportErrorLevels;
}
public function filterExceptionsUsing(callable $filterExceptionsCallable)
{
$this->filterExceptionsCallable = $filterExceptionsCallable;
}
public function filterReportsUsing(callable $filterReportsCallable)
{
$this->filterReportsCallable = $filterReportsCallable;
}
/**
* @return null|string
*/
public function version()
{
if (! $this->determineVersionCallable) {
return null;
}
return ($this->determineVersionCallable)();
}
public function __construct(Client $client, ContextDetectorInterface $contextDetector = null, Container $container = null, array $middleware = [])
{
$this->client = $client;
$this->recorder = new Recorder();
$this->contextDetector = $contextDetector ?? new ContextContextDetector();
$this->container = $container;
$this->middleware = $middleware;
$this->api = new Api($this->client);
$this->registerDefaultMiddleware();
}
public function getMiddleware(): array
{
return $this->middleware;
}
public function registerFlareHandlers()
{
$this->registerExceptionHandler();
$this->registerErrorHandler();
return $this;
}
public function registerExceptionHandler()
{
$this->previousExceptionHandler = set_exception_handler([$this, 'handleException']);
return $this;
}
public function registerErrorHandler()
{
$this->previousErrorHandler = set_error_handler([$this, 'handleError']);
return $this;
}
private function registerDefaultMiddleware()
{
return $this->registerMiddleware(new AddGlows($this->recorder));
}
public function registerMiddleware($callable)
{
$this->middleware[] = $callable;
return $this;
}
public function getMiddlewares(): array
{
return $this->middleware;
}
public function glow(
string $name,
string $messageLevel = MessageLevels::INFO,
array $metaData = []
) {
$this->recorder->record(new Glow($name, $messageLevel, $metaData));
}
public function handleException(Throwable $throwable)
{
$this->report($throwable);
if ($this->previousExceptionHandler) {
call_user_func($this->previousExceptionHandler, $throwable);
}
}
public function handleError($code, $message, $file = '', $line = 0)
{
$exception = new ErrorException($message, 0, $code, $file, $line);
$this->report($exception);
if ($this->previousErrorHandler) {
return call_user_func(
$this->previousErrorHandler,
$message,
$code,
$file,
$line
);
}
}
public function applicationPath(string $applicationPath)
{
$this->applicationPath = $applicationPath;
return $this;
}
public function report(Throwable $throwable, callable $callback = null): ?Report
{
if (! $this->shouldSendReport($throwable)) {
return null;
}
$report = $this->createReport($throwable);
if (! is_null($callback)) {
call_user_func($callback, $report);
}
$this->sendReportToApi($report);
return $report;
}
protected function shouldSendReport(Throwable $throwable): bool
{
if ($this->reportErrorLevels && $throwable instanceof Error) {
return $this->reportErrorLevels & $throwable->getCode();
}
if ($this->reportErrorLevels && $throwable instanceof ErrorException) {
return $this->reportErrorLevels & $throwable->getSeverity();
}
if ($this->filterExceptionsCallable && $throwable instanceof Exception) {
return call_user_func($this->filterExceptionsCallable, $throwable);
}
return true;
}
public function reportMessage(string $message, string $logLevel, callable $callback = null)
{
$report = $this->createReportFromMessage($message, $logLevel);
if (! is_null($callback)) {
call_user_func($callback, $report);
}
$this->sendReportToApi($report);
}
public function sendTestReport(Throwable $throwable)
{
$this->api->sendTestReport($this->createReport($throwable));
}
private function sendReportToApi(Report $report)
{
if ($this->filterReportsCallable) {
if (! call_user_func($this->filterReportsCallable, $report)) {
return;
}
}
try {
$this->api->report($report);
} catch (Exception $exception) {
}
}
public function reset()
{
$this->api->sendQueuedReports();
$this->userProvidedContext = [];
$this->recorder->reset();
}
private function applyAdditionalParameters(Report $report)
{
$report
->stage($this->stage)
->messageLevel($this->messageLevel)
->setApplicationPath($this->applicationPath)
->userProvidedContext($this->userProvidedContext);
}
public function anonymizeIp()
{
$this->registerMiddleware(new AnonymizeIp());
return $this;
}
public function censorRequestBodyFields(array $fieldNames)
{
$this->registerMiddleware(new CensorRequestBodyFields($fieldNames));
return $this;
}
public function createReport(Throwable $throwable): Report
{
$report = Report::createForThrowable(
$throwable,
$this->contextDetector->detectCurrentContext(),
$this->applicationPath,
$this->version()
);
return $this->applyMiddlewareToReport($report);
}
public function createReportFromMessage(string $message, string $logLevel): Report
{
$report = Report::createForMessage(
$message,
$logLevel,
$this->contextDetector->detectCurrentContext(),
$this->applicationPath
);
return $this->applyMiddlewareToReport($report);
}
protected function applyMiddlewareToReport(Report $report): Report
{
$this->applyAdditionalParameters($report);
$report = (new Pipeline($this->container))
->send($report)
->through($this->middleware)
->then(function ($report) {
return $report;
});
return $report;
}
}

View File

@@ -0,0 +1,66 @@
<?php
namespace Facade\FlareClient;
use Facade\FlareClient\Stacktrace\Codesnippet;
class Frame
{
/** @var string */
protected $file;
/** @var int */
protected $lineNumber;
/** @var string */
protected $method;
/** @var string */
protected $class;
public function __construct(
string $file,
int $lineNumber,
string $method = null,
string $class = null
) {
$this->file = $file;
$this->lineNumber = $lineNumber;
$this->method = $method;
$this->class = $class;
}
public function toArray(): array
{
$codeSnippet = (new Codesnippet())
->snippetLineCount(9)
->surroundingLine($this->lineNumber)
->get($this->file);
return [
'line_number' => $this->lineNumber,
'method' => $this->getFullMethod(),
'code_snippet' => $codeSnippet,
'file' => $this->file,
];
}
private function getFullMethod(): string
{
$method = $this->method;
if ($class = $this->class ?? false) {
$method = "{$class}::{$method}";
}
return $method;
}
public function getFile(): string
{
return $this->file;
}
}

View File

@@ -0,0 +1,42 @@
<?php
namespace Facade\FlareClient\Glows;
use Facade\FlareClient\Concerns\UsesTime;
use Facade\FlareClient\Enums\MessageLevels;
class Glow
{
use UsesTime;
/** @var string */
private $name;
/** @var array */
private $metaData;
/** @var string */
private $messageLevel;
/** @var float */
private $microtime;
public function __construct(string $name, string $messageLevel = MessageLevels::INFO, array $metaData = [], ?float $microtime = null)
{
$this->name = $name;
$this->messageLevel = $messageLevel;
$this->metaData = $metaData;
$this->microtime = $microtime ?? microtime(true);
}
public function toArray()
{
return [
'time' => $this->getCurrentTime(),
'name' => $this->name,
'message_level' => $this->messageLevel,
'meta_data' => $this->metaData,
'microtime' => $this->microtime,
];
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace Facade\FlareClient\Glows;
class Recorder
{
public const GLOW_LIMIT = 30;
private $glows = [];
public function record(Glow $glow)
{
$this->glows[] = $glow;
$this->glows = array_slice($this->glows, static::GLOW_LIMIT * -1, static::GLOW_LIMIT);
}
public function glows(): array
{
return $this->glows;
}
public function reset()
{
$this->glows = [];
}
}

View File

@@ -0,0 +1,217 @@
<?php
namespace Facade\FlareClient\Http;
use Facade\FlareClient\Http\Exceptions\BadResponseCode;
use Facade\FlareClient\Http\Exceptions\InvalidData;
use Facade\FlareClient\Http\Exceptions\MissingParameter;
use Facade\FlareClient\Http\Exceptions\NotFound;
class Client
{
/** @var null|string */
private $apiToken;
/** @var null|string */
private $apiSecret;
/** @var string */
private $baseUrl;
/** @var int */
private $timeout;
public function __construct(
?string $apiToken,
?string $apiSecret,
string $baseUrl = 'https://reporting.flareapp.io/api',
int $timeout = 10
) {
$this->apiToken = $apiToken;
$this->apiSecret = $apiSecret;
if (! $baseUrl) {
throw MissingParameter::create('baseUrl');
}
$this->baseUrl = $baseUrl;
if (! $timeout) {
throw MissingParameter::create('timeout');
}
$this->timeout = $timeout;
}
/**
* @param string $url
* @param array $arguments
*
* @return array|false
*/
public function get(string $url, array $arguments = [])
{
return $this->makeRequest('get', $url, $arguments);
}
/**
* @param string $url
* @param array $arguments
*
* @return array|false
*/
public function post(string $url, array $arguments = [])
{
return $this->makeRequest('post', $url, $arguments);
}
/**
* @param string $url
* @param array $arguments
*
* @return array|false
*/
public function patch(string $url, array $arguments = [])
{
return $this->makeRequest('patch', $url, $arguments);
}
/**
* @param string $url
* @param array $arguments
*
* @return array|false
*/
public function put(string $url, array $arguments = [])
{
return $this->makeRequest('put', $url, $arguments);
}
/**
* @param string $method
* @param array $arguments
*
* @return array|false
*/
public function delete(string $method, array $arguments = [])
{
return $this->makeRequest('delete', $method, $arguments);
}
/**
* @param string $httpVerb
* @param string $url
* @param array $arguments
*
* @return array
*/
private function makeRequest(string $httpVerb, string $url, array $arguments = [])
{
$queryString = http_build_query([
'key' => $this->apiToken,
'secret' => $this->apiSecret,
]);
$fullUrl = "{$this->baseUrl}/{$url}?{$queryString}";
$headers = [
'x-api-token: '.$this->apiToken,
];
$response = $this->makeCurlRequest($httpVerb, $fullUrl, $headers, $arguments);
if ($response->getHttpResponseCode() === 422) {
throw InvalidData::createForResponse($response);
}
if ($response->getHttpResponseCode() === 404) {
throw NotFound::createForResponse($response);
}
if ($response->getHttpResponseCode() !== 200 && $response->getHttpResponseCode() !== 204) {
throw BadResponseCode::createForResponse($response);
}
return $response->getBody();
}
public function makeCurlRequest(string $httpVerb, string $fullUrl, array $headers = [], array $arguments = []): Response
{
$curlHandle = $this->getCurlHandle($fullUrl, $headers);
switch ($httpVerb) {
case 'post':
curl_setopt($curlHandle, CURLOPT_POST, true);
$this->attachRequestPayload($curlHandle, $arguments);
break;
case 'get':
curl_setopt($curlHandle, CURLOPT_URL, $fullUrl.'&'.http_build_query($arguments));
break;
case 'delete':
curl_setopt($curlHandle, CURLOPT_CUSTOMREQUEST, 'DELETE');
break;
case 'patch':
curl_setopt($curlHandle, CURLOPT_CUSTOMREQUEST, 'PATCH');
$this->attachRequestPayload($curlHandle, $arguments);
break;
case 'put':
curl_setopt($curlHandle, CURLOPT_CUSTOMREQUEST, 'PUT');
$this->attachRequestPayload($curlHandle, $arguments);
break;
}
$body = json_decode(curl_exec($curlHandle), true);
$headers = curl_getinfo($curlHandle);
$error = curl_error($curlHandle);
return new Response($headers, $body, $error);
}
private function attachRequestPayload(&$curlHandle, array $data)
{
$encoded = json_encode($data);
$this->lastRequest['body'] = $encoded;
curl_setopt($curlHandle, CURLOPT_POSTFIELDS, $encoded);
}
/**
* @param string $fullUrl
* @param array $headers
*
* @return resource
*/
private function getCurlHandle(string $fullUrl, array $headers = [])
{
$curlHandle = curl_init();
curl_setopt($curlHandle, CURLOPT_URL, $fullUrl);
curl_setopt($curlHandle, CURLOPT_HTTPHEADER, array_merge([
'Accept: application/json',
'Content-Type: application/json',
], $headers));
curl_setopt($curlHandle, CURLOPT_USERAGENT, 'Laravel/Flare API 1.0');
curl_setopt($curlHandle, CURLOPT_RETURNTRANSFER, true);
curl_setopt($curlHandle, CURLOPT_TIMEOUT, $this->timeout);
curl_setopt($curlHandle, CURLOPT_SSL_VERIFYPEER, true);
curl_setopt($curlHandle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_0);
curl_setopt($curlHandle, CURLOPT_ENCODING, '');
curl_setopt($curlHandle, CURLINFO_HEADER_OUT, true);
curl_setopt($curlHandle, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($curlHandle, CURLOPT_MAXREDIRS, 1);
return $curlHandle;
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace Facade\FlareClient\Http\Exceptions;
use Exception;
use Facade\FlareClient\Http\Response;
class BadResponse extends Exception
{
/** @var \Facade\FlareClient\Http\Response */
public $response;
public static function createForResponse(Response $response)
{
$exception = new static("Could not perform request because: {$response->getError()}");
$exception->response = $response;
return $exception;
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace Facade\FlareClient\Http\Exceptions;
use Exception;
use Facade\FlareClient\Http\Response;
class BadResponseCode extends Exception
{
/** @var \Facade\FlareClient\Http\Response */
public $response;
/** @var array */
public $errors;
public static function createForResponse(Response $response)
{
$exception = new static(static::getMessageForResponse($response));
$exception->response = $response;
$bodyErrors = isset($response->getBody()['errors']) ? $response->getBody()['errors'] : [];
$exception->errors = $bodyErrors;
return $exception;
}
public static function getMessageForResponse(Response $response)
{
return "Response code {$response->getHttpResponseCode()} returned";
}
}

View File

@@ -0,0 +1,13 @@
<?php
namespace Facade\FlareClient\Http\Exceptions;
use Facade\FlareClient\Http\Response;
class InvalidData extends BadResponseCode
{
public static function getMessageForResponse(Response $response)
{
return 'Invalid data found';
}
}

View File

@@ -0,0 +1,13 @@
<?php
namespace Facade\FlareClient\Http\Exceptions;
use Exception;
class MissingParameter extends Exception
{
public static function create(string $parameterName)
{
return new static("`$parameterName` is a required parameter");
}
}

View File

@@ -0,0 +1,13 @@
<?php
namespace Facade\FlareClient\Http\Exceptions;
use Facade\FlareClient\Http\Response;
class NotFound extends BadResponseCode
{
public static function getMessageForResponse(Response $response)
{
return 'Not found';
}
}

View File

@@ -0,0 +1,65 @@
<?php
namespace Facade\FlareClient\Http;
class Response
{
private $headers;
private $body;
private $error;
public function __construct($headers, $body, $error)
{
$this->headers = $headers;
$this->body = $body;
$this->error = $error;
}
/**
* @return mixed
*/
public function getHeaders()
{
return $this->headers;
}
/**
* @return mixed
*/
public function getBody()
{
return $this->body;
}
/**
* @return bool
*/
public function hasBody()
{
return $this->body != false;
}
/**
* @return mixed
*/
public function getError()
{
return $this->error;
}
/**
* @return null|int
*/
public function getHttpResponseCode()
{
if (! isset($this->headers['http_code'])) {
return;
}
return (int) $this->headers['http_code'];
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace Facade\FlareClient\Middleware;
use Facade\FlareClient\Glows\Recorder;
use Facade\FlareClient\Report;
class AddGlows
{
/** @var Recorder */
private $recorder;
public function __construct(Recorder $recorder)
{
$this->recorder = $recorder;
}
public function handle(Report $report, $next)
{
foreach ($this->recorder->glows() as $glow) {
$report->addGlow($glow);
}
return $next($report);
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace Facade\FlareClient\Middleware;
use Facade\FlareClient\Report;
class AnonymizeIp
{
public function handle(Report $report, $next)
{
$context = $report->allContext();
$context['request']['ip'] = null;
$report->userProvidedContext($context);
return $next($report);
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace Facade\FlareClient\Middleware;
use Facade\FlareClient\Report;
class CensorRequestBodyFields
{
protected $fieldNames = [];
public function __construct(array $fieldNames)
{
$this->fieldNames = $fieldNames;
}
public function handle(Report $report, $next)
{
$context = $report->allContext();
foreach ($this->fieldNames as $fieldName) {
if (isset($context['request_data']['body'][$fieldName])) {
$context['request_data']['body'][$fieldName] = '<CENSORED>';
}
}
$report->userProvidedContext($context);
return $next($report);
}
}

View File

@@ -0,0 +1,334 @@
<?php
namespace Facade\FlareClient;
use Facade\FlareClient\Concerns\HasContext;
use Facade\FlareClient\Concerns\UsesTime;
use Facade\FlareClient\Context\ContextInterface;
use Facade\FlareClient\Contracts\ProvidesFlareContext;
use Facade\FlareClient\Enums\GroupingTypes;
use Facade\FlareClient\Glows\Glow;
use Facade\FlareClient\Solutions\ReportSolution;
use Facade\FlareClient\Stacktrace\Stacktrace;
use Facade\IgnitionContracts\Solution;
use Throwable;
class Report
{
use UsesTime;
use HasContext;
/** @var \Facade\FlareClient\Stacktrace\Stacktrace */
private $stacktrace;
/** @var string */
private $exceptionClass;
/** @var string */
private $message;
/** @var array */
private $glows = [];
/** @var array */
private $solutions = [];
/** @var ContextInterface */
private $context;
/** @var string */
private $applicationPath;
/** @var ?string */
private $applicationVersion;
/** @var array */
private $userProvidedContext = [];
/** @var array */
private $exceptionContext = [];
/** @var Throwable */
private $throwable;
/** @var string */
private $notifierName;
/** @var string */
private $languageVersion;
/** @var string */
private $frameworkVersion;
/** @var int */
private $openFrameIndex;
/** @var string */
private $groupBy ;
/** @var string */
private $trackingUuid;
/** @var null string|null */
public static $fakeTrackingUuid = null;
public static function createForThrowable(
Throwable $throwable,
ContextInterface $context,
?string $applicationPath = null,
?string $version = null
): self {
return (new static())
->setApplicationPath($applicationPath)
->throwable($throwable)
->useContext($context)
->exceptionClass(self::getClassForThrowable($throwable))
->message($throwable->getMessage())
->stackTrace(Stacktrace::createForThrowable($throwable, $applicationPath))
->exceptionContext($throwable)
->setApplicationVersion($version);
}
protected static function getClassForThrowable(Throwable $throwable): string
{
if ($throwable instanceof \Facade\Ignition\Exceptions\ViewException) {
if ($previous = $throwable->getPrevious()) {
return get_class($previous);
}
}
return get_class($throwable);
}
public static function createForMessage(string $message, string $logLevel, ContextInterface $context, ?string $applicationPath = null): self
{
$stacktrace = Stacktrace::create($applicationPath);
return (new static())
->setApplicationPath($applicationPath)
->message($message)
->useContext($context)
->exceptionClass($logLevel)
->stacktrace($stacktrace)
->openFrameIndex($stacktrace->firstApplicationFrameIndex());
}
public function __construct()
{
$this->trackingUuid = self::$fakeTrackingUuid ?? $this->generateUuid();
}
public function trackingUuid(): string
{
return $this->trackingUuid;
}
public function exceptionClass(string $exceptionClass)
{
$this->exceptionClass = $exceptionClass;
return $this;
}
public function getExceptionClass(): string
{
return $this->exceptionClass;
}
public function throwable(Throwable $throwable)
{
$this->throwable = $throwable;
return $this;
}
public function getThrowable(): ?Throwable
{
return $this->throwable;
}
public function message(string $message)
{
$this->message = $message;
return $this;
}
public function getMessage(): string
{
return $this->message;
}
public function stacktrace(Stacktrace $stacktrace)
{
$this->stacktrace = $stacktrace;
return $this;
}
public function getStacktrace(): Stacktrace
{
return $this->stacktrace;
}
public function notifierName(string $notifierName)
{
$this->notifierName = $notifierName;
return $this;
}
public function languageVersion(string $languageVersion)
{
$this->languageVersion = $languageVersion;
return $this;
}
public function frameworkVersion(string $frameworkVersion)
{
$this->frameworkVersion = $frameworkVersion;
return $this;
}
public function useContext(ContextInterface $request)
{
$this->context = $request;
return $this;
}
public function openFrameIndex(?int $index)
{
$this->openFrameIndex = $index;
return $this;
}
public function setApplicationPath(?string $applicationPath)
{
$this->applicationPath = $applicationPath;
return $this;
}
public function getApplicationPath(): ?string
{
return $this->applicationPath;
}
public function setApplicationVersion(?string $applicationVersion)
{
$this->applicationVersion = $applicationVersion;
return $this;
}
public function getApplicationVersion(): ?string
{
return $this->applicationVersion;
}
public function view(?View $view)
{
$this->view = $view;
return $this;
}
public function addGlow(Glow $glow)
{
$this->glows[] = $glow->toArray();
return $this;
}
public function addSolution(Solution $solution)
{
$this->solutions[] = ReportSolution::fromSolution($solution)->toArray();
return $this;
}
public function userProvidedContext(array $userProvidedContext)
{
$this->userProvidedContext = $userProvidedContext;
return $this;
}
/** @deprecated */
public function groupByTopFrame()
{
$this->groupBy = GroupingTypes::TOP_FRAME;
return $this;
}
/** @deprecated */
public function groupByException()
{
$this->groupBy = GroupingTypes::EXCEPTION;
return $this;
}
public function allContext(): array
{
$context = $this->context->toArray();
$context = array_merge_recursive_distinct($context, $this->exceptionContext);
return array_merge_recursive_distinct($context, $this->userProvidedContext);
}
private function exceptionContext(Throwable $throwable)
{
if ($throwable instanceof ProvidesFlareContext) {
$this->exceptionContext = $throwable->context();
}
return $this;
}
public function toArray()
{
return [
'notifier' => $this->notifierName ?? 'Flare Client',
'language' => 'PHP',
'framework_version' => $this->frameworkVersion,
'language_version' => $this->languageVersion ?? phpversion(),
'exception_class' => $this->exceptionClass,
'seen_at' => $this->getCurrentTime(),
'message' => $this->message,
'glows' => $this->glows,
'solutions' => $this->solutions,
'stacktrace' => $this->stacktrace->toArray(),
'context' => $this->allContext(),
'stage' => $this->stage,
'message_level' => $this->messageLevel,
'open_frame_index' => $this->openFrameIndex,
'application_path' => $this->applicationPath,
'application_version' => $this->applicationVersion,
'tracking_uuid' => $this->trackingUuid,
];
}
/*
* Found on https://stackoverflow.com/questions/2040240/php-function-to-generate-v4-uuid/15875555#15875555
*/
private function generateUuid(): string
{
// Generate 16 bytes (128 bits) of random data or use the data passed into the function.
$data = $data ?? random_bytes(16);
// Set version to 0100
$data[6] = chr(ord($data[6]) & 0x0f | 0x40);
// Set bits 6-7 to 10
$data[8] = chr(ord($data[8]) & 0x3f | 0x80);
// Output the 36 character UUID.
return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4));
}
}

View File

@@ -0,0 +1,36 @@
<?php
namespace Facade\FlareClient\Solutions;
use Facade\IgnitionContracts\RunnableSolution;
use Facade\IgnitionContracts\Solution as SolutionContract;
class ReportSolution
{
/** @var SolutionContract */
protected $solution;
public function __construct(SolutionContract $solution)
{
$this->solution = $solution;
}
public static function fromSolution(SolutionContract $solution)
{
return new static($solution);
}
public function toArray(): array
{
$isRunnable = ($this->solution instanceof RunnableSolution);
return [
'class' => get_class($this->solution),
'title' => $this->solution->getSolutionTitle(),
'description' => $this->solution->getSolutionDescription(),
'links' => $this->solution->getDocumentationLinks(),
'action_description' => $isRunnable ? $this->solution->getSolutionActionDescription() : null,
'is_runnable' => $isRunnable,
];
}
}

View File

@@ -0,0 +1,72 @@
<?php
namespace Facade\FlareClient\Stacktrace;
use RuntimeException;
class Codesnippet
{
/** @var int */
private $surroundingLine = 1;
/** @var int */
private $snippetLineCount = 9;
public function surroundingLine(int $surroundingLine): self
{
$this->surroundingLine = $surroundingLine;
return $this;
}
public function snippetLineCount(int $snippetLineCount): self
{
$this->snippetLineCount = $snippetLineCount;
return $this;
}
public function get(string $fileName): array
{
if (! file_exists($fileName)) {
return [];
}
try {
$file = new File($fileName);
[$startLineNumber, $endLineNumber] = $this->getBounds($file->numberOfLines());
$code = [];
$line = $file->getLine($startLineNumber);
$currentLineNumber = $startLineNumber;
while ($currentLineNumber <= $endLineNumber) {
$code[$currentLineNumber] = rtrim(substr($line, 0, 250));
$line = $file->getNextLine();
$currentLineNumber++;
}
return $code;
} catch (RuntimeException $exception) {
return [];
}
}
private function getBounds($totalNumberOfLineInFile): array
{
$startLine = max($this->surroundingLine - floor($this->snippetLineCount / 2), 1);
$endLine = $startLine + ($this->snippetLineCount - 1);
if ($endLine > $totalNumberOfLineInFile) {
$endLine = $totalNumberOfLineInFile;
$startLine = max($endLine - ($this->snippetLineCount - 1), 1);
}
return [$startLine, $endLine];
}
}

View File

@@ -0,0 +1,41 @@
<?php
namespace Facade\FlareClient\Stacktrace;
use SplFileObject;
class File
{
/** @var \SplFileObject */
private $file;
public function __construct(string $path)
{
$this->file = new SplFileObject($path);
}
public function numberOfLines(): int
{
$this->file->seek(PHP_INT_MAX);
return $this->file->key() + 1;
}
public function getLine(int $lineNumber = null): string
{
if (is_null($lineNumber)) {
return $this->getNextLine();
}
$this->file->seek($lineNumber - 1);
return $this->file->current();
}
public function getNextLine(): string
{
$this->file->next();
return $this->file->current();
}
}

View File

@@ -0,0 +1,71 @@
<?php
namespace Facade\FlareClient\Stacktrace;
class Frame
{
/** @var string */
private $file;
/** @var int */
private $lineNumber;
/** @var string */
private $method;
/** @var string */
private $class;
/** @var bool */
private $isApplicationFrame;
public function __construct(
string $file,
int $lineNumber,
string $method = null,
string $class = null,
bool $isApplicationFrame = false
) {
$this->file = $file;
$this->lineNumber = $lineNumber;
$this->method = $method;
$this->class = $class;
$this->isApplicationFrame = $isApplicationFrame;
}
public function toArray(): array
{
$codeSnippet = (new Codesnippet())
->snippetLineCount(31)
->surroundingLine($this->lineNumber)
->get($this->file);
return [
'line_number' => $this->lineNumber,
'method' => $this->method,
'class' => $this->class,
'code_snippet' => $codeSnippet,
'file' => $this->file,
'is_application_frame' => $this->isApplicationFrame,
];
}
public function getFile(): string
{
return $this->file;
}
public function getLinenumber(): int
{
return $this->lineNumber;
}
public function isApplicationFrame()
{
return $this->isApplicationFrame;
}
}

View File

@@ -0,0 +1,126 @@
<?php
namespace Facade\FlareClient\Stacktrace;
use Throwable;
class Stacktrace
{
/** @var \Facade\FlareClient\Stacktrace\Frame[] */
private $frames;
/** @var string */
private $applicationPath;
public static function createForThrowable(Throwable $throwable, ?string $applicationPath = null): self
{
return new static($throwable->getTrace(), $applicationPath, $throwable->getFile(), $throwable->getLine());
}
public static function create(?string $applicationPath = null): self
{
$backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS & ~DEBUG_BACKTRACE_PROVIDE_OBJECT);
return new static($backtrace, $applicationPath);
}
public function __construct(array $backtrace, ?string $applicationPath = null, string $topmostFile = null, string $topmostLine = null)
{
$this->applicationPath = $applicationPath;
$currentFile = $topmostFile;
$currentLine = $topmostLine;
foreach ($backtrace as $rawFrame) {
if (! $this->frameFromFlare($rawFrame) && ! $this->fileIgnored($currentFile)) {
$this->frames[] = new Frame(
$currentFile,
$currentLine,
$rawFrame['function'] ?? null,
$rawFrame['class'] ?? null,
$this->frameFileFromApplication($currentFile)
);
}
$currentFile = $rawFrame['file'] ?? 'unknown';
$currentLine = $rawFrame['line'] ?? 0;
}
$this->frames[] = new Frame(
$currentFile,
$currentLine,
'[top]'
);
}
protected function frameFromFlare(array $rawFrame): bool
{
return isset($rawFrame['class']) && strpos($rawFrame['class'], 'Facade\\FlareClient\\') === 0;
}
protected function frameFileFromApplication(string $frameFilename): bool
{
$relativeFile = str_replace('\\', DIRECTORY_SEPARATOR, $frameFilename);
if (! empty($this->applicationPath)) {
$relativeFile = array_reverse(explode($this->applicationPath ?? '', $frameFilename, 2))[0];
}
if (strpos($relativeFile, DIRECTORY_SEPARATOR . 'vendor') === 0) {
return false;
}
return true;
}
protected function fileIgnored(string $currentFile): bool
{
$currentFile = str_replace('\\', DIRECTORY_SEPARATOR, $currentFile);
$ignoredFiles = [
'/ignition/src/helpers.php',
];
foreach ($ignoredFiles as $ignoredFile) {
if (strstr($currentFile, $ignoredFile) !== false) {
return true;
}
}
return false;
}
public function firstFrame(): Frame
{
return $this->frames[0];
}
public function toArray(): array
{
return array_map(function (Frame $frame) {
return $frame->toArray();
}, $this->frames);
}
public function firstApplicationFrame(): ?Frame
{
foreach ($this->frames as $index => $frame) {
if ($frame->isApplicationFrame()) {
return $frame;
}
}
return null;
}
public function firstApplicationFrameIndex(): ?int
{
foreach ($this->frames as $index => $frame) {
if ($frame->isApplicationFrame()) {
return $index;
}
}
return null;
}
}

View File

@@ -0,0 +1,13 @@
<?php
namespace Facade\FlareClient\Time;
use DateTimeImmutable;
class SystemTime implements Time
{
public function getCurrentTime(): int
{
return (new DateTimeImmutable())->getTimestamp();
}
}

View File

@@ -0,0 +1,8 @@
<?php
namespace Facade\FlareClient\Time;
interface Time
{
public function getCurrentTime(): int;
}

View File

@@ -0,0 +1,14 @@
<?php
namespace Facade\FlareClient\Truncation;
abstract class AbstractTruncationStrategy implements TruncationStrategy
{
/** @var ReportTrimmer */
protected $reportTrimmer;
public function __construct(ReportTrimmer $reportTrimmer)
{
$this->reportTrimmer = $reportTrimmer;
}
}

View File

@@ -0,0 +1,41 @@
<?php
namespace Facade\FlareClient\Truncation;
class ReportTrimmer
{
protected static $maxPayloadSize = 524288;
protected $strategies = [
TrimStringsStrategy::class,
TrimContextItemsStrategy::class,
];
public function trim(array $payload): array
{
foreach ($this->strategies as $strategy) {
if (! $this->needsToBeTrimmed($payload)) {
break;
}
$payload = (new $strategy($this))->execute($payload);
}
return $payload;
}
public function needsToBeTrimmed(array $payload): bool
{
return strlen(json_encode($payload)) > self::getMaxPayloadSize();
}
public static function getMaxPayloadSize(): int
{
return self::$maxPayloadSize;
}
public static function setMaxPayloadSize(int $maxPayloadSize): void
{
self::$maxPayloadSize = $maxPayloadSize;
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace Facade\FlareClient\Truncation;
class TrimContextItemsStrategy extends AbstractTruncationStrategy
{
public static function thresholds()
{
return [100, 50, 25, 10];
}
public function execute(array $payload): array
{
foreach (static::thresholds() as $threshold) {
if (! $this->reportTrimmer->needsToBeTrimmed($payload)) {
break;
}
$payload['context'] = $this->iterateContextItems($payload['context'], $threshold);
}
return $payload;
}
protected function iterateContextItems(array $contextItems, int $threshold): array
{
array_walk($contextItems, [$this, 'trimContextItems'], $threshold);
return $contextItems;
}
protected function trimContextItems(&$value, $key, int $threshold)
{
if (is_array($value)) {
if (count($value) > $threshold) {
$value = array_slice($value, $threshold * -1, $threshold);
}
array_walk($value, [$this, 'trimContextItems'], $threshold);
}
return $value;
}
}

View File

@@ -0,0 +1,35 @@
<?php
namespace Facade\FlareClient\Truncation;
class TrimStringsStrategy extends AbstractTruncationStrategy
{
public static function thresholds()
{
return [1024, 512, 256];
}
public function execute(array $payload): array
{
foreach (static::thresholds() as $threshold) {
if (! $this->reportTrimmer->needsToBeTrimmed($payload)) {
break;
}
$payload = $this->trimPayloadString($payload, $threshold);
}
return $payload;
}
protected function trimPayloadString(array $payload, int $threshold): array
{
array_walk_recursive($payload, function (&$value) use ($threshold) {
if (is_string($value) && strlen($value) > $threshold) {
$value = substr($value, 0, $threshold);
}
});
return $payload;
}
}

View File

@@ -0,0 +1,8 @@
<?php
namespace Facade\FlareClient\Truncation;
interface TruncationStrategy
{
public function execute(array $payload): array;
}

View File

@@ -0,0 +1,51 @@
<?php
namespace Facade\FlareClient;
use Symfony\Component\VarDumper\Cloner\VarCloner;
use Symfony\Component\VarDumper\Dumper\HtmlDumper;
class View
{
/** @var string */
private $file;
/** @var array */
private $data = [];
public function __construct(string $file, array $data = [])
{
$this->file = $file;
$this->data = $data;
}
public static function create(string $file, array $data = []): self
{
return new static($file, $data);
}
private function dumpViewData($variable): string
{
$cloner = new VarCloner();
$dumper = new HtmlDumper();
$dumper->setDumpHeader('');
$output = fopen('php://memory', 'r+b');
$dumper->dump($cloner->cloneVar($variable)->withMaxDepth(1), $output, [
'maxDepth' => 1,
'maxStringLength' => 160,
]);
return stream_get_contents($output, -1, 0);
}
public function toArray()
{
return [
'file' => $this->file,
'data' => array_map([$this, 'dumpViewData'], $this->data),
];
}
}

View File

@@ -0,0 +1,17 @@
<?php
if (! function_exists('array_merge_recursive_distinct')) {
function array_merge_recursive_distinct(array &$array1, array &$array2)
{
$merged = $array1;
foreach ($array2 as $key => &$value) {
if (is_array($value) && isset($merged[$key]) && is_array($merged[$key])) {
$merged[$key] = array_merge_recursive_distinct($merged[$key], $value);
} else {
$merged[$key] = $value;
}
}
return $merged;
}
}