diff --git a/.gitattributes b/.gitattributes index df47436..02ff471 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,3 +1,4 @@ ./tests export-ignore .gitattributes export-ignore -.gitignore export-ignore \ No newline at end of file +.gitignore export-ignore +composer.lock \ No newline at end of file diff --git a/.gitignore b/.gitignore index 9bd38c1..b735a16 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,7 @@ Homestead.json .rocketeer/ .idea/ +tests/tmp/ +tests/vendors/*/vendor/te +test_multiple_versions/ +composer.lock diff --git a/.travis.yml b/.travis.yml index f2df30c..843e192 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,9 +4,45 @@ matrix: fast_finish: true include: - php: 5.6 + env: ILLUMINATE_VERSION=5.2.* PHPUNIT_VERSION=5.* + - php: 5.6 + env: ILLUMINATE_VERSION=5.3.* PHPUNIT_VERSION=5.* + - php: 5.6 + env: ILLUMINATE_VERSION=5.4.* PHPUNIT_VERSION=5.* + - php: 7.0 + env: ILLUMINATE_VERSION=5.2.* PHPUNIT_VERSION=5.* + - php: 7.0 + env: ILLUMINATE_VERSION=5.3.* PHPUNIT_VERSION=5.* + - php: 7.0 + env: ILLUMINATE_VERSION=5.4.* PHPUNIT_VERSION=5.* - php: 7.0 + env: ILLUMINATE_VERSION=5.5.* PHPUNIT_VERSION=6.* + - php: 7.1 + env: ILLUMINATE_VERSION=5.2.* PHPUNIT_VERSION=5.* + - php: 7.1 + env: ILLUMINATE_VERSION=5.3.* PHPUNIT_VERSION=5.* + - php: 7.1 + env: ILLUMINATE_VERSION=5.4.* PHPUNIT_VERSION=5.* + - php: 7.1 + env: ILLUMINATE_VERSION=5.5.* PHPUNIT_VERSION=6.* - php: 7.1 + env: ILLUMINATE_VERSION=5.6.* PHPUNIT_VERSION=7.* + - php: 7.2 + env: ILLUMINATE_VERSION=5.2.* PHPUNIT_VERSION=5.* + - php: 7.2 + env: ILLUMINATE_VERSION=5.3.* PHPUNIT_VERSION=5.* + - php: 7.2 + env: ILLUMINATE_VERSION=5.4.* PHPUNIT_VERSION=5.* + - php: 7.2 + env: ILLUMINATE_VERSION=5.5.* PHPUNIT_VERSION=6.* + - php: 7.2 + env: ILLUMINATE_VERSION=5.6.* PHPUNIT_VERSION=7.* before_script: - composer self-update - - composer update + - composer config secure-http false + - composer config disable-tls true + - composer config + - composer require "illuminate/support:${ILLUMINATE_VERSION}" --no-update + - composer require "phpunit/phpunit:${PHPUNIT_VERSION}" --no-update + - travis_wait 30 composer update -vvv diff --git a/README.md b/README.md index 235e743..8fea288 100644 --- a/README.md +++ b/README.md @@ -1,86 +1,86 @@ [![Packagist](https://img.shields.io/packagist/dt/lezhnev74/apideveloperio-laravel.svg)]() [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/lezhnev74/apideveloperio-laravel/master/LICENSE) -[![Build Status](https://travis-ci.org/lezhnev74/apideveloperio-laravel.svg?branch=laravel-54)](https://travis-ci.org/lezhnev74/apideveloperio-laravel) +[![Build Status](https://travis-ci.org/lezhnev74/apideveloperio-laravel.svg?branch=master)](https://travis-ci.org/lezhnev74/apideveloperio-laravel) -# Laravel package to dump HTTP requests to your Dashboard -Laravel API adapter to track each http request app handled. +# Laravel package to record and send text logs and HTTP logs to your Apideveloper.io Dashboard +It works with both HTTP logs and normal text logs. It requires you to have an API Key to send recorded logs to your dashboard. HTTP logs and text logs require different keys. -## Installation +## Requirements +This package works with **PHP 5.6** and higher including (7.0, 7.1 and 7.2). It also supports **Laravel framework 5.2** and higher. -### Version Compability - Laravel version | Package version | Composer command -:---------|:----------|:--------- - 5.3.x | 3.0.x | `composer require "lezhnev74/apideveloperio-laravel=~3.0"` - 5.5.x, 5.4.x | 4.0.x | `composer require "lezhnev74/apideveloperio-laravel=~4.0"` +## Installation ### Steps #### 1. Install the package ``` -composer require "lezhnev74/apideveloperio-laravel=~3.0" +composer require "lezhnev74/apideveloperio-laravel=~6.0" ``` #### 2. Add service provider to your `config/app.php` ```php 'providers' => [ - ... - '\HttpAnalyzer\Laravel\HttpAnalyzerServiceProvider' + //... + '\Apideveloper\Laravel\Laravel\ApideveloperioServiceProvider' ], ``` -#### 3. Run this this command to publish configuration file to your `/config` folder. +#### 3. Run this command to publish configuration file to your `/config` folder. ``` -php artisan vendor:publish --provider="HttpAnalyzer\Laravel\HttpAnalyzerServiceProvider" +php artisan vendor:publish --provider="Apideveloper\Laravel\Laravel\ApideveloperioServiceProvider" ``` -#### 4. Set-up cron command -To dump recorded requests to the Dashboard. Open your `app/Console/Kernel.php` and add class to commands list. +#### 4. Set-up CRON command +To dump recorded logs to the Dashboard. Open your `app/Console/Kernel.php` and add class to commands list. ```php -#app/Console/Kernel.php +#file: app/Console/Kernel.php .... protected $commands = [ - ... - '\HttpAnalyzer\Laravel\DumpRecordedRequests', + //... + '\Apideveloper\Laravel\Laravel\SendDumpsToDashboard', ]; ... protected function schedule(Schedule $schedule) { - // you can set how often you want it to dump your requests to the Dashboard + // you can set how often you want it to dump your data to the Dashboard // every minute is the most frequent mode - $schedule->command('http_analyzer:dump')->everyMinute(); + $schedule->command('apideveloper:send-logs')->everyMinute(); } ``` #### 5. That's it! ## Configuration -After publishing, config file will be located at `config/http_analyzer.php` and speaks for himself. -The only required configuration is to put your API Key under `api_key` field. +After publishing, the config file will be located at `config/apideveloperio_logs.php` and speaks for himself. +The only required configuration is to put your API Key under `api_key` field for both `httplog` and `textlog` sections. +You can disable/enable HTTP logging as well as textual logging independently. ## FAQ #### How it works? -It hooks into Laravel app and records request, response and other data that you will see in your Dashboard: +It hooks into Laravel app and records request, response and text logs that you will see on your Dashboard: * incoming request * response * database queries * log entries +* exceptions (including stack traces) -You can tweak which information you would like to send to the Dashboard. +You can tweak which information you would like to send to the Dashboard using the config file. -The command `http_analyzer:dump` that you have set up will send all recorded requests to your Dashboard. +The command `apideveloper:send-logs` that you have set up will send all recorded logs to your Dashboard. -#### I see no errors on the screen, but I don't see any requests in my dashboard. Why? +#### I see no errors on the screen, but I don't see any requests on my dashboard. Why? -This package is designed to fail silently. If something went wrong while recording your requests - plugin won't interrupt your request lifecycle. Open your log and see if the package appended any critical information in there. +This package is designed to fail silently. If something went wrong while recording your requests - plugin won't interrupt your request lifecycle. +Open your log and see if the package appended any critical information in there. -Also check the tmp storage folder if there are any stale dump files. +Also, check the tmp storage folder if there are any stale dump files. ## Suggestions @@ -90,8 +90,12 @@ That happens due to some Symfony's Request issue. Try using this package - https #### What is the best way to track each request? When someone refers to particular request/response app cycle, it is best to know it's unique ID. - Knowing it you can easily find it in the Dashboard. + Knowing it, you can easily find it on the Dashboard. Just add a middleware (like this one https://github.com/softonic/laravel-middleware-request-id) which will append a unique ID to each response your app provides. +## 🏆 Contributors +- [Owen Melbourne](https://github.com/OwenMelbz) - improved dates conversions +- [Mark Topper](https://github.com/marktopper) - added support for Laravel 5.6 + ## Support Just open a new Issue here and get help. \ No newline at end of file diff --git a/composer.json b/composer.json index 9489730..c3c23d5 100644 --- a/composer.json +++ b/composer.json @@ -18,22 +18,23 @@ } ], "require": { - "php": ">=5.6.4", - "illuminate/support": "~5.4|~5.5", - "guzzlehttp/guzzle": "~6.0|~5.0" + "php": ">=5.6", + "illuminate/support": "~5.2", + "guzzlehttp/guzzle": "~5.0|~6.0", + "ramsey/uuid": "~3.0" }, "require-dev": { - "phpunit/phpunit": "~5.0", - "orchestra/testbench": "^3.4" + "phpunit/phpunit": "~5.0|~6.0|~7.0", + "orchestra/testbench": "^3.2|^3.3|^3.4|^3.5|^3.6" }, "autoload": { "psr-4": { - "HttpAnalyzer\\": "src/" + "Apideveloper\\Laravel\\": "src/" } }, "autoload-dev": { "psr-4": { - "HttpAnalyzerTest\\": "tests/" + "Apideveloper\\Laravel\\Tests\\": "tests/" } } } diff --git a/composer.lock b/composer.lock deleted file mode 100644 index 79efdcd..0000000 --- a/composer.lock +++ /dev/null @@ -1,3311 +0,0 @@ -{ - "_readme": [ - "This file locks the dependencies of your project to a known state", - "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", - "This file is @generated automatically" - ], - "content-hash": "223a75277ab5a8cf3365b1063c24068b", - "packages": [ - { - "name": "doctrine/inflector", - "version": "v1.2.0", - "source": { - "type": "git", - "url": "https://github.com/doctrine/inflector.git", - "reference": "e11d84c6e018beedd929cff5220969a3c6d1d462" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/doctrine/inflector/zipball/e11d84c6e018beedd929cff5220969a3c6d1d462", - "reference": "e11d84c6e018beedd929cff5220969a3c6d1d462", - "shasum": "" - }, - "require": { - "php": "^7.0" - }, - "require-dev": { - "phpunit/phpunit": "^6.2" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.2.x-dev" - } - }, - "autoload": { - "psr-4": { - "Doctrine\\Common\\Inflector\\": "lib/Doctrine/Common/Inflector" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Roman Borschel", - "email": "roman@code-factory.org" - }, - { - "name": "Benjamin Eberlei", - "email": "kontakt@beberlei.de" - }, - { - "name": "Guilherme Blanco", - "email": "guilhermeblanco@gmail.com" - }, - { - "name": "Jonathan Wage", - "email": "jonwage@gmail.com" - }, - { - "name": "Johannes Schmitt", - "email": "schmittjoh@gmail.com" - } - ], - "description": "Common String Manipulations with regard to casing and singular/plural rules.", - "homepage": "http://www.doctrine-project.org", - "keywords": [ - "inflection", - "pluralize", - "singularize", - "string" - ], - "time": "2017-07-22T12:18:28+00:00" - }, - { - "name": "erusev/parsedown", - "version": "1.6.3", - "source": { - "type": "git", - "url": "https://github.com/erusev/parsedown.git", - "reference": "728952b90a333b5c6f77f06ea9422b94b585878d" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/erusev/parsedown/zipball/728952b90a333b5c6f77f06ea9422b94b585878d", - "reference": "728952b90a333b5c6f77f06ea9422b94b585878d", - "shasum": "" - }, - "require": { - "php": ">=5.3.0" - }, - "type": "library", - "autoload": { - "psr-0": { - "Parsedown": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Emanuil Rusev", - "email": "hello@erusev.com", - "homepage": "http://erusev.com" - } - ], - "description": "Parser for Markdown.", - "homepage": "http://parsedown.org", - "keywords": [ - "markdown", - "parser" - ], - "time": "2017-05-14T14:47:48+00:00" - }, - { - "name": "guzzlehttp/guzzle", - "version": "6.3.0", - "source": { - "type": "git", - "url": "https://github.com/guzzle/guzzle.git", - "reference": "f4db5a78a5ea468d4831de7f0bf9d9415e348699" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/f4db5a78a5ea468d4831de7f0bf9d9415e348699", - "reference": "f4db5a78a5ea468d4831de7f0bf9d9415e348699", - "shasum": "" - }, - "require": { - "guzzlehttp/promises": "^1.0", - "guzzlehttp/psr7": "^1.4", - "php": ">=5.5" - }, - "require-dev": { - "ext-curl": "*", - "phpunit/phpunit": "^4.0 || ^5.0", - "psr/log": "^1.0" - }, - "suggest": { - "psr/log": "Required for using the Log middleware" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "6.2-dev" - } - }, - "autoload": { - "files": [ - "src/functions_include.php" - ], - "psr-4": { - "GuzzleHttp\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Michael Dowling", - "email": "mtdowling@gmail.com", - "homepage": "https://github.com/mtdowling" - } - ], - "description": "Guzzle is a PHP HTTP client library", - "homepage": "http://guzzlephp.org/", - "keywords": [ - "client", - "curl", - "framework", - "http", - "http client", - "rest", - "web service" - ], - "time": "2017-06-22T18:50:49+00:00" - }, - { - "name": "guzzlehttp/promises", - "version": "v1.3.1", - "source": { - "type": "git", - "url": "https://github.com/guzzle/promises.git", - "reference": "a59da6cf61d80060647ff4d3eb2c03a2bc694646" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/guzzle/promises/zipball/a59da6cf61d80060647ff4d3eb2c03a2bc694646", - "reference": "a59da6cf61d80060647ff4d3eb2c03a2bc694646", - "shasum": "" - }, - "require": { - "php": ">=5.5.0" - }, - "require-dev": { - "phpunit/phpunit": "^4.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.4-dev" - } - }, - "autoload": { - "psr-4": { - "GuzzleHttp\\Promise\\": "src/" - }, - "files": [ - "src/functions_include.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Michael Dowling", - "email": "mtdowling@gmail.com", - "homepage": "https://github.com/mtdowling" - } - ], - "description": "Guzzle promises library", - "keywords": [ - "promise" - ], - "time": "2016-12-20T10:07:11+00:00" - }, - { - "name": "guzzlehttp/psr7", - "version": "1.4.2", - "source": { - "type": "git", - "url": "https://github.com/guzzle/psr7.git", - "reference": "f5b8a8512e2b58b0071a7280e39f14f72e05d87c" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/f5b8a8512e2b58b0071a7280e39f14f72e05d87c", - "reference": "f5b8a8512e2b58b0071a7280e39f14f72e05d87c", - "shasum": "" - }, - "require": { - "php": ">=5.4.0", - "psr/http-message": "~1.0" - }, - "provide": { - "psr/http-message-implementation": "1.0" - }, - "require-dev": { - "phpunit/phpunit": "~4.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.4-dev" - } - }, - "autoload": { - "psr-4": { - "GuzzleHttp\\Psr7\\": "src/" - }, - "files": [ - "src/functions_include.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Michael Dowling", - "email": "mtdowling@gmail.com", - "homepage": "https://github.com/mtdowling" - }, - { - "name": "Tobias Schultze", - "homepage": "https://github.com/Tobion" - } - ], - "description": "PSR-7 message implementation that also provides common utility methods", - "keywords": [ - "http", - "message", - "request", - "response", - "stream", - "uri", - "url" - ], - "time": "2017-03-20T17:10:46+00:00" - }, - { - "name": "laravel/framework", - "version": "v5.4.36", - "source": { - "type": "git", - "url": "https://github.com/laravel/framework.git", - "reference": "1062a22232071c3e8636487c86ec1ae75681bbf9" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/1062a22232071c3e8636487c86ec1ae75681bbf9", - "reference": "1062a22232071c3e8636487c86ec1ae75681bbf9", - "shasum": "" - }, - "require": { - "doctrine/inflector": "~1.1", - "erusev/parsedown": "~1.6", - "ext-mbstring": "*", - "ext-openssl": "*", - "league/flysystem": "~1.0", - "monolog/monolog": "~1.11", - "mtdowling/cron-expression": "~1.0", - "nesbot/carbon": "~1.20", - "paragonie/random_compat": "~1.4|~2.0", - "php": ">=5.6.4", - "ramsey/uuid": "~3.0", - "swiftmailer/swiftmailer": "~5.4", - "symfony/console": "~3.2", - "symfony/debug": "~3.2", - "symfony/finder": "~3.2", - "symfony/http-foundation": "~3.2", - "symfony/http-kernel": "~3.2", - "symfony/process": "~3.2", - "symfony/routing": "~3.2", - "symfony/var-dumper": "~3.2", - "tijsverkoyen/css-to-inline-styles": "~2.2", - "vlucas/phpdotenv": "~2.2" - }, - "replace": { - "illuminate/auth": "self.version", - "illuminate/broadcasting": "self.version", - "illuminate/bus": "self.version", - "illuminate/cache": "self.version", - "illuminate/config": "self.version", - "illuminate/console": "self.version", - "illuminate/container": "self.version", - "illuminate/contracts": "self.version", - "illuminate/cookie": "self.version", - "illuminate/database": "self.version", - "illuminate/encryption": "self.version", - "illuminate/events": "self.version", - "illuminate/exception": "self.version", - "illuminate/filesystem": "self.version", - "illuminate/hashing": "self.version", - "illuminate/http": "self.version", - "illuminate/log": "self.version", - "illuminate/mail": "self.version", - "illuminate/notifications": "self.version", - "illuminate/pagination": "self.version", - "illuminate/pipeline": "self.version", - "illuminate/queue": "self.version", - "illuminate/redis": "self.version", - "illuminate/routing": "self.version", - "illuminate/session": "self.version", - "illuminate/support": "self.version", - "illuminate/translation": "self.version", - "illuminate/validation": "self.version", - "illuminate/view": "self.version", - "tightenco/collect": "self.version" - }, - "require-dev": { - "aws/aws-sdk-php": "~3.0", - "doctrine/dbal": "~2.5", - "mockery/mockery": "~0.9.4", - "pda/pheanstalk": "~3.0", - "phpunit/phpunit": "~5.7", - "predis/predis": "~1.0", - "symfony/css-selector": "~3.2", - "symfony/dom-crawler": "~3.2" - }, - "suggest": { - "aws/aws-sdk-php": "Required to use the SQS queue driver and SES mail driver (~3.0).", - "doctrine/dbal": "Required to rename columns and drop SQLite columns (~2.5).", - "fzaninotto/faker": "Required to use the eloquent factory builder (~1.4).", - "guzzlehttp/guzzle": "Required to use the Mailgun and Mandrill mail drivers and the ping methods on schedules (~6.0).", - "laravel/tinker": "Required to use the tinker console command (~1.0).", - "league/flysystem-aws-s3-v3": "Required to use the Flysystem S3 driver (~1.0).", - "league/flysystem-rackspace": "Required to use the Flysystem Rackspace driver (~1.0).", - "nexmo/client": "Required to use the Nexmo transport (~1.0).", - "pda/pheanstalk": "Required to use the beanstalk queue driver (~3.0).", - "predis/predis": "Required to use the redis cache and queue drivers (~1.0).", - "pusher/pusher-php-server": "Required to use the Pusher broadcast driver (~2.0).", - "symfony/css-selector": "Required to use some of the crawler integration testing tools (~3.2).", - "symfony/dom-crawler": "Required to use most of the crawler integration testing tools (~3.2).", - "symfony/psr-http-message-bridge": "Required to psr7 bridging features (0.2.*)." - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.4-dev" - } - }, - "autoload": { - "files": [ - "src/Illuminate/Foundation/helpers.php", - "src/Illuminate/Support/helpers.php" - ], - "psr-4": { - "Illuminate\\": "src/Illuminate/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Taylor Otwell", - "email": "taylor@laravel.com" - } - ], - "description": "The Laravel Framework.", - "homepage": "https://laravel.com", - "keywords": [ - "framework", - "laravel" - ], - "time": "2017-08-30T09:26:16+00:00" - }, - { - "name": "league/flysystem", - "version": "1.0.41", - "source": { - "type": "git", - "url": "https://github.com/thephpleague/flysystem.git", - "reference": "f400aa98912c561ba625ea4065031b7a41e5a155" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/f400aa98912c561ba625ea4065031b7a41e5a155", - "reference": "f400aa98912c561ba625ea4065031b7a41e5a155", - "shasum": "" - }, - "require": { - "php": ">=5.5.9" - }, - "conflict": { - "league/flysystem-sftp": "<1.0.6" - }, - "require-dev": { - "ext-fileinfo": "*", - "mockery/mockery": "~0.9", - "phpspec/phpspec": "^2.2", - "phpunit/phpunit": "~4.8" - }, - "suggest": { - "ext-fileinfo": "Required for MimeType", - "league/flysystem-aws-s3-v2": "Allows you to use S3 storage with AWS SDK v2", - "league/flysystem-aws-s3-v3": "Allows you to use S3 storage with AWS SDK v3", - "league/flysystem-azure": "Allows you to use Windows Azure Blob storage", - "league/flysystem-cached-adapter": "Flysystem adapter decorator for metadata caching", - "league/flysystem-eventable-filesystem": "Allows you to use EventableFilesystem", - "league/flysystem-rackspace": "Allows you to use Rackspace Cloud Files", - "league/flysystem-sftp": "Allows you to use SFTP server storage via phpseclib", - "league/flysystem-webdav": "Allows you to use WebDAV storage", - "league/flysystem-ziparchive": "Allows you to use ZipArchive adapter", - "spatie/flysystem-dropbox": "Allows you to use Dropbox storage", - "srmklive/flysystem-dropbox-v2": "Allows you to use Dropbox storage for PHP 5 applications" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.1-dev" - } - }, - "autoload": { - "psr-4": { - "League\\Flysystem\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Frank de Jonge", - "email": "info@frenky.net" - } - ], - "description": "Filesystem abstraction: Many filesystems, one API.", - "keywords": [ - "Cloud Files", - "WebDAV", - "abstraction", - "aws", - "cloud", - "copy.com", - "dropbox", - "file systems", - "files", - "filesystem", - "filesystems", - "ftp", - "rackspace", - "remote", - "s3", - "sftp", - "storage" - ], - "time": "2017-08-06T17:41:04+00:00" - }, - { - "name": "monolog/monolog", - "version": "1.23.0", - "source": { - "type": "git", - "url": "https://github.com/Seldaek/monolog.git", - "reference": "fd8c787753b3a2ad11bc60c063cff1358a32a3b4" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/Seldaek/monolog/zipball/fd8c787753b3a2ad11bc60c063cff1358a32a3b4", - "reference": "fd8c787753b3a2ad11bc60c063cff1358a32a3b4", - "shasum": "" - }, - "require": { - "php": ">=5.3.0", - "psr/log": "~1.0" - }, - "provide": { - "psr/log-implementation": "1.0.0" - }, - "require-dev": { - "aws/aws-sdk-php": "^2.4.9 || ^3.0", - "doctrine/couchdb": "~1.0@dev", - "graylog2/gelf-php": "~1.0", - "jakub-onderka/php-parallel-lint": "0.9", - "php-amqplib/php-amqplib": "~2.4", - "php-console/php-console": "^3.1.3", - "phpunit/phpunit": "~4.5", - "phpunit/phpunit-mock-objects": "2.3.0", - "ruflin/elastica": ">=0.90 <3.0", - "sentry/sentry": "^0.13", - "swiftmailer/swiftmailer": "^5.3|^6.0" - }, - "suggest": { - "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB", - "doctrine/couchdb": "Allow sending log messages to a CouchDB server", - "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)", - "ext-mongo": "Allow sending log messages to a MongoDB server", - "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server", - "mongodb/mongodb": "Allow sending log messages to a MongoDB server via PHP Driver", - "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib", - "php-console/php-console": "Allow sending log messages to Google Chrome", - "rollbar/rollbar": "Allow sending log messages to Rollbar", - "ruflin/elastica": "Allow sending log messages to an Elastic Search server", - "sentry/sentry": "Allow sending log messages to a Sentry server" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "Monolog\\": "src/Monolog" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Jordi Boggiano", - "email": "j.boggiano@seld.be", - "homepage": "http://seld.be" - } - ], - "description": "Sends your logs to files, sockets, inboxes, databases and various web services", - "homepage": "http://github.com/Seldaek/monolog", - "keywords": [ - "log", - "logging", - "psr-3" - ], - "time": "2017-06-19T01:22:40+00:00" - }, - { - "name": "mtdowling/cron-expression", - "version": "v1.2.0", - "source": { - "type": "git", - "url": "https://github.com/mtdowling/cron-expression.git", - "reference": "9504fa9ea681b586028adaaa0877db4aecf32bad" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/mtdowling/cron-expression/zipball/9504fa9ea681b586028adaaa0877db4aecf32bad", - "reference": "9504fa9ea681b586028adaaa0877db4aecf32bad", - "shasum": "" - }, - "require": { - "php": ">=5.3.2" - }, - "require-dev": { - "phpunit/phpunit": "~4.0|~5.0" - }, - "type": "library", - "autoload": { - "psr-4": { - "Cron\\": "src/Cron/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Michael Dowling", - "email": "mtdowling@gmail.com", - "homepage": "https://github.com/mtdowling" - } - ], - "description": "CRON for PHP: Calculate the next or previous run date and determine if a CRON expression is due", - "keywords": [ - "cron", - "schedule" - ], - "time": "2017-01-23T04:29:33+00:00" - }, - { - "name": "nesbot/carbon", - "version": "1.22.1", - "source": { - "type": "git", - "url": "https://github.com/briannesbitt/Carbon.git", - "reference": "7cdf42c0b1cc763ab7e4c33c47a24e27c66bfccc" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/7cdf42c0b1cc763ab7e4c33c47a24e27c66bfccc", - "reference": "7cdf42c0b1cc763ab7e4c33c47a24e27c66bfccc", - "shasum": "" - }, - "require": { - "php": ">=5.3.0", - "symfony/translation": "~2.6 || ~3.0" - }, - "require-dev": { - "friendsofphp/php-cs-fixer": "~2", - "phpunit/phpunit": "~4.0 || ~5.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.23-dev" - } - }, - "autoload": { - "psr-4": { - "Carbon\\": "src/Carbon/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Brian Nesbitt", - "email": "brian@nesbot.com", - "homepage": "http://nesbot.com" - } - ], - "description": "A simple API extension for DateTime.", - "homepage": "http://carbon.nesbot.com", - "keywords": [ - "date", - "datetime", - "time" - ], - "time": "2017-01-16T07:55:07+00:00" - }, - { - "name": "paragonie/random_compat", - "version": "v2.0.10", - "source": { - "type": "git", - "url": "https://github.com/paragonie/random_compat.git", - "reference": "634bae8e911eefa89c1abfbf1b66da679ac8f54d" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/paragonie/random_compat/zipball/634bae8e911eefa89c1abfbf1b66da679ac8f54d", - "reference": "634bae8e911eefa89c1abfbf1b66da679ac8f54d", - "shasum": "" - }, - "require": { - "php": ">=5.2.0" - }, - "require-dev": { - "phpunit/phpunit": "4.*|5.*" - }, - "suggest": { - "ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes." - }, - "type": "library", - "autoload": { - "files": [ - "lib/random.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Paragon Initiative Enterprises", - "email": "security@paragonie.com", - "homepage": "https://paragonie.com" - } - ], - "description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7", - "keywords": [ - "csprng", - "pseudorandom", - "random" - ], - "time": "2017-03-13T16:27:32+00:00" - }, - { - "name": "psr/http-message", - "version": "1.0.1", - "source": { - "type": "git", - "url": "https://github.com/php-fig/http-message.git", - "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363", - "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363", - "shasum": "" - }, - "require": { - "php": ">=5.3.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psr\\Http\\Message\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" - } - ], - "description": "Common interface for HTTP messages", - "homepage": "https://github.com/php-fig/http-message", - "keywords": [ - "http", - "http-message", - "psr", - "psr-7", - "request", - "response" - ], - "time": "2016-08-06T14:39:51+00:00" - }, - { - "name": "psr/log", - "version": "1.0.2", - "source": { - "type": "git", - "url": "https://github.com/php-fig/log.git", - "reference": "4ebe3a8bf773a19edfe0a84b6585ba3d401b724d" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/log/zipball/4ebe3a8bf773a19edfe0a84b6585ba3d401b724d", - "reference": "4ebe3a8bf773a19edfe0a84b6585ba3d401b724d", - "shasum": "" - }, - "require": { - "php": ">=5.3.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psr\\Log\\": "Psr/Log/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" - } - ], - "description": "Common interface for logging libraries", - "homepage": "https://github.com/php-fig/log", - "keywords": [ - "log", - "psr", - "psr-3" - ], - "time": "2016-10-10T12:19:37+00:00" - }, - { - "name": "ramsey/uuid", - "version": "3.7.0", - "source": { - "type": "git", - "url": "https://github.com/ramsey/uuid.git", - "reference": "0ef23d1b10cf1bc576e9d865a7e9c47982c5715e" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/ramsey/uuid/zipball/0ef23d1b10cf1bc576e9d865a7e9c47982c5715e", - "reference": "0ef23d1b10cf1bc576e9d865a7e9c47982c5715e", - "shasum": "" - }, - "require": { - "paragonie/random_compat": "^1.0|^2.0", - "php": "^5.4 || ^7.0" - }, - "replace": { - "rhumsaa/uuid": "self.version" - }, - "require-dev": { - "apigen/apigen": "^4.1", - "codeception/aspect-mock": "^1.0 | ^2.0", - "doctrine/annotations": "~1.2.0", - "goaop/framework": "1.0.0-alpha.2 | ^1.0 | ^2.1", - "ircmaxell/random-lib": "^1.1", - "jakub-onderka/php-parallel-lint": "^0.9.0", - "mockery/mockery": "^0.9.4", - "moontoast/math": "^1.1", - "php-mock/php-mock-phpunit": "^0.3|^1.1", - "phpunit/phpunit": "^4.7|>=5.0 <5.4", - "satooshi/php-coveralls": "^0.6.1", - "squizlabs/php_codesniffer": "^2.3" - }, - "suggest": { - "ext-libsodium": "Provides the PECL libsodium extension for use with the SodiumRandomGenerator", - "ext-uuid": "Provides the PECL UUID extension for use with the PeclUuidTimeGenerator and PeclUuidRandomGenerator", - "ircmaxell/random-lib": "Provides RandomLib for use with the RandomLibAdapter", - "moontoast/math": "Provides support for converting UUID to 128-bit integer (in string form).", - "ramsey/uuid-console": "A console application for generating UUIDs with ramsey/uuid", - "ramsey/uuid-doctrine": "Allows the use of Ramsey\\Uuid\\Uuid as Doctrine field type." - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.x-dev" - } - }, - "autoload": { - "psr-4": { - "Ramsey\\Uuid\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Marijn Huizendveld", - "email": "marijn.huizendveld@gmail.com" - }, - { - "name": "Thibaud Fabre", - "email": "thibaud@aztech.io" - }, - { - "name": "Ben Ramsey", - "email": "ben@benramsey.com", - "homepage": "https://benramsey.com" - } - ], - "description": "Formerly rhumsaa/uuid. A PHP 5.4+ library for generating RFC 4122 version 1, 3, 4, and 5 universally unique identifiers (UUID).", - "homepage": "https://github.com/ramsey/uuid", - "keywords": [ - "guid", - "identifier", - "uuid" - ], - "time": "2017-08-04T13:39:04+00:00" - }, - { - "name": "swiftmailer/swiftmailer", - "version": "v5.4.8", - "source": { - "type": "git", - "url": "https://github.com/swiftmailer/swiftmailer.git", - "reference": "9a06dc570a0367850280eefd3f1dc2da45aef517" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/swiftmailer/swiftmailer/zipball/9a06dc570a0367850280eefd3f1dc2da45aef517", - "reference": "9a06dc570a0367850280eefd3f1dc2da45aef517", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "require-dev": { - "mockery/mockery": "~0.9.1", - "symfony/phpunit-bridge": "~3.2" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.4-dev" - } - }, - "autoload": { - "files": [ - "lib/swift_required.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Chris Corbyn" - }, - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - } - ], - "description": "Swiftmailer, free feature-rich PHP mailer", - "homepage": "http://swiftmailer.org", - "keywords": [ - "email", - "mail", - "mailer" - ], - "time": "2017-05-01T15:54:03+00:00" - }, - { - "name": "symfony/console", - "version": "v3.3.9", - "source": { - "type": "git", - "url": "https://github.com/symfony/console.git", - "reference": "a1e1b01293a090cb9ae2ddd221a3251a4a7e4abf" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/a1e1b01293a090cb9ae2ddd221a3251a4a7e4abf", - "reference": "a1e1b01293a090cb9ae2ddd221a3251a4a7e4abf", - "shasum": "" - }, - "require": { - "php": "^5.5.9|>=7.0.8", - "symfony/debug": "~2.8|~3.0", - "symfony/polyfill-mbstring": "~1.0" - }, - "conflict": { - "symfony/dependency-injection": "<3.3" - }, - "require-dev": { - "psr/log": "~1.0", - "symfony/config": "~3.3", - "symfony/dependency-injection": "~3.3", - "symfony/event-dispatcher": "~2.8|~3.0", - "symfony/filesystem": "~2.8|~3.0", - "symfony/process": "~2.8|~3.0" - }, - "suggest": { - "psr/log": "For using the console logger", - "symfony/event-dispatcher": "", - "symfony/filesystem": "", - "symfony/process": "" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.3-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Component\\Console\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony Console Component", - "homepage": "https://symfony.com", - "time": "2017-09-06T16:40:18+00:00" - }, - { - "name": "symfony/css-selector", - "version": "v3.3.9", - "source": { - "type": "git", - "url": "https://github.com/symfony/css-selector.git", - "reference": "c5f5263ed231f164c58368efbce959137c7d9488" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/css-selector/zipball/c5f5263ed231f164c58368efbce959137c7d9488", - "reference": "c5f5263ed231f164c58368efbce959137c7d9488", - "shasum": "" - }, - "require": { - "php": "^5.5.9|>=7.0.8" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.3-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Component\\CssSelector\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Jean-François Simon", - "email": "jeanfrancois.simon@sensiolabs.com" - }, - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony CssSelector Component", - "homepage": "https://symfony.com", - "time": "2017-07-29T21:54:42+00:00" - }, - { - "name": "symfony/debug", - "version": "v3.3.9", - "source": { - "type": "git", - "url": "https://github.com/symfony/debug.git", - "reference": "8beb24eec70b345c313640962df933499373a944" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/debug/zipball/8beb24eec70b345c313640962df933499373a944", - "reference": "8beb24eec70b345c313640962df933499373a944", - "shasum": "" - }, - "require": { - "php": "^5.5.9|>=7.0.8", - "psr/log": "~1.0" - }, - "conflict": { - "symfony/http-kernel": ">=2.3,<2.3.24|~2.4.0|>=2.5,<2.5.9|>=2.6,<2.6.2" - }, - "require-dev": { - "symfony/http-kernel": "~2.8|~3.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.3-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Component\\Debug\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony Debug Component", - "homepage": "https://symfony.com", - "time": "2017-09-01T13:23:39+00:00" - }, - { - "name": "symfony/event-dispatcher", - "version": "v3.3.9", - "source": { - "type": "git", - "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "54ca9520a00386f83bca145819ad3b619aaa2485" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/54ca9520a00386f83bca145819ad3b619aaa2485", - "reference": "54ca9520a00386f83bca145819ad3b619aaa2485", - "shasum": "" - }, - "require": { - "php": "^5.5.9|>=7.0.8" - }, - "conflict": { - "symfony/dependency-injection": "<3.3" - }, - "require-dev": { - "psr/log": "~1.0", - "symfony/config": "~2.8|~3.0", - "symfony/dependency-injection": "~3.3", - "symfony/expression-language": "~2.8|~3.0", - "symfony/stopwatch": "~2.8|~3.0" - }, - "suggest": { - "symfony/dependency-injection": "", - "symfony/http-kernel": "" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.3-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Component\\EventDispatcher\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony EventDispatcher Component", - "homepage": "https://symfony.com", - "time": "2017-07-29T21:54:42+00:00" - }, - { - "name": "symfony/finder", - "version": "v3.3.9", - "source": { - "type": "git", - "url": "https://github.com/symfony/finder.git", - "reference": "b2260dbc80f3c4198f903215f91a1ac7fe9fe09e" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/b2260dbc80f3c4198f903215f91a1ac7fe9fe09e", - "reference": "b2260dbc80f3c4198f903215f91a1ac7fe9fe09e", - "shasum": "" - }, - "require": { - "php": "^5.5.9|>=7.0.8" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.3-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Component\\Finder\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony Finder Component", - "homepage": "https://symfony.com", - "time": "2017-07-29T21:54:42+00:00" - }, - { - "name": "symfony/http-foundation", - "version": "v3.3.9", - "source": { - "type": "git", - "url": "https://github.com/symfony/http-foundation.git", - "reference": "2cdc7de1921d1a1c805a13dc05e44a2cd58f5ad3" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/2cdc7de1921d1a1c805a13dc05e44a2cd58f5ad3", - "reference": "2cdc7de1921d1a1c805a13dc05e44a2cd58f5ad3", - "shasum": "" - }, - "require": { - "php": "^5.5.9|>=7.0.8", - "symfony/polyfill-mbstring": "~1.1" - }, - "require-dev": { - "symfony/expression-language": "~2.8|~3.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.3-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Component\\HttpFoundation\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony HttpFoundation Component", - "homepage": "https://symfony.com", - "time": "2017-09-06T17:07:39+00:00" - }, - { - "name": "symfony/http-kernel", - "version": "v3.3.9", - "source": { - "type": "git", - "url": "https://github.com/symfony/http-kernel.git", - "reference": "70f5bb3cdd737624249953b61023411e26be5db7" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/70f5bb3cdd737624249953b61023411e26be5db7", - "reference": "70f5bb3cdd737624249953b61023411e26be5db7", - "shasum": "" - }, - "require": { - "php": "^5.5.9|>=7.0.8", - "psr/log": "~1.0", - "symfony/debug": "~2.8|~3.0", - "symfony/event-dispatcher": "~2.8|~3.0", - "symfony/http-foundation": "~3.3" - }, - "conflict": { - "symfony/config": "<2.8", - "symfony/dependency-injection": "<3.3", - "symfony/var-dumper": "<3.3", - "twig/twig": "<1.34|<2.4,>=2" - }, - "require-dev": { - "psr/cache": "~1.0", - "symfony/browser-kit": "~2.8|~3.0", - "symfony/class-loader": "~2.8|~3.0", - "symfony/config": "~2.8|~3.0", - "symfony/console": "~2.8|~3.0", - "symfony/css-selector": "~2.8|~3.0", - "symfony/dependency-injection": "~3.3", - "symfony/dom-crawler": "~2.8|~3.0", - "symfony/expression-language": "~2.8|~3.0", - "symfony/finder": "~2.8|~3.0", - "symfony/process": "~2.8|~3.0", - "symfony/routing": "~2.8|~3.0", - "symfony/stopwatch": "~2.8|~3.0", - "symfony/templating": "~2.8|~3.0", - "symfony/translation": "~2.8|~3.0", - "symfony/var-dumper": "~3.3" - }, - "suggest": { - "symfony/browser-kit": "", - "symfony/class-loader": "", - "symfony/config": "", - "symfony/console": "", - "symfony/dependency-injection": "", - "symfony/finder": "", - "symfony/var-dumper": "" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.3-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Component\\HttpKernel\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony HttpKernel Component", - "homepage": "https://symfony.com", - "time": "2017-09-11T16:13:23+00:00" - }, - { - "name": "symfony/polyfill-mbstring", - "version": "v1.5.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "7c8fae0ac1d216eb54349e6a8baa57d515fe8803" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/7c8fae0ac1d216eb54349e6a8baa57d515fe8803", - "reference": "7c8fae0ac1d216eb54349e6a8baa57d515fe8803", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "suggest": { - "ext-mbstring": "For best performance" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.5-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Mbstring\\": "" - }, - "files": [ - "bootstrap.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill for the Mbstring extension", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "mbstring", - "polyfill", - "portable", - "shim" - ], - "time": "2017-06-14T15:44:48+00:00" - }, - { - "name": "symfony/process", - "version": "v3.3.9", - "source": { - "type": "git", - "url": "https://github.com/symfony/process.git", - "reference": "b7666e9b438027a1ea0e1ee813ec5042d5d7f6f0" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/b7666e9b438027a1ea0e1ee813ec5042d5d7f6f0", - "reference": "b7666e9b438027a1ea0e1ee813ec5042d5d7f6f0", - "shasum": "" - }, - "require": { - "php": "^5.5.9|>=7.0.8" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.3-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Component\\Process\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony Process Component", - "homepage": "https://symfony.com", - "time": "2017-07-29T21:54:42+00:00" - }, - { - "name": "symfony/routing", - "version": "v3.3.9", - "source": { - "type": "git", - "url": "https://github.com/symfony/routing.git", - "reference": "970326dcd04522e1cd1fe128abaee54c225e27f9" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/routing/zipball/970326dcd04522e1cd1fe128abaee54c225e27f9", - "reference": "970326dcd04522e1cd1fe128abaee54c225e27f9", - "shasum": "" - }, - "require": { - "php": "^5.5.9|>=7.0.8" - }, - "conflict": { - "symfony/config": "<2.8", - "symfony/dependency-injection": "<3.3", - "symfony/yaml": "<3.3" - }, - "require-dev": { - "doctrine/annotations": "~1.0", - "doctrine/common": "~2.2", - "psr/log": "~1.0", - "symfony/config": "~2.8|~3.0", - "symfony/dependency-injection": "~3.3", - "symfony/expression-language": "~2.8|~3.0", - "symfony/http-foundation": "~2.8|~3.0", - "symfony/yaml": "~3.3" - }, - "suggest": { - "doctrine/annotations": "For using the annotation loader", - "symfony/config": "For using the all-in-one router or any loader", - "symfony/dependency-injection": "For loading routes from a service", - "symfony/expression-language": "For using expression matching", - "symfony/http-foundation": "For using a Symfony Request object", - "symfony/yaml": "For using the YAML loader" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.3-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Component\\Routing\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony Routing Component", - "homepage": "https://symfony.com", - "keywords": [ - "router", - "routing", - "uri", - "url" - ], - "time": "2017-07-29T21:54:42+00:00" - }, - { - "name": "symfony/translation", - "version": "v3.3.9", - "source": { - "type": "git", - "url": "https://github.com/symfony/translation.git", - "reference": "add53753d978f635492dfe8cd6953f6a7361ef90" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/add53753d978f635492dfe8cd6953f6a7361ef90", - "reference": "add53753d978f635492dfe8cd6953f6a7361ef90", - "shasum": "" - }, - "require": { - "php": "^5.5.9|>=7.0.8", - "symfony/polyfill-mbstring": "~1.0" - }, - "conflict": { - "symfony/config": "<2.8", - "symfony/yaml": "<3.3" - }, - "require-dev": { - "psr/log": "~1.0", - "symfony/config": "~2.8|~3.0", - "symfony/intl": "^2.8.18|^3.2.5", - "symfony/yaml": "~3.3" - }, - "suggest": { - "psr/log": "To use logging capability in translator", - "symfony/config": "", - "symfony/yaml": "" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.3-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Component\\Translation\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony Translation Component", - "homepage": "https://symfony.com", - "time": "2017-07-29T21:54:42+00:00" - }, - { - "name": "symfony/var-dumper", - "version": "v3.3.9", - "source": { - "type": "git", - "url": "https://github.com/symfony/var-dumper.git", - "reference": "89fcb5a73e0ede2be2512234c4e40457bb22b35f" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/89fcb5a73e0ede2be2512234c4e40457bb22b35f", - "reference": "89fcb5a73e0ede2be2512234c4e40457bb22b35f", - "shasum": "" - }, - "require": { - "php": "^5.5.9|>=7.0.8", - "symfony/polyfill-mbstring": "~1.0" - }, - "conflict": { - "phpunit/phpunit": "<4.8.35|<5.4.3,>=5.0" - }, - "require-dev": { - "ext-iconv": "*", - "twig/twig": "~1.34|~2.4" - }, - "suggest": { - "ext-iconv": "To convert non-UTF-8 strings to UTF-8 (or symfony/polyfill-iconv in case ext-iconv cannot be used).", - "ext-symfony_debug": "" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.3-dev" - } - }, - "autoload": { - "files": [ - "Resources/functions/dump.php" - ], - "psr-4": { - "Symfony\\Component\\VarDumper\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony mechanism for exploring and dumping PHP variables", - "homepage": "https://symfony.com", - "keywords": [ - "debug", - "dump" - ], - "time": "2017-08-27T14:52:21+00:00" - }, - { - "name": "tijsverkoyen/css-to-inline-styles", - "version": "2.2.0", - "source": { - "type": "git", - "url": "https://github.com/tijsverkoyen/CssToInlineStyles.git", - "reference": "ab03919dfd85a74ae0372f8baf9f3c7d5c03b04b" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/tijsverkoyen/CssToInlineStyles/zipball/ab03919dfd85a74ae0372f8baf9f3c7d5c03b04b", - "reference": "ab03919dfd85a74ae0372f8baf9f3c7d5c03b04b", - "shasum": "" - }, - "require": { - "php": "^5.5 || ^7", - "symfony/css-selector": "^2.7|~3.0" - }, - "require-dev": { - "phpunit/phpunit": "~4.8|5.1.*" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "TijsVerkoyen\\CssToInlineStyles\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Tijs Verkoyen", - "email": "css_to_inline_styles@verkoyen.eu", - "role": "Developer" - } - ], - "description": "CssToInlineStyles is a class that enables you to convert HTML-pages/files into HTML-pages/files with inline styles. This is very useful when you're sending emails.", - "homepage": "https://github.com/tijsverkoyen/CssToInlineStyles", - "time": "2016-09-20T12:50:39+00:00" - }, - { - "name": "vlucas/phpdotenv", - "version": "v2.4.0", - "source": { - "type": "git", - "url": "https://github.com/vlucas/phpdotenv.git", - "reference": "3cc116adbe4b11be5ec557bf1d24dc5e3a21d18c" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/3cc116adbe4b11be5ec557bf1d24dc5e3a21d18c", - "reference": "3cc116adbe4b11be5ec557bf1d24dc5e3a21d18c", - "shasum": "" - }, - "require": { - "php": ">=5.3.9" - }, - "require-dev": { - "phpunit/phpunit": "^4.8 || ^5.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.4-dev" - } - }, - "autoload": { - "psr-4": { - "Dotenv\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause-Attribution" - ], - "authors": [ - { - "name": "Vance Lucas", - "email": "vance@vancelucas.com", - "homepage": "http://www.vancelucas.com" - } - ], - "description": "Loads environment variables from `.env` to `getenv()`, `$_ENV` and `$_SERVER` automagically.", - "keywords": [ - "dotenv", - "env", - "environment" - ], - "time": "2016-09-01T10:05:43+00:00" - } - ], - "packages-dev": [ - { - "name": "doctrine/instantiator", - "version": "1.1.0", - "source": { - "type": "git", - "url": "https://github.com/doctrine/instantiator.git", - "reference": "185b8868aa9bf7159f5f953ed5afb2d7fcdc3bda" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/doctrine/instantiator/zipball/185b8868aa9bf7159f5f953ed5afb2d7fcdc3bda", - "reference": "185b8868aa9bf7159f5f953ed5afb2d7fcdc3bda", - "shasum": "" - }, - "require": { - "php": "^7.1" - }, - "require-dev": { - "athletic/athletic": "~0.1.8", - "ext-pdo": "*", - "ext-phar": "*", - "phpunit/phpunit": "^6.2.3", - "squizlabs/php_codesniffer": "^3.0.2" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.2.x-dev" - } - }, - "autoload": { - "psr-4": { - "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Marco Pivetta", - "email": "ocramius@gmail.com", - "homepage": "http://ocramius.github.com/" - } - ], - "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", - "homepage": "https://github.com/doctrine/instantiator", - "keywords": [ - "constructor", - "instantiate" - ], - "time": "2017-07-22T11:58:36+00:00" - }, - { - "name": "fzaninotto/faker", - "version": "v1.7.1", - "source": { - "type": "git", - "url": "https://github.com/fzaninotto/Faker.git", - "reference": "d3ed4cc37051c1ca52d22d76b437d14809fc7e0d" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/fzaninotto/Faker/zipball/d3ed4cc37051c1ca52d22d76b437d14809fc7e0d", - "reference": "d3ed4cc37051c1ca52d22d76b437d14809fc7e0d", - "shasum": "" - }, - "require": { - "php": "^5.3.3 || ^7.0" - }, - "require-dev": { - "ext-intl": "*", - "phpunit/phpunit": "^4.0 || ^5.0", - "squizlabs/php_codesniffer": "^1.5" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.8-dev" - } - }, - "autoload": { - "psr-4": { - "Faker\\": "src/Faker/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "François Zaninotto" - } - ], - "description": "Faker is a PHP library that generates fake data for you.", - "keywords": [ - "data", - "faker", - "fixtures" - ], - "time": "2017-08-15T16:48:10+00:00" - }, - { - "name": "myclabs/deep-copy", - "version": "1.6.1", - "source": { - "type": "git", - "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "8e6e04167378abf1ddb4d3522d8755c5fd90d102" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/8e6e04167378abf1ddb4d3522d8755c5fd90d102", - "reference": "8e6e04167378abf1ddb4d3522d8755c5fd90d102", - "shasum": "" - }, - "require": { - "php": ">=5.4.0" - }, - "require-dev": { - "doctrine/collections": "1.*", - "phpunit/phpunit": "~4.1" - }, - "type": "library", - "autoload": { - "psr-4": { - "DeepCopy\\": "src/DeepCopy/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "Create deep copies (clones) of your objects", - "homepage": "https://github.com/myclabs/DeepCopy", - "keywords": [ - "clone", - "copy", - "duplicate", - "object", - "object graph" - ], - "time": "2017-04-12T18:52:22+00:00" - }, - { - "name": "orchestra/testbench", - "version": "v3.4.8", - "source": { - "type": "git", - "url": "https://github.com/orchestral/testbench.git", - "reference": "954b15e551e53f0c6d3ce02dfab6b890288e5164" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/orchestral/testbench/zipball/954b15e551e53f0c6d3ce02dfab6b890288e5164", - "reference": "954b15e551e53f0c6d3ce02dfab6b890288e5164", - "shasum": "" - }, - "require": { - "fzaninotto/faker": "~1.4", - "laravel/framework": "~5.4.17", - "orchestra/testbench-core": "~3.4.0", - "php": ">=5.6.0" - }, - "require-dev": { - "mockery/mockery": "^0.9.4", - "orchestra/database": "~3.4.0", - "phpunit/phpunit": "~5.7" - }, - "suggest": { - "mockery/mockery": "Allow to use Mockery for testing (^0.9.4).", - "orchestra/database": "Allow to use --realpath migration for testing (~3.4).", - "orchestra/testbench-browser-kit": "Allow to use legacy BrowserKit for testing (~3.4).", - "phpunit/phpunit": "Allow to use PHPUnit for testing (~5.7)." - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.4-dev" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Mior Muhammad Zaki", - "email": "crynobone@gmail.com", - "homepage": "https://github.com/crynobone" - } - ], - "description": "Laravel Testing Helper for Packages Development", - "homepage": "http://orchestraplatform.com/docs/latest/components/testbench/", - "keywords": [ - "BDD", - "TDD", - "laravel", - "orchestra-platform", - "orchestral", - "testing" - ], - "time": "2017-07-03T15:48:06+00:00" - }, - { - "name": "orchestra/testbench-core", - "version": "v3.4.1", - "source": { - "type": "git", - "url": "https://github.com/orchestral/testbench-core.git", - "reference": "2a5bafeaac992c4c89da371f5bdd76cb99abb837" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/orchestral/testbench-core/zipball/2a5bafeaac992c4c89da371f5bdd76cb99abb837", - "reference": "2a5bafeaac992c4c89da371f5bdd76cb99abb837", - "shasum": "" - }, - "require": { - "fzaninotto/faker": "~1.4", - "php": ">=5.6.0" - }, - "require-dev": { - "laravel/framework": "~5.4.17", - "mockery/mockery": "^0.9.4", - "orchestra/database": "~3.4.0", - "phpunit/phpunit": "~5.7 || ~6.0" - }, - "suggest": { - "laravel/framework": "Required for testing (~5.4.0).", - "mockery/mockery": "Allow to use Mockery for testing (^0.9.4).", - "orchestra/database": "Allow to use --realpath migration for testing (~3.4).", - "orchestra/testbench-browser-kit": "Allow to use legacy BrowserKit for testing (~3.4).", - "phpunit/phpunit": "Allow to use PHPUnit for testing (~6.0)." - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.5-dev" - } - }, - "autoload": { - "psr-4": { - "Orchestra\\Testbench\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Mior Muhammad Zaki", - "email": "crynobone@gmail.com", - "homepage": "https://github.com/crynobone" - } - ], - "description": "Testing Helper for Laravel Development", - "homepage": "http://orchestraplatform.com/docs/latest/components/testbench/", - "keywords": [ - "BDD", - "TDD", - "laravel", - "orchestra-platform", - "orchestral", - "testing" - ], - "time": "2017-08-19T03:07:38+00:00" - }, - { - "name": "phpdocumentor/reflection-common", - "version": "1.0.1", - "source": { - "type": "git", - "url": "https://github.com/phpDocumentor/ReflectionCommon.git", - "reference": "21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6", - "reference": "21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6", - "shasum": "" - }, - "require": { - "php": ">=5.5" - }, - "require-dev": { - "phpunit/phpunit": "^4.6" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "phpDocumentor\\Reflection\\": [ - "src" - ] - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Jaap van Otterdijk", - "email": "opensource@ijaap.nl" - } - ], - "description": "Common reflection classes used by phpdocumentor to reflect the code structure", - "homepage": "http://www.phpdoc.org", - "keywords": [ - "FQSEN", - "phpDocumentor", - "phpdoc", - "reflection", - "static analysis" - ], - "time": "2017-09-11T18:02:19+00:00" - }, - { - "name": "phpdocumentor/reflection-docblock", - "version": "4.1.1", - "source": { - "type": "git", - "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "2d3d238c433cf69caeb4842e97a3223a116f94b2" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/2d3d238c433cf69caeb4842e97a3223a116f94b2", - "reference": "2d3d238c433cf69caeb4842e97a3223a116f94b2", - "shasum": "" - }, - "require": { - "php": "^7.0", - "phpdocumentor/reflection-common": "^1.0@dev", - "phpdocumentor/type-resolver": "^0.4.0", - "webmozart/assert": "^1.0" - }, - "require-dev": { - "mockery/mockery": "^0.9.4", - "phpunit/phpunit": "^4.4" - }, - "type": "library", - "autoload": { - "psr-4": { - "phpDocumentor\\Reflection\\": [ - "src/" - ] - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Mike van Riel", - "email": "me@mikevanriel.com" - } - ], - "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", - "time": "2017-08-30T18:51:59+00:00" - }, - { - "name": "phpdocumentor/type-resolver", - "version": "0.4.0", - "source": { - "type": "git", - "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "9c977708995954784726e25d0cd1dddf4e65b0f7" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/9c977708995954784726e25d0cd1dddf4e65b0f7", - "reference": "9c977708995954784726e25d0cd1dddf4e65b0f7", - "shasum": "" - }, - "require": { - "php": "^5.5 || ^7.0", - "phpdocumentor/reflection-common": "^1.0" - }, - "require-dev": { - "mockery/mockery": "^0.9.4", - "phpunit/phpunit": "^5.2||^4.8.24" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "phpDocumentor\\Reflection\\": [ - "src/" - ] - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Mike van Riel", - "email": "me@mikevanriel.com" - } - ], - "time": "2017-07-14T14:27:02+00:00" - }, - { - "name": "phpspec/prophecy", - "version": "v1.7.2", - "source": { - "type": "git", - "url": "https://github.com/phpspec/prophecy.git", - "reference": "c9b8c6088acd19d769d4cc0ffa60a9fe34344bd6" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpspec/prophecy/zipball/c9b8c6088acd19d769d4cc0ffa60a9fe34344bd6", - "reference": "c9b8c6088acd19d769d4cc0ffa60a9fe34344bd6", - "shasum": "" - }, - "require": { - "doctrine/instantiator": "^1.0.2", - "php": "^5.3|^7.0", - "phpdocumentor/reflection-docblock": "^2.0|^3.0.2|^4.0", - "sebastian/comparator": "^1.1|^2.0", - "sebastian/recursion-context": "^1.0|^2.0|^3.0" - }, - "require-dev": { - "phpspec/phpspec": "^2.5|^3.2", - "phpunit/phpunit": "^4.8 || ^5.6.5" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.7.x-dev" - } - }, - "autoload": { - "psr-0": { - "Prophecy\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Konstantin Kudryashov", - "email": "ever.zet@gmail.com", - "homepage": "http://everzet.com" - }, - { - "name": "Marcello Duarte", - "email": "marcello.duarte@gmail.com" - } - ], - "description": "Highly opinionated mocking framework for PHP 5.3+", - "homepage": "https://github.com/phpspec/prophecy", - "keywords": [ - "Double", - "Dummy", - "fake", - "mock", - "spy", - "stub" - ], - "time": "2017-09-04T11:05:03+00:00" - }, - { - "name": "phpunit/php-code-coverage", - "version": "4.0.8", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "ef7b2f56815df854e66ceaee8ebe9393ae36a40d" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/ef7b2f56815df854e66ceaee8ebe9393ae36a40d", - "reference": "ef7b2f56815df854e66ceaee8ebe9393ae36a40d", - "shasum": "" - }, - "require": { - "ext-dom": "*", - "ext-xmlwriter": "*", - "php": "^5.6 || ^7.0", - "phpunit/php-file-iterator": "^1.3", - "phpunit/php-text-template": "^1.2", - "phpunit/php-token-stream": "^1.4.2 || ^2.0", - "sebastian/code-unit-reverse-lookup": "^1.0", - "sebastian/environment": "^1.3.2 || ^2.0", - "sebastian/version": "^1.0 || ^2.0" - }, - "require-dev": { - "ext-xdebug": "^2.1.4", - "phpunit/phpunit": "^5.7" - }, - "suggest": { - "ext-xdebug": "^2.5.1" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.0.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sb@sebastian-bergmann.de", - "role": "lead" - } - ], - "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", - "homepage": "https://github.com/sebastianbergmann/php-code-coverage", - "keywords": [ - "coverage", - "testing", - "xunit" - ], - "time": "2017-04-02T07:44:40+00:00" - }, - { - "name": "phpunit/php-file-iterator", - "version": "1.4.2", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "3cc8f69b3028d0f96a9078e6295d86e9bf019be5" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/3cc8f69b3028d0f96a9078e6295d86e9bf019be5", - "reference": "3cc8f69b3028d0f96a9078e6295d86e9bf019be5", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.4.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sb@sebastian-bergmann.de", - "role": "lead" - } - ], - "description": "FilterIterator implementation that filters files based on a list of suffixes.", - "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", - "keywords": [ - "filesystem", - "iterator" - ], - "time": "2016-10-03T07:40:28+00:00" - }, - { - "name": "phpunit/php-text-template", - "version": "1.2.1", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-text-template.git", - "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/31f8b717e51d9a2afca6c9f046f5d69fc27c8686", - "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "type": "library", - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Simple template engine.", - "homepage": "https://github.com/sebastianbergmann/php-text-template/", - "keywords": [ - "template" - ], - "time": "2015-06-21T13:50:34+00:00" - }, - { - "name": "phpunit/php-timer", - "version": "1.0.9", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-timer.git", - "reference": "3dcf38ca72b158baf0bc245e9184d3fdffa9c46f" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/3dcf38ca72b158baf0bc245e9184d3fdffa9c46f", - "reference": "3dcf38ca72b158baf0bc245e9184d3fdffa9c46f", - "shasum": "" - }, - "require": { - "php": "^5.3.3 || ^7.0" - }, - "require-dev": { - "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sb@sebastian-bergmann.de", - "role": "lead" - } - ], - "description": "Utility class for timing", - "homepage": "https://github.com/sebastianbergmann/php-timer/", - "keywords": [ - "timer" - ], - "time": "2017-02-26T11:10:40+00:00" - }, - { - "name": "phpunit/php-token-stream", - "version": "2.0.1", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-token-stream.git", - "reference": "9a02332089ac48e704c70f6cefed30c224e3c0b0" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/9a02332089ac48e704c70f6cefed30c224e3c0b0", - "reference": "9a02332089ac48e704c70f6cefed30c224e3c0b0", - "shasum": "" - }, - "require": { - "ext-tokenizer": "*", - "php": "^7.0" - }, - "require-dev": { - "phpunit/phpunit": "^6.2.4" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Wrapper around PHP's tokenizer extension.", - "homepage": "https://github.com/sebastianbergmann/php-token-stream/", - "keywords": [ - "tokenizer" - ], - "time": "2017-08-20T05:47:52+00:00" - }, - { - "name": "phpunit/phpunit", - "version": "5.7.21", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "3b91adfb64264ddec5a2dee9851f354aa66327db" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/3b91adfb64264ddec5a2dee9851f354aa66327db", - "reference": "3b91adfb64264ddec5a2dee9851f354aa66327db", - "shasum": "" - }, - "require": { - "ext-dom": "*", - "ext-json": "*", - "ext-libxml": "*", - "ext-mbstring": "*", - "ext-xml": "*", - "myclabs/deep-copy": "~1.3", - "php": "^5.6 || ^7.0", - "phpspec/prophecy": "^1.6.2", - "phpunit/php-code-coverage": "^4.0.4", - "phpunit/php-file-iterator": "~1.4", - "phpunit/php-text-template": "~1.2", - "phpunit/php-timer": "^1.0.6", - "phpunit/phpunit-mock-objects": "^3.2", - "sebastian/comparator": "^1.2.4", - "sebastian/diff": "^1.4.3", - "sebastian/environment": "^1.3.4 || ^2.0", - "sebastian/exporter": "~2.0", - "sebastian/global-state": "^1.1", - "sebastian/object-enumerator": "~2.0", - "sebastian/resource-operations": "~1.0", - "sebastian/version": "~1.0.3|~2.0", - "symfony/yaml": "~2.1|~3.0" - }, - "conflict": { - "phpdocumentor/reflection-docblock": "3.0.2" - }, - "require-dev": { - "ext-pdo": "*" - }, - "suggest": { - "ext-xdebug": "*", - "phpunit/php-invoker": "~1.1" - }, - "bin": [ - "phpunit" - ], - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.7.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "The PHP Unit Testing framework.", - "homepage": "https://phpunit.de/", - "keywords": [ - "phpunit", - "testing", - "xunit" - ], - "time": "2017-06-21T08:11:54+00:00" - }, - { - "name": "phpunit/phpunit-mock-objects", - "version": "3.4.4", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git", - "reference": "a23b761686d50a560cc56233b9ecf49597cc9118" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/a23b761686d50a560cc56233b9ecf49597cc9118", - "reference": "a23b761686d50a560cc56233b9ecf49597cc9118", - "shasum": "" - }, - "require": { - "doctrine/instantiator": "^1.0.2", - "php": "^5.6 || ^7.0", - "phpunit/php-text-template": "^1.2", - "sebastian/exporter": "^1.2 || ^2.0" - }, - "conflict": { - "phpunit/phpunit": "<5.4.0" - }, - "require-dev": { - "phpunit/phpunit": "^5.4" - }, - "suggest": { - "ext-soap": "*" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.2.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sb@sebastian-bergmann.de", - "role": "lead" - } - ], - "description": "Mock Object library for PHPUnit", - "homepage": "https://github.com/sebastianbergmann/phpunit-mock-objects/", - "keywords": [ - "mock", - "xunit" - ], - "time": "2017-06-30T09:13:00+00:00" - }, - { - "name": "sebastian/code-unit-reverse-lookup", - "version": "1.0.1", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", - "reference": "4419fcdb5eabb9caa61a27c7a1db532a6b55dd18" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/4419fcdb5eabb9caa61a27c7a1db532a6b55dd18", - "reference": "4419fcdb5eabb9caa61a27c7a1db532a6b55dd18", - "shasum": "" - }, - "require": { - "php": "^5.6 || ^7.0" - }, - "require-dev": { - "phpunit/phpunit": "^5.7 || ^6.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Looks up which function or method a line of code belongs to", - "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", - "time": "2017-03-04T06:30:41+00:00" - }, - { - "name": "sebastian/comparator", - "version": "1.2.4", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "2b7424b55f5047b47ac6e5ccb20b2aea4011d9be" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/2b7424b55f5047b47ac6e5ccb20b2aea4011d9be", - "reference": "2b7424b55f5047b47ac6e5ccb20b2aea4011d9be", - "shasum": "" - }, - "require": { - "php": ">=5.3.3", - "sebastian/diff": "~1.2", - "sebastian/exporter": "~1.2 || ~2.0" - }, - "require-dev": { - "phpunit/phpunit": "~4.4" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.2.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Jeff Welch", - "email": "whatthejeff@gmail.com" - }, - { - "name": "Volker Dusch", - "email": "github@wallbash.com" - }, - { - "name": "Bernhard Schussek", - "email": "bschussek@2bepublished.at" - }, - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Provides the functionality to compare PHP values for equality", - "homepage": "http://www.github.com/sebastianbergmann/comparator", - "keywords": [ - "comparator", - "compare", - "equality" - ], - "time": "2017-01-29T09:50:25+00:00" - }, - { - "name": "sebastian/diff", - "version": "1.4.3", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "7f066a26a962dbe58ddea9f72a4e82874a3975a4" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/7f066a26a962dbe58ddea9f72a4e82874a3975a4", - "reference": "7f066a26a962dbe58ddea9f72a4e82874a3975a4", - "shasum": "" - }, - "require": { - "php": "^5.3.3 || ^7.0" - }, - "require-dev": { - "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.4-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Kore Nordmann", - "email": "mail@kore-nordmann.de" - }, - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Diff implementation", - "homepage": "https://github.com/sebastianbergmann/diff", - "keywords": [ - "diff" - ], - "time": "2017-05-22T07:24:03+00:00" - }, - { - "name": "sebastian/environment", - "version": "2.0.0", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "5795ffe5dc5b02460c3e34222fee8cbe245d8fac" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/5795ffe5dc5b02460c3e34222fee8cbe245d8fac", - "reference": "5795ffe5dc5b02460c3e34222fee8cbe245d8fac", - "shasum": "" - }, - "require": { - "php": "^5.6 || ^7.0" - }, - "require-dev": { - "phpunit/phpunit": "^5.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Provides functionality to handle HHVM/PHP environments", - "homepage": "http://www.github.com/sebastianbergmann/environment", - "keywords": [ - "Xdebug", - "environment", - "hhvm" - ], - "time": "2016-11-26T07:53:53+00:00" - }, - { - "name": "sebastian/exporter", - "version": "2.0.0", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "ce474bdd1a34744d7ac5d6aad3a46d48d9bac4c4" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/ce474bdd1a34744d7ac5d6aad3a46d48d9bac4c4", - "reference": "ce474bdd1a34744d7ac5d6aad3a46d48d9bac4c4", - "shasum": "" - }, - "require": { - "php": ">=5.3.3", - "sebastian/recursion-context": "~2.0" - }, - "require-dev": { - "ext-mbstring": "*", - "phpunit/phpunit": "~4.4" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Jeff Welch", - "email": "whatthejeff@gmail.com" - }, - { - "name": "Volker Dusch", - "email": "github@wallbash.com" - }, - { - "name": "Bernhard Schussek", - "email": "bschussek@2bepublished.at" - }, - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - }, - { - "name": "Adam Harvey", - "email": "aharvey@php.net" - } - ], - "description": "Provides the functionality to export PHP variables for visualization", - "homepage": "http://www.github.com/sebastianbergmann/exporter", - "keywords": [ - "export", - "exporter" - ], - "time": "2016-11-19T08:54:04+00:00" - }, - { - "name": "sebastian/global-state", - "version": "1.1.1", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "bc37d50fea7d017d3d340f230811c9f1d7280af4" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bc37d50fea7d017d3d340f230811c9f1d7280af4", - "reference": "bc37d50fea7d017d3d340f230811c9f1d7280af4", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "require-dev": { - "phpunit/phpunit": "~4.2" - }, - "suggest": { - "ext-uopz": "*" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Snapshotting of global state", - "homepage": "http://www.github.com/sebastianbergmann/global-state", - "keywords": [ - "global state" - ], - "time": "2015-10-12T03:26:01+00:00" - }, - { - "name": "sebastian/object-enumerator", - "version": "2.0.1", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/object-enumerator.git", - "reference": "1311872ac850040a79c3c058bea3e22d0f09cbb7" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/1311872ac850040a79c3c058bea3e22d0f09cbb7", - "reference": "1311872ac850040a79c3c058bea3e22d0f09cbb7", - "shasum": "" - }, - "require": { - "php": ">=5.6", - "sebastian/recursion-context": "~2.0" - }, - "require-dev": { - "phpunit/phpunit": "~5" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Traverses array structures and object graphs to enumerate all referenced objects", - "homepage": "https://github.com/sebastianbergmann/object-enumerator/", - "time": "2017-02-18T15:18:39+00:00" - }, - { - "name": "sebastian/recursion-context", - "version": "2.0.0", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "2c3ba150cbec723aa057506e73a8d33bdb286c9a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/2c3ba150cbec723aa057506e73a8d33bdb286c9a", - "reference": "2c3ba150cbec723aa057506e73a8d33bdb286c9a", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "require-dev": { - "phpunit/phpunit": "~4.4" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Jeff Welch", - "email": "whatthejeff@gmail.com" - }, - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - }, - { - "name": "Adam Harvey", - "email": "aharvey@php.net" - } - ], - "description": "Provides functionality to recursively process PHP variables", - "homepage": "http://www.github.com/sebastianbergmann/recursion-context", - "time": "2016-11-19T07:33:16+00:00" - }, - { - "name": "sebastian/resource-operations", - "version": "1.0.0", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/resource-operations.git", - "reference": "ce990bb21759f94aeafd30209e8cfcdfa8bc3f52" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/ce990bb21759f94aeafd30209e8cfcdfa8bc3f52", - "reference": "ce990bb21759f94aeafd30209e8cfcdfa8bc3f52", - "shasum": "" - }, - "require": { - "php": ">=5.6.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Provides a list of PHP built-in functions that operate on resources", - "homepage": "https://www.github.com/sebastianbergmann/resource-operations", - "time": "2015-07-28T20:34:47+00:00" - }, - { - "name": "sebastian/version", - "version": "2.0.1", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/version.git", - "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/99732be0ddb3361e16ad77b68ba41efc8e979019", - "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019", - "shasum": "" - }, - "require": { - "php": ">=5.6" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Library that helps with managing the version number of Git-hosted PHP projects", - "homepage": "https://github.com/sebastianbergmann/version", - "time": "2016-10-03T07:35:21+00:00" - }, - { - "name": "symfony/yaml", - "version": "v3.3.9", - "source": { - "type": "git", - "url": "https://github.com/symfony/yaml.git", - "reference": "1d8c2a99c80862bdc3af94c1781bf70f86bccac0" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/1d8c2a99c80862bdc3af94c1781bf70f86bccac0", - "reference": "1d8c2a99c80862bdc3af94c1781bf70f86bccac0", - "shasum": "" - }, - "require": { - "php": "^5.5.9|>=7.0.8" - }, - "require-dev": { - "symfony/console": "~2.8|~3.0" - }, - "suggest": { - "symfony/console": "For validating YAML files using the lint command" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.3-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Component\\Yaml\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony Yaml Component", - "homepage": "https://symfony.com", - "time": "2017-07-29T21:54:42+00:00" - }, - { - "name": "webmozart/assert", - "version": "1.2.0", - "source": { - "type": "git", - "url": "https://github.com/webmozart/assert.git", - "reference": "2db61e59ff05fe5126d152bd0655c9ea113e550f" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/webmozart/assert/zipball/2db61e59ff05fe5126d152bd0655c9ea113e550f", - "reference": "2db61e59ff05fe5126d152bd0655c9ea113e550f", - "shasum": "" - }, - "require": { - "php": "^5.3.3 || ^7.0" - }, - "require-dev": { - "phpunit/phpunit": "^4.6", - "sebastian/version": "^1.0.1" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.3-dev" - } - }, - "autoload": { - "psr-4": { - "Webmozart\\Assert\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Bernhard Schussek", - "email": "bschussek@gmail.com" - } - ], - "description": "Assertions to validate method input/output with nice error messages.", - "keywords": [ - "assert", - "check", - "validate" - ], - "time": "2016-11-23T20:04:58+00:00" - } - ], - "aliases": [], - "minimum-stability": "stable", - "stability-flags": [], - "prefer-stable": false, - "prefer-lowest": false, - "platform": { - "php": ">=5.6.4" - }, - "platform-dev": [] -} diff --git a/composer_backup.json b/composer_backup.json new file mode 100644 index 0000000..4d77485 --- /dev/null +++ b/composer_backup.json @@ -0,0 +1,40 @@ +{ + "name": "lezhnev74/apideveloperio-laravel", + "description": "Laravel package to track each http request and send them to apideveloper.io", + "type": "library", + "tags": [ + "http", + "performance", + "analytics", + "logging", + "request", + "response" + ], + "license": "MIT", + "authors": [ + { + "name": "Dmitriy Lezhnev", + "email": "lezhnev.work@gmail.com" + } + ], + "require": { + "php": ">=5.6", + "illuminate/support": "~5.2", + "guzzlehttp/guzzle": "~5.0|~6.0", + "ramsey/uuid": "~3.0" + }, + "require-dev": { + "phpunit/phpunit": "~5.0|~6.0", + "orchestra/testbench": "^3.2|^3.3|^3.4|^3.5|^3.6" + }, + "autoload": { + "psr-4": { + "Apideveloper\\Laravel\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Apideveloper\\Laravel\\Tests\\": "tests/" + } + } +} diff --git a/config/apideveloperio_logs.php b/config/apideveloperio_logs.php new file mode 100644 index 0000000..638edb7 --- /dev/null +++ b/config/apideveloperio_logs.php @@ -0,0 +1,44 @@ + [ + // API key to sign requests to the API + 'api_key' => 'your key goes here', + // Enable recording + 'enabled' => env('APIDEVELOPERIO_HTTPLOG_RECORDING_ENABLED', true), + // a directory to put recorded requests at until dumped to the API backend + 'tmp_storage_path' => storage_path('logs/apideveloperio/httplog'), + // Configure what data to strip from recorded requests + 'filtering' => [ + // App environment in which recording is off + 'ignore_environment' => ['testing'], + // array of allowed values: + // "request_headers", "request_body", "response_headers", "response_body", "log", "external_queries" + 'strip_data' => [], + // if you still want to see headers, but some of values should be omitted, put hte name of the header here + // it is case insensitive, for example, good idea is to strip out "Authorization" header's value + 'strip_header_values' => ['authorization'], + // strip out values of certain query string arguments + 'strip_query_string_values' => ['api_key', 'access_token'], + // skip any request which match this regular expressions + // matched against the path part of the URL, like "/api/auth/signup" + // for example '^/api/auth' + 'skip_url_matching_regexp' => [], + // Avoid logging this http methods + 'skip_http_methods' => ['OPTIONS', 'HEAD'], + ], + ], + 'textlog' => [ + // API key to sign requests to the API + 'api_key' => 'your key goes here', + // Enable recording + 'enabled' => env('APIDEVELOPERIO_TEXTLOG_RECORDING_ENABLED', true), + // a directory to put recorded text logs at until dumped to the API backend + 'tmp_storage_path' => storage_path('logs/apideveloperio/textlog'), + + // Configure what data to strip from recorded requests + 'filtering' => [ + // App environment in which recording is off + 'ignore_environment' => ['testing'], + ], + ], +]; \ No newline at end of file diff --git a/config/http_analyzer.php b/config/http_analyzer.php deleted file mode 100644 index fff389a..0000000 --- a/config/http_analyzer.php +++ /dev/null @@ -1,31 +0,0 @@ - 'your key goes here', - - // Enable recording - 'enabled' => env('APIDEVELOPERIO_RECORDING_ENABLED', true), - - // a directory to put recorded requests at until dumped to the API backend - 'tmp_storage_path' => storage_path('logs/http_analyzer'), - - // Configure what data to strip from recorded requests - 'filtering' => [ - // App environment in which recording is off - 'ignore_environment' => ['testing'], - // array of allowed values: - // "request_headers", "request_body", "response_headers", "response_body", "log", "external_queries" - 'strip_data' => [], - // if you still want to see headers, but some of values should be omitted, put hte name of the header here - // it is case insensitive, for example, good idea is to strip out "Authorization" header's value - 'strip_header_values' => ['authorization'], - // strip out values of certain query string arguments - 'strip_query_string_values' => ['api_key', 'access_token'], - // skip any request which match this regular expressions - // matched against the path part of the URL, like "/api/auth/signup" - // for example '^/api/auth' - 'skip_url_matching_regexp' => [], - // Avoid logging this http methods - 'skip_http_methods' => ['OPTIONS', 'HEAD'], - ], -]; \ No newline at end of file diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 1fa8dd2..e8c1570 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -7,8 +7,7 @@ convertNoticesToExceptions="true" convertWarningsToExceptions="true" processIsolation="false" - stopOnFailure="false" - syntaxCheck="false" + stopOnFailure="true" bootstrap="vendor/autoload.php" > diff --git a/src/Backend/File/FileDumper.php b/src/Backend/File/FileDumper.php new file mode 100644 index 0000000..b92b4af --- /dev/null +++ b/src/Backend/File/FileDumper.php @@ -0,0 +1,75 @@ + + * Date: 25/01/2018 + */ + +namespace Apideveloper\Laravel\Backend\File; + + +class FileDumper implements LogsDumper +{ + /** @var PersistingStrategy */ + private $options; + + /** + * FileDumper constructor. + * @param PersistingStrategy $options + */ + public function __construct(PersistingStrategy $options) + { + $this->options = $options; + } + + + public function dump($data_array) + { + $file_path = $this->getFileForDumping(); + file_put_contents( + $file_path, + json_encode($data_array) . ",", // comma is for latter wrapping to json array "[...]" + FILE_APPEND | LOCK_EX + ); + } + + + /** + * Make a filename for storing buffered log entries + * + * @throws \Exception + * @return string + */ + protected function getFileForDumping() + { + $tmp_path_folder = $this->options->getFolder(); + // + // Now persist data till the next data dump to the API backend + // + if (!is_dir($tmp_path_folder) && !mkdir($tmp_path_folder, 0777, true)) { + throw new \Exception("Unable to create a directory for storing buffered texts at " . $tmp_path_folder); + } + + // + // If there are too many dumped un-sent files then stop recording + // + $max_files_count = $this->options->getMaxFiles(); + $dump_files = array_filter(scandir($tmp_path_folder), function ($file) { + return strpos($file, "buffered_text_logs") !== false; + }); + if (count($dump_files) >= $max_files_count) { + throw new \Exception("Maximum count of dump files reached. Recording of text logs stopped."); + } + + // + // make a file to dump every request to + // + $file_path = $tmp_path_folder . "/" . $this->options->getFilenamePrefix(); + $max_file_size = $this->options->getMaxFileSize(); + if (file_exists($file_path) && filesize($file_path) > $max_file_size) { + // rename it and write to a fresh one + rename($file_path, $file_path . "_batch_" . date("d-m-Y_H_i_s") . "_" . str_random(8)); + } + + return $file_path; + } +} \ No newline at end of file diff --git a/src/Backend/File/LogsDumper.php b/src/Backend/File/LogsDumper.php new file mode 100644 index 0000000..b05e114 --- /dev/null +++ b/src/Backend/File/LogsDumper.php @@ -0,0 +1,12 @@ + + * Date: 25/01/2018 + */ + +namespace Apideveloper\Laravel\Backend\File; + +interface LogsDumper +{ + public function dump($data_array); +} \ No newline at end of file diff --git a/src/Backend/File/PersistingStrategy.php b/src/Backend/File/PersistingStrategy.php new file mode 100644 index 0000000..7f74ee2 --- /dev/null +++ b/src/Backend/File/PersistingStrategy.php @@ -0,0 +1,79 @@ + + * Date: 25/01/2018 + */ + +namespace Apideveloper\Laravel\Backend\File; + + +class PersistingStrategy +{ + /** @var string */ + private $folder; + /** @var int */ + private $max_files; + /** @var int */ + private $max_file_size; + /** @var string */ + private $filename_prefix; + + /** + * PersistingOptions constructor. + * @param string $folder + * @param int $max_files + * @param int $max_file_size + * @param string $filename_prefix + */ + public function __construct($folder, $max_files, $max_file_size_bytes, $filename_prefix) + { + $max_files = intval($max_files); + $max_file_size_bytes = intval($max_file_size_bytes); + + if ($max_files < 1) { + throw new \InvalidArgumentException("Max files count has wrong value: $max_files"); + } + if ($max_file_size_bytes < 1) { + throw new \InvalidArgumentException("Max file size must positive, but set as: $max_file_size_bytes"); + } + + $this->folder = $folder; + $this->max_files = $max_files; + $this->max_file_size = $max_file_size_bytes; + $this->filename_prefix = $filename_prefix; + } + + /** + * @return string + */ + public function getFolder() + { + return $this->folder; + } + + /** + * @return int + */ + public function getMaxFiles() + { + return $this->max_files; + } + + /** + * @return int + */ + public function getMaxFileSize() + { + return $this->max_file_size; + } + + /** + * @return string + */ + public function getFilenamePrefix() + { + return $this->filename_prefix; + } + + +} \ No newline at end of file diff --git a/src/Backend/LoggedRequest.php b/src/Backend/LoggedHTTPRequest.php similarity index 98% rename from src/Backend/LoggedRequest.php rename to src/Backend/LoggedHTTPRequest.php index 0d5aeb3..077ad01 100644 --- a/src/Backend/LoggedRequest.php +++ b/src/Backend/LoggedHTTPRequest.php @@ -3,7 +3,7 @@ * @author Dmitriy Lezhnev */ -namespace HttpAnalyzer\Backend; +namespace Apideveloper\Laravel\Backend; use Carbon\Carbon; use Illuminate\Http\Request; @@ -13,7 +13,7 @@ /** * This class generates data, which conform with our backend API */ -final class LoggedRequest +final class LoggedHTTPRequest { /** @var array */ @@ -26,6 +26,7 @@ public function __construct( Request $request, Response $response, $time_to_response_ms, + $session_id, $log_text = null, $external_queries = null, $filtering_config = [] @@ -38,6 +39,7 @@ public function __construct( // Append other data $this->data['ttr_ms'] = $time_to_response_ms; + $this->data['session_id'] = $session_id; // Make sure log is not longer than 30000 bytes if ($log_text && $this->dataShouldBeRecorded('log')) { $this->data['log'] = substr($log_text, 0, 30000); diff --git a/src/Laravel/ApideveloperioServiceProvider.php b/src/Laravel/ApideveloperioServiceProvider.php new file mode 100644 index 0000000..243a8d9 --- /dev/null +++ b/src/Laravel/ApideveloperioServiceProvider.php @@ -0,0 +1,169 @@ +publishes([ + __DIR__ . '/../../config/apideveloperio_logs.php' => config_path('apideveloperio_logs.php'), + ], 'config'); + + // + // HTTP Log related + // + $this->listenHTTPRelatedEvents(); + + // + // TextLog related + // + $this->listenTextLogRelatedEvents(); + } + + protected function listenHTTPRelatedEvents() + { + $event = app(Dispatcher::class); + + if (Str::startsWith(app()->version(), ['5.2', '5.3'])) { + // Here no object oriented events were available, so I have to listen to + // legacy event names + + $event->listen('kernel.handled', function ($request, $response) { + $listener = app()[HTTPEventListener::class]; + $listener->onRequestHandled($request, $response); + }); + $event->listen(QueryExecuted::class, HTTPEventListener::class . '@onDatabaseQueryExecuted'); + $event->listen('illuminate.log', function ($level, $message, $context) { + $listener = app()[HTTPEventListener::class]; + $listener->onLog($level, $message, $context); + }); + } else { + $event->listen(RequestHandled::class, function (RequestHandled $event) { + $listener = app()[HTTPEventListener::class]; + $listener->onRequestHandled($event->request, $event->response); + }); + $event->listen(QueryExecuted::class, HTTPEventListener::class . '@onDatabaseQueryExecuted'); + $event->listen(MessageLogged::class, function (MessageLogged $event) { + $listener = app()[HTTPEventListener::class]; + $listener->onLog( + $event->level, + $event->message, + $event->context + ); + }); + } + } + + protected function listenTextLogRelatedEvents() + { + $event = app(Dispatcher::class); + $event_listener = app()[TextEventListener::class]; + + if (Str::startsWith(app()->version(), ['5.2', '5.3'])) { + $event->listen('illuminate.log', function ($level, $message, $context) use ($event_listener) { + $event_listener->onLog($level, $message, $context); + }); + } else { + $event->listen(MessageLogged::class, function (MessageLogged $event) use ($event_listener) { + $event_listener->onLog($event->level, $event->message, $event->context); + }); + } + } + + + /** + * Register bindings in the container. + * + * @return void + */ + public function register() + { + $app_session_id = Uuid::uuid4()->toString(); + + $this->mergeConfigFrom( + __DIR__ . '/../../config/apideveloperio_logs.php', 'apideveloperio_logs' + ); + + // + // Prepare Guzzle Http Client to communicate with API backend + // + $this->app->bind(GuzzleHttpClient::class, function ($app) { + $config = $app[Repository::class]; + + $api_host = $config->get('apideveloperio_logs.api_host', 'backend.apideveloper.io'); + $ignore_ssl_problems = $config->get('apideveloperio_logs.api_host_ignore_ssl', false); + + return new GuzzleHttpClient([ + 'base_uri' => 'https://' . $api_host, + 'http_errors' => false, + 'verify' => !$ignore_ssl_problems, + ]); + }); + + // + // HTTPLog related + // + // Make sure event listener has just single instance + $this->app->singleton(HTTPEventListener::class, function ($app) use ($app_session_id) { + $config = $app[Repository::class]; + + return new HTTPEventListener( + $app_session_id, + $app[Repository::class], + $app[LoggerInterface::class], + new FileDumper(new PersistingStrategy( + $config->get('apideveloperio_logs.httplog.tmp_storage_path', 'unknown_path'), + $config->get('apideveloperio_logs.httplog.dump_files_max_count', 100), + $config->get('apideveloperio_logs.httplog.dump_file_max_size', 10 * 1024 * 1024), + $config->get('apideveloperio_logs.httplog.dump_file_prefix', 'recorded_requests') + )) + ); + }); + + // + // Text log related + // + $this->app->singleton(TextEventListener::class, function ($app) use ($app_session_id) { + $config = $app[Repository::class]; + + return new TextEventListener( + $app_session_id, + new FileDumper(new PersistingStrategy( + $config->get('apideveloperio_logs.textlog.tmp_storage_path', 'unknown_path'), + $config->get('apideveloperio_logs.textlog.dump_files_max_count', 100), + $config->get('apideveloperio_logs.textlog.dump_file_max_size', 10 * 1024 * 1024), + $config->get('apideveloperio_logs.textlog.dump_file_prefix', 'buffered_text_logs') + )), + $app[Repository::class], + $app[LoggerInterface::class] + ); + }); + // Before app has been shut down, let's dump recorded logs into file + $this->app->terminating(function () { + $this->app[TextEventListener::class]->flush(); + }); + + + } +} \ No newline at end of file diff --git a/src/Laravel/DumpRecordedRequests.php b/src/Laravel/DumpRecordedRequests.php deleted file mode 100644 index d1c73fb..0000000 --- a/src/Laravel/DumpRecordedRequests.php +++ /dev/null @@ -1,125 +0,0 @@ - - */ - -namespace HttpAnalyzer\Laravel; - -use Illuminate\Config\Repository; -use Illuminate\Console\Command; -use Illuminate\Contracts\Logging\Log; - -final class DumpRecordedRequests extends Command -{ - protected $signature = 'http_analyzer:dump'; - protected $description = 'Send recorded http requests to API backend and them remove them from local filesystem'; - /** @var Repository */ - private $config_repo; - /** @var Log */ - private $log; - /** @var GuzzleHttpClient */ - private $guzzle_http_client; - - - public function __construct(Repository $config_repo, Log $log, GuzzleHttpClient $client) - { - $this->config_repo = $config_repo; - $this->log = $log; - $this->guzzle_http_client = $client; - - parent::__construct(); - } - - public function handle() - { - $tmp_storage_path = $this->config_repo->get('http_analyzer.tmp_storage_path'); - $dump_file = $tmp_storage_path . "/recorded_requests"; - - if (file_exists($dump_file)) { - $this->renameFile($dump_file); - } - - $dump_files_full_paths = $this->findDumpFiles($tmp_storage_path); - // send if there are any - count($dump_files_full_paths) && $this->sendDumps($dump_files_full_paths); - - } - - /** - * Rename the file so no-one will attempt to write in it while transmitting - * - * @return string - */ - protected function renameFile($file) - { - $new_name = $file . "_batch_" . date('d-m-Y_H_i_s') . "_" . str_random(8); - rename($file, $new_name); - - return $new_name; - } - - /** - * Will scan directory for any files prepared to be dumped to API backend - * There could be more than just one file, because some dump requests can fail - * and file will stay until dumped again next time - * - * @param $dump_directory - * - * @return array - */ - protected function findDumpFiles($dump_directory) - { - return array_map(function ($filename) use ($dump_directory) { - return $dump_directory . "/" . $filename; - }, array_filter(scandir($dump_directory), function ($filename) { - return strpos($filename, "batch") !== false; - })); - } - - /** - * Send file with recorded requests to API backend server - * - * - * @param array $dump_file - * - * @return void - */ - protected function sendDumps($dump_files) - { - //Each file is big enough to be sent, so send one file per request - foreach ($dump_files as $dump_file) { - // Files contain valid JSON entries, concatenated with commas, - // so I just wrap them into an array - $concatenated_jsons = file_get_contents($dump_file); - $concatenated_jsons = trim($concatenated_jsons, ",");// remove trailing commas - $json_data = '{"requests":[' . $concatenated_jsons . ']}'; - - $response = $this->guzzle_http_client->request( - 'POST', - '/api/report/log', - [ - 'headers' => [ - 'content-type' => 'application/json', - ], - 'body' => $json_data, - ] - ); - - if ($response->getStatusCode() != 200) { - $this->log->alert("Http Analyzer's backend server could not handle the request", [ - 'response_code' => $response->getStatusCode(), - 'response_content' => $response->getBody()->getContents(), - ]); - - // Stop sending because it is something wrong with the API server - // wait till the next cycle - return; - } else { - $this->log->debug('recorded http requests sent to backend', - ['dump_file_name' => $dump_file, 'filesize' => filesize($dump_file)]); - unlink($dump_file); - } - } - } - -} \ No newline at end of file diff --git a/src/Laravel/GuzzleHttpClient.php b/src/Laravel/GuzzleHttpClient.php index 501ae38..32be56d 100644 --- a/src/Laravel/GuzzleHttpClient.php +++ b/src/Laravel/GuzzleHttpClient.php @@ -4,7 +4,7 @@ */ -namespace HttpAnalyzer\Laravel; +namespace Apideveloper\Laravel\Laravel; use GuzzleHttp\Client; diff --git a/src/Laravel/EventListener.php b/src/Laravel/HTTP/EventListener.php similarity index 59% rename from src/Laravel/EventListener.php rename to src/Laravel/HTTP/EventListener.php index 7f49a47..7b3ae67 100644 --- a/src/Laravel/EventListener.php +++ b/src/Laravel/HTTP/EventListener.php @@ -3,14 +3,15 @@ * @author Dmitriy Lezhnev */ -namespace HttpAnalyzer\Laravel; +namespace Apideveloper\Laravel\Laravel\HTTP; -use HttpAnalyzer\Backend\LoggedRequest; +use Apideveloper\Laravel\Backend\File\LogsDumper; +use Apideveloper\Laravel\Backend\LoggedHTTPRequest; use Illuminate\Config\Repository; -use Illuminate\Contracts\Logging\Log; use Illuminate\Database\Events\QueryExecuted; use Illuminate\Http\Request; +use Psr\Log\LoggerInterface; use Symfony\Component\HttpFoundation\Response; @@ -20,82 +21,92 @@ */ class EventListener { - + /** @var array */ private $recorded_data = [ 'external_queries' => [], 'log_entries' => [], ]; + /** @var string */ + private $session_id; /** @var Repository */ private $config_repo; - /** @var Log */ + /** @var LoggerInterface */ private $log_writer; - + /** @var LogsDumper */ + private $dumper; + /** * EventListener constructor. * + * @param string $session_id ; * @param Repository $config_repo + * @param Log $log + * @param LogsDumper $dumper */ - public function __construct(Repository $config_repo, Log $log) + public function __construct($session_id, Repository $config_repo, LoggerInterface $log, LogsDumper $dumper) { + $this->session_id = $session_id; $this->config_repo = $config_repo; $this->log_writer = $log; + $this->dumper = $dumper; } - - + + public function onRequestHandled(Request $request, Response $response) { // If possible - calculate time duration $time_to_response = defined('LARAVEL_START') ? intval((microtime(true) - constant('LARAVEL_START')) * 1000) : 1; - + try { if ($this->isRecordingDisabled() || $this->shouldSkipRequest($request)) { return; } - + // // Prepare packet with all recorded data // - $logged_request = new LoggedRequest( + $logged_request = new LoggedHTTPRequest( $request, $response, $time_to_response, + $this->session_id, implode("\n\n", $this->recorded_data['log_entries']), $this->recorded_data['external_queries'], - $this->config_repo->get('http_analyzer.filtering') + $this->config_repo->get('apideveloperio_logs.httplog.filtering') ); - + $this->saveRecordedRequest($logged_request); } catch (\Throwable $e) { $this->fail($e); } } - + public function onDatabaseQueryExecuted(QueryExecuted $event) { try { if ($this->isRecordingDisabled()) { return; } - + $pdo = $event->connection->getPdo(); $vendor = $pdo->getAttribute(\PDO::ATTR_DRIVER_NAME) . - "/" . - $pdo->getAttribute(\PDO::ATTR_SERVER_VERSION); - + "/" . + $pdo->getAttribute(\PDO::ATTR_SERVER_VERSION); + // Laravel casting returns some items as DateTime objects foreach ($event->bindings as $key => $binding) { if (is_a($binding, 'DateTime')) { $event->bindings[$key] = $binding->format('Y-m-d H:i:s'); } } - + // Replace bindings with real values $sql = str_replace(['%', '?'], ['%%', '%s'], $event->sql); $sql = vsprintf($sql, $event->bindings); - + // Log this query $this->recorded_data['external_queries'][] = [ "query" => $sql, @@ -103,82 +114,49 @@ public function onDatabaseQueryExecuted(QueryExecuted $event) "type" => "database", "vendor" => $vendor, ]; - + } catch (\Throwable $e) { $this->fail($e); } } - + public function onLog($level, $message, $context) { try { if ($this->isRecordingDisabled()) { return; } - + $logged_message = $message . "\n"; - + // if it can be jsonified - do it otherway just serialize if (($json = json_encode($context, JSON_UNESCAPED_UNICODE)) !== false) { $logged_message .= $json; } else { $logged_message .= serialize($context); } - + $this->recorded_data['log_entries'][] = $logged_message; } catch (\Throwable $e) { $this->fail($e); } } - - + + /** * saveRecordedRequest * * - * @param LoggedRequest $request + * @param LoggedHTTPRequest $request * * @throws \Exception * @return void */ - protected function saveRecordedRequest(LoggedRequest $request) + protected function saveRecordedRequest(LoggedHTTPRequest $request) { - $tmp_path_folder = $this->config_repo->get('http_analyzer.tmp_storage_path'); - - // - // Now persist data till the next data dump to the API backend - // - if (!is_dir($tmp_path_folder) && !mkdir($tmp_path_folder)) { - throw new \Exception("Unable to create a directory for storing recorded requests at " . $tmp_path_folder); - } - - // - // If there are too many dumped un-sent files then stop recording - // - $max_files_count = (int)$this->config_repo->get('http_analyzer.dump_files_max_count', 100); - $dump_files = array_filter(scandir($tmp_path_folder), function ($file) { - return strpos($file, "recorded_requests") !== false; - }); - if (count($dump_files) >= $max_files_count) { - throw new \Exception("Maximum count of dump files reached. Recording stopped."); - } - - // - // make a file to dump every request to - // - $file_path = $tmp_path_folder . "/recorded_requests"; - $max_file_size = (int)$this->config_repo->get('http_analyzer.dump_file_max_size', 10 * 1024 * 1024); - if (file_exists($file_path) && filesize($file_path) > $max_file_size) { - // rename it and write to a fresh one - rename($file_path, $file_path . "_batch_" . date("d-m-Y_H_i_s") . "_" . str_random(8)); - } - - // - // Dump it - // - file_put_contents($file_path, $request->toJson() . ",", FILE_APPEND | LOCK_EX); + $this->dumper->dump($request->toArray()); } - + /** * Detect if current environment is allowed to be recorded * @@ -188,11 +166,11 @@ protected function saveRecordedRequest(LoggedRequest $request) protected function isRecordingDisabled() { return in_array( - app()->environment(), - $this->config_repo->get('http_analyzer.filtering.ignore_environment', []) - ) || !$this->config_repo->get('http_analyzer.enabled'); + app()->environment(), + $this->config_repo->get('apideveloperio_logs.httplog.filtering.ignore_environment', []) + ) || !$this->config_repo->get('apideveloperio_logs.httplog.enabled'); } - + /** * Check if current request matches the filtering regexp * @@ -203,24 +181,25 @@ protected function isRecordingDisabled() */ protected function shouldSkipRequest(Request $request) { - $regexp_patterns = $this->config_repo->get('http_analyzer.filtering.skip_url_matching_regexp', []); - $skip_http_methods = $this->config_repo->get('http_analyzer.filtering.skip_http_methods'); - + $regexp_patterns = $this->config_repo->get('apideveloperio_logs.httplog.filtering.skip_url_matching_regexp', + []); + $skip_http_methods = $this->config_repo->get('apideveloperio_logs.httplog.filtering.skip_http_methods'); + $should_skip = false; - + // check against HTTP method $should_skip = $should_skip || - in_array(strtoupper($request->method()), array_map('strtoupper', $skip_http_methods)); - + in_array(strtoupper($request->method()), array_map('strtoupper', $skip_http_methods)); + // Check URL against regexps $should_skip = $should_skip || - (bool)count(array_filter($regexp_patterns, function ($pattern) use ($request) { - return preg_match("#$pattern#", $request->getPathInfo()); - })); - + (bool)count(array_filter($regexp_patterns, function ($pattern) use ($request) { + return preg_match("#$pattern#", $request->getPathInfo()); + })); + return $should_skip; } - + /** * Silently fail with log message * @@ -233,6 +212,6 @@ protected function fail(\Throwable $e) { $this->log_writer->alert("Http analyzer failed", ['reason' => $e->getMessage()]); } - - + + } diff --git a/src/Laravel/HttpAnalyzerServiceProvider.php b/src/Laravel/HttpAnalyzerServiceProvider.php deleted file mode 100644 index 380fb53..0000000 --- a/src/Laravel/HttpAnalyzerServiceProvider.php +++ /dev/null @@ -1,87 +0,0 @@ -publishes([ - __DIR__ . '/../../config/http_analyzer.php' => config_path('http_analyzer.php'), - ]); - - // - // Hook on events - // - - $event = app(Dispatcher::class); - $event->listen(RequestHandled::class, function (RequestHandled $event) { - $listener = app()[EventListener::class]; - $listener->onRequestHandled($event->request, $event->response); - }); - $event->listen(QueryExecuted::class, EventListener::class . '@onDatabaseQueryExecuted'); - $event->listen(MessageLogged::class, function (MessageLogged $event) { - $listener = app()[EventListener::class]; - $listener->onLog( - $event->level, - $event->message, - $event->context - ); - }); - - } - - /** - * Register bindings in the container. - * - * @return void - */ - public function register() - { - $this->mergeConfigFrom( - __DIR__ . '/../../config/http_analyzer.php', 'http_analyzer' - ); - - - // Make sure event listener has just single instance - $this->app->singleton(EventListener::class, function ($app) { - return new EventListener( - $app[Repository::class], - $app[Log::class] - ); - }); - - // - // Prepare Guzzle Http Client to communicate with API backend - // - $this->app->bind(GuzzleHttpClient::class, function ($app) { - $config = $app[Repository::class]; - - $api_host = $config->get('http_analyzer.api_host', 'backend.apideveloper.io'); - $api_key = $config->get('http_analyzer.api_key'); - - return new GuzzleHttpClient([ - 'base_uri' => 'https://' . $api_host, - 'http_errors' => false, - 'query' => ['api_key' => $api_key], - ]); - }); - - } -} \ No newline at end of file diff --git a/src/Laravel/SendDumpsToDashboard.php b/src/Laravel/SendDumpsToDashboard.php new file mode 100644 index 0000000..684c55c --- /dev/null +++ b/src/Laravel/SendDumpsToDashboard.php @@ -0,0 +1,176 @@ + + * Date: 25/01/2018 + */ + +namespace Apideveloper\Laravel\Laravel; + + +use Illuminate\Config\Repository; +use Illuminate\Console\Command; +use Psr\Log\LoggerInterface; + +class SendDumpsToDashboard extends Command +{ + protected $signature = 'apideveloper:send-logs {--types=http,text : Specify what logs to dump}'; + protected $description = 'Send recorded http requests and text logs to API backend and them remove them from local filesystem'; + /** @var Repository */ + private $config_repo; + /** @var GuzzleHttpClient */ + private $guzzle; + /** @var LoggerInterface */ + private $log; + + /** + * SendDumpsToDashboard constructor. + * @param Repository $config_repo + * @param GuzzleHttpClient $guzzle + * @param LoggerInterface $log + */ + public function __construct(Repository $config_repo, GuzzleHttpClient $guzzle, LoggerInterface $log) + { + $this->config_repo = $config_repo; + $this->guzzle = $guzzle; + $this->log = $log; + + parent::__construct(); + } + + + public function handle() + { + $types = explode(",", $this->option('types')); + + in_array('http', $types) && $this->sendHTTPLogs(); + in_array('text', $types) && $this->sendTextLogs(); + } + + function sendHTTPLogs() + { + $enabled = $this->config_repo->get('apideveloperio_logs.httplog.enabled', false); + if (!$enabled) { + // this featured is disabled, cancel operation + return; + } + + $url = '/api/report/log'; + $batch_key = "requests"; + $api_key = $this->config_repo->get('apideveloperio_logs.httplog.api_key'); + $file_prefix = $this->config_repo->get('apideveloperio_logs.httplog.dump_file_prefix', 'recorded_requests'); + $folder = $this->config_repo->get('apideveloperio_logs.httplog.tmp_storage_path', 'unknown_path'); + + + $dump_files_full_paths = $this->findDumpFiles($folder, $file_prefix); + + count($dump_files_full_paths) && $this->sendDumps($url, $api_key, $dump_files_full_paths, $batch_key); + } + + function sendTextLogs() + { + $enabled = $this->config_repo->get('apideveloperio_logs.textlog.enabled', false); + if (!$enabled) { + // this featured is disabled, cancel operation + return; + } + + $url = '/api/report/log-text'; + $batch_key = "entries"; + $api_key = $this->config_repo->get('apideveloperio_logs.textlog.api_key'); + $file_prefix = $this->config_repo->get('apideveloperio_logs.textlog.dump_file_prefix', 'buffered_text_logs'); + $folder = $this->config_repo->get('apideveloperio_logs.textlog.tmp_storage_path', 'unknown_path'); + + + $dump_files_full_paths = $this->findDumpFiles($folder, $file_prefix); + count($dump_files_full_paths) && $this->sendDumps($url, $api_key, $dump_files_full_paths, $batch_key); + } + + /** + * Will scan directory for any files prepared to be dumped to API backend + * There could be more than just one file, because some dump requests can fail + * and file will stay until dumped again next time + * + * @param $dump_directory + * @param $file_prefix + * + * @return array + */ + protected function findDumpFiles($dump_directory, $file_prefix) + { + if (!is_dir($dump_directory) || !is_readable($dump_directory)) { + // directory where logs are supposed to be stored is not discoverable + // just skip it and continue + return []; + } + + // Before finding files, I want to rename last file and thus mark it for sending + $current_file = $dump_directory . "/" . $file_prefix; + if (is_file($current_file) && filesize($current_file)) { + rename($current_file, $current_file . "_batch_" . date("d-m-Y_H_i_s") . "_" . str_random(8)); + } + + // Now we are ready to look for files to send + return array_map(function ($filename) use ($dump_directory) { + return $dump_directory . "/" . $filename; + }, array_filter(scandir($dump_directory), function ($filename) use ($file_prefix) { + return strpos($filename, "batch") !== false && strpos($filename, $file_prefix) !== false; + })); + } + + /** + * Send file with recorded requests to API backend server + * + * @param $url + * @param $api_key + * @param $dump_files + * @param $batch_key + */ + protected function sendDumps($url, $api_key, $dump_files, $batch_key) + { + //Each file is big enough to be sent, so send one file per request + foreach ($dump_files as $dump_file) { + // Files contain valid JSON entries, concatenated with commas, + // so I just wrap them into an array + $concatenated_jsons = file_get_contents($dump_file); + $concatenated_jsons = trim($concatenated_jsons, ",");// remove trailing commas + $json_data = '{"' . $batch_key . '":[' . $concatenated_jsons . ']}'; + + $response = $this->guzzle->request( + 'POST', + $url, + [ + 'headers' => [ + 'content-type' => 'application/json', + ], + 'query' => [ + 'api_key' => $api_key, + ], + 'body' => $json_data, + ] + ); + + if ($response->getStatusCode() != 200) { + $this->log->alert("Apideveloper's backend server could not handle the request", [ + 'response_code' => $response->getStatusCode(), + 'response_content' => $response->getBody()->getContents(), + ]); + + // Stop sending because it is something wrong with the API server + // wait till the next cycle + return; + } else { + $logOnSend = $this->config_repo->get('apideveloperio_logs.log'); + if ($logOnSend) { + $this->log->debug( + 'recorded logs sent to apideveloper.io dashboard', + [ + 'dump_file_name' => $dump_file, + 'filesize' => filesize($dump_file), + ] + ); + } + unlink($dump_file); + } + } + } +} \ No newline at end of file diff --git a/src/Laravel/Text/EventListener.php b/src/Laravel/Text/EventListener.php new file mode 100644 index 0000000..668389c --- /dev/null +++ b/src/Laravel/Text/EventListener.php @@ -0,0 +1,172 @@ + + * Date: 24/01/2018 + */ + +namespace Apideveloper\Laravel\Laravel\Text; + +use Apideveloper\Laravel\Backend\File\LogsDumper; +use Carbon\Carbon; +use Illuminate\Config\Repository; +use Psr\Log\LoggerInterface; + +class EventListener +{ + private $buffer = []; + /** @var Repository */ + private $config_repo; + /** @var LoggerInterface */ + private $log_writer; + /** @var LogsDumper */ + private $dumper; + + /** + * EventListener constructor. + * @param string $session_id the key of current app execution (session, used to unite log entry within the same session) + * @param LogsDumper $dumper + * @param Repository $config_repo + * @param LoggerInterface $log_writer + */ + public function __construct($session_id, LogsDumper $dumper, Repository $config_repo, LoggerInterface $log_writer) + { + $this->dumper = $dumper; + $this->config_repo = $config_repo; + $this->log_writer = $log_writer; + + $this->buffer = [ + 'meta' => [ + 'env' => app()->environment(), + 'session_id' => $session_id, + 'io_channel' => app()->runningInConsole() ? "console" : "http", + ], + 'messages' => [], + ]; + } + + + function onLog($level, $message, $context) + { + + if ($this->isRecordingDisabled()) { + return; + } + + $entry = [ + 'level' => $level, + 'date' => Carbon::now()->toIso8601String(), + ]; + + // Check exception in the context array + // Laravel's default behaviour is to throw normal error message + // and put exception in the context under 'exception' key + // see 'laravel/framework/src/Illuminate/Foundation/Exceptions/Handler.php' + if ( + ($e = array_get($context, 'exception')) instanceof \Exception || + ($e = $message) instanceof \Exception + ) { + $entry['exception'] = ExceptionFormatter::fromException($e)->toArray(); + unset($context['exception']); + } else { + if (is_scalar($message)) { + $entry['message'] = $message; + } else { + $entry['message'] = (array)$message; + } + } + + // Transform context to transferable values (scalars and arrays) + // Context can contain compound values, like objects, so I want to attempt to stringify them + array_walk_recursive($context, function (&$value, $key) { + if (is_object($value)) { + $value = (string)$value; + } + }); + $entry['context'] = (array)$context; // context is supposed to be array + + // Ok push the log to the buffer until dumped to the file + $this->buffer['messages'][] = $entry; + } + + /** + * Detect if current environment is allowed to be recorded + * + * + * @return bool + */ + protected function isRecordingDisabled() + { + return + in_array( + app()->environment(), + $this->config_repo->get('apideveloperio_logs.textlog.filtering.ignore_environment', []) + ) || !$this->config_repo->get('apideveloperio_logs.textlog.enabled'); + } + + + /** + * Silently fail with log message + * + * + * @param \Throwable $e + * + * @return void + */ + protected function fail(\Throwable $e) + { + // do nothing since there is an issue with logging + // the only thing we can do is throw something to the stderr + fwrite(fopen('php://stderr', 'w'), (string)$e); + } + + public function flush() + { + try { + // Dump is performed upon application destruction (at the very end) + if (count($this->buffer['messages'])) { + // no logs, no writing + $this->checkDuplicates(); + $this->dumper->dump($this->buffer); + $this->buffer['messages'] = []; // flush written data + } + } catch (\Exception $e) { + $this->fail($e); + } + } + + /** + * Remove message duplicates. In some cases 3rd-party packages can emit logging events multiple times (laravel-bugsnag for example) + * And this logger will record such events multiple times which is wrong + * + * This option is off by default since it can remove legitimate log entries which happens to be the same + * + * @return void + */ + private function checkDuplicates() + { + $duplicateRemovalEnabled = $this + ->config_repo + ->get('apideveloperio_logs.textlog.filtering.remove_duplicates', false); + + if ($duplicateRemovalEnabled) { + for ($cur = 1, $count = count($this->buffer['messages']); $cur < $count; $cur++) { + $prev = $cur - 1; + if ($this->buffer['messages'][$prev] === $this->buffer['messages'][$cur]) { + unset($this->buffer['messages'][$prev]); + } + } + + // reindex + $this->buffer['messages'] = array_values($this->buffer['messages']); + } + } + + // This is an alternative way to initiate dumping to file + // So far not required since we hooked to app's shutdown sequence in service provider +// public function __destruct() +// { +// $this->flush(); +// } + + +} \ No newline at end of file diff --git a/src/Laravel/Text/ExceptionFormatter.php b/src/Laravel/Text/ExceptionFormatter.php new file mode 100644 index 0000000..3c7e045 --- /dev/null +++ b/src/Laravel/Text/ExceptionFormatter.php @@ -0,0 +1,86 @@ + + * Date: 24/01/2018 + */ + +namespace Apideveloper\Laravel\Laravel\Text; + + +/** + * Class ExceptionFormatter transforms exception object to array + * @package Apideveloper\Laravel\Laravel\Text + */ +class ExceptionFormatter +{ + private $array = []; + + /** + * ExceptionFormatter constructor. + * @param \Exception $e + */ + private function __construct($e) + { + do { + $this->array[] = $this->getLevel($e); + } while ($e = $e->getPrevious()); + } + + /** + * @param \Exception $e + * @return array + */ + private function getLevel($e) + { + return [ + "class" => get_class($e), + "message" => $e->getMessage(), + "code" => $e->getCode(), + "file" => $e->getFile(), + "line" => $e->getLine(), + "trace" => $e->getTraceAsString(), + ]; + } + +// /** +// * @param \Exception $e +// * @return array +// */ +// private function getTrace($e) +// { +// return array_map(function ($trace_step) { +// return [ +// 'file' => array_get($trace_step, 'file'), +// 'line' => array_get($trace_step, 'line'), +// 'function' => array_get($trace_step, 'function'), +// 'class' => array_get($trace_step, 'class'), +// 'type' => array_get($trace_step, 'type'), +// 'args' => $this->getTraceArgs(array_get($trace_step, 'args', [])), +// ]; +// }, $e->getTrace()); +// } +// +// private function getTraceArgs($args) +// { +// dump($args, json_encode($args, JSON_PRETTY_PRINT)); +// +// return $args; +// } + + /** + * @param \Exception|\Throwable $e + * @return self + */ + static public function fromException($e) + { + return new self($e); + } + + /** + * @return array + */ + function toArray() + { + return $this->array; + } +} \ No newline at end of file diff --git a/tests/Backend/LoggedRequestTest.php b/tests/Backend/LoggedRequestTest.php index 4b427e8..594ecf8 100644 --- a/tests/Backend/LoggedRequestTest.php +++ b/tests/Backend/LoggedRequestTest.php @@ -3,13 +3,13 @@ * @author Dmitriy Lezhnev */ -namespace HttpAnalyzerTest\Backend; +namespace Apideveloper\Laravel\Tests\Backend; -use function GuzzleHttp\Psr7\parse_query; -use HttpAnalyzer\Backend\LoggedRequest; +use Apideveloper\Laravel\Backend\LoggedHTTPRequest; use Illuminate\Http\Request; use Illuminate\Http\Response; use PHPUnit\Framework\TestCase; +use function GuzzleHttp\Psr7\parse_query; final class LoggedRequestTest extends TestCase { @@ -28,21 +28,23 @@ function test_it_produces_valid_minimum_required_data() 'HTTP_USER_AGENT' => 'Mozilla Firefox', ] ); - + $response = new Response('', 200); - - $logged_request = new LoggedRequest( + + $logged_request = new LoggedHTTPRequest( $request, $response, 100, + '123', null, null, ['strip_data' => ["request_headers", "request_body", "response_headers", "response_body"]] ); $data = $logged_request->toArray(); - + $this->assertEquals( [ + "session_id" => "123", "full_url" => "http://example.org/shop/cart.php?num=one&price=10", "http_method" => "post", "user_ip" => "192.167.35.22", @@ -58,13 +60,13 @@ function test_it_produces_valid_minimum_required_data() $data ); } - + function test_it_produces_valid_full_optional_data() { // // Set environment // - + $request = Request::create( 'http://example.org/shop/cart.php?num=one&price=10', 'POST', @@ -81,21 +83,22 @@ function test_it_produces_valid_full_optional_data() ], 'I know you can see it' ); - + $response = new Response('Hello guys!', 200, [ 'content-type' => 'text/html', 'date' => 1497847023, ]); - - + + // // Now produce logged packet with data (which can be sent to API backend) // - - $logged_request = new LoggedRequest( + + $logged_request = new LoggedHTTPRequest( $request, $response, 100, + "123", "line1\nline2\n", [ [ @@ -107,13 +110,18 @@ function test_it_produces_valid_full_optional_data() ] ); $data = $logged_request->toArray(); - + // // Assert that packet has expected format // - + + // older response class did not add "private" + // so I just hack it here (no big deal) + $data['http_response_headers'][2]['value'] = "no-cache, private"; + $this->assertEquals( [ + "session_id" => "123", "full_url" => "http://example.org/shop/cart.php?num=one&price=10", "http_method" => "post", "user_ip" => "192.167.35.22", @@ -121,7 +129,7 @@ function test_it_produces_valid_full_optional_data() "ttr_ms" => 100, "timestamp" => 1497847022, "http_response_code" => 200, - + "server" => [ "hostname" => gethostname(), "ip" => "127.0.0.1", @@ -185,7 +193,7 @@ function test_it_produces_valid_full_optional_data() $data ); } - + function test_it_stripps_header_values() { $request = Request::create( @@ -198,17 +206,18 @@ function test_it_stripps_header_values() 'HTTP_USER_AGENT' => 'Mozilla Firefox', ] ); - - $logged_request = new LoggedRequest( + + $logged_request = new LoggedHTTPRequest( $request, new Response('', 200), 100, + "123", null, null, ['strip_header_values' => ["HOST", "cache-CONtrol"]] ); $data = $logged_request->toArray(); - + $this->assertTrue( array_search(['name' => 'host', 'value' => "__STRIPPED_VALUE__"], $data['http_request_headers']) !== false ); @@ -217,7 +226,7 @@ function test_it_stripps_header_values() $data['http_response_headers']) !== false ); } - + function test_it_stripps_query_string_values() { $request = Request::create( @@ -230,11 +239,12 @@ function test_it_stripps_query_string_values() 'HTTP_USER_AGENT' => 'Mozilla Firefox', ] ); - - $logged_request = new LoggedRequest( + + $logged_request = new LoggedHTTPRequest( $request, new Response('', 200), 100, + "123", null, null, [ @@ -244,10 +254,10 @@ function test_it_stripps_query_string_values() $data = $logged_request->toArray(); $info = parse_url($data['full_url']); $query_string = parse_query($info['query']); - + $this->assertEquals("__STRIPPED_VALUE__", $query_string['api_key']); } - + function test_it_converts_header_names_and_values_to_strings() { $request = Request::create( @@ -265,19 +275,19 @@ function test_it_converts_header_names_and_values_to_strings() ); $request->headers->add([1 => 2]); $request->headers->add([3 => null]); - + $response = new Response('', 200); - - $logged_request = new LoggedRequest($request, $response, 100); + + $logged_request = new LoggedHTTPRequest($request, $response, 100, "123"); $data = $logged_request->toArray(); - + $this->assertTrue(array_search(['name' => 'user-agent', 'value' => ''], $data['http_request_headers'], true) !== false); $this->assertTrue(array_search(['name' => '1', 'value' => '2'], $data['http_request_headers'], true) !== false); $this->assertTrue(array_search(['name' => '3', 'value' => ''], $data['http_request_headers'], true) !== false); - + } - + } \ No newline at end of file diff --git a/tests/PackageTest.php b/tests/HTTP/HTTPLogTest.php similarity index 66% rename from tests/PackageTest.php rename to tests/HTTP/HTTPLogTest.php index 446dcd9..b6b878d 100644 --- a/tests/PackageTest.php +++ b/tests/HTTP/HTTPLogTest.php @@ -1,44 +1,51 @@ createApplication(); - + $stub = static::prophesize(EventListener::class); $app[EventListener::class] = $stub->reveal(); - + $request = new Request(); $response = new Response(); $stub->onRequestHandled($request, $response)->shouldBeCalled(); - - $app[Dispatcher::class]->fire(new RequestHandled($request, $response)); + + if (Str::startsWith(app()->version(), ['5.2', '5.3'])) { + $app[Dispatcher::class]->fire('kernel.handled', [$request, $response]); + } else { + $app[Dispatcher::class]->fire(new RequestHandled($request, $response)); + } } - + function test_it_subscribed_on_query_executed_event() { $app = $this->createApplication(); - + $stub = static::prophesize(EventListener::class); $app[EventListener::class] = $stub->reveal(); - + $event = new QueryExecuted( '', [], @@ -46,78 +53,87 @@ function test_it_subscribed_on_query_executed_event() static::prophesize(Connection::class) ); $stub->onDatabaseQueryExecuted($event)->shouldBeCalled(); - + $app[Dispatcher::class]->fire($event); } - + function test_it_subscribed_on_new_message_in_log_event() { $app = $this->createApplication(); - + $stub = static::prophesize(EventListener::class); $app[EventListener::class] = $stub->reveal(); - - + + $stub->onLog( Argument::is('alert'), Argument::is('message'), Argument::is(['a' => 2]) )->shouldBeCalled(); - + $app['log']->alert("message", ['a' => 2]); } - - + + function test_it_dumps_data_to_file() { $app = $this->createApplication(); - + /** @var Repository $config */ $config = app()[Repository::class]; $tmp_storage_path = $this->getTmpPath(__LINE__); - $config->set('http_analyzer.tmp_storage_path', $tmp_storage_path); - + $config->set('apideveloperio_logs.httplog.tmp_storage_path', $tmp_storage_path); + $request = new Request(); $response = new Response(); - $app[Dispatcher::class]->fire(new RequestHandled($request, $response)); - + if (Str::startsWith(app()->version(), ['5.2', '5.3'])) { + $app[Dispatcher::class]->fire('kernel.handled', [$request, $response]); + } else { + $app[Dispatcher::class]->fire(new RequestHandled($request, $response)); + } + // Make sure file is there - + $this->assertFileExists($tmp_storage_path . "/recorded_requests"); - + // clean up unlink($tmp_storage_path . "/recorded_requests"); rmdir($tmp_storage_path); } - + function test_it_dumps_multipart_post_data_to_file() { $app = $this->createApplication(); - + /** @var Repository $config */ $config = app()[Repository::class]; $tmp_storage_path = $this->getTmpPath(__LINE__); - $config->set('http_analyzer.tmp_storage_path', $tmp_storage_path); - + $config->set('apideveloperio_logs.httplog.tmp_storage_path', $tmp_storage_path); + $data = ['a' => 'some']; $request = Request::create('/api/signup?b=12', 'POST', $data); $response = new Response(); - $app[Dispatcher::class]->fire(new RequestHandled($request, $response)); - + if (Str::startsWith(app()->version(), ['5.2', '5.3'])) { + $app[Dispatcher::class]->fire('kernel.handled', [$request, $response]); + } else { + $app[Dispatcher::class]->fire(new RequestHandled($request, $response)); + } + // Make sure file is there - + $recorded_response = json_decode(trim(file_get_contents($tmp_storage_path . "/recorded_requests"), ','), true); $this->assertEquals($data, json_decode($recorded_response['http_request_body'], true)); - + // clean up unlink($tmp_storage_path . "/recorded_requests"); rmdir($tmp_storage_path); } - + function test_it_sends_dump_to_api_backend() { $console_app = new Application($this->app, $this->app[Dispatcher::class], "1"); - + $config = $console_app->getLaravel()[Repository::class]; + // I want to know that HTTP request was attempted to be send $fake_http_client = static::prophesize(GuzzleHttpClient::class); $fake_http_client @@ -126,6 +142,7 @@ function test_it_sends_dump_to_api_backend() Argument::is('/api/report/log'), Argument::is([ "headers" => ["content-type" => "application/json"], + "query" => ["api_key" => $config->set('apideveloperio_logs.httplog.api_key')], "body" => '{"requests":[{"sample"=>"data"}]}', ]) ) @@ -134,137 +151,152 @@ function test_it_sends_dump_to_api_backend() ) ->shouldBeCalled(); $console_app->getLaravel()[GuzzleHttpClient::class] = $fake_http_client->reveal(); - - $console_app->resolve(DumpRecordedRequests::class); - $config = $console_app->getLaravel()[Repository::class]; - + + $console_app->resolve(SendDumpsToDashboard::class); + // Make up dump file $tmp_storage_path = $this->getTmpPath(__LINE__); - $config->set('http_analyzer.tmp_storage_path', $tmp_storage_path); + $config->set('apideveloperio_logs.httplog.tmp_storage_path', $tmp_storage_path); file_put_contents($tmp_storage_path . "/recorded_requests", '{"sample"=>"data"},', FILE_APPEND); - + // Initiate a command - $console_app->call('http_analyzer:dump'); - + $console_app->call('apideveloper:send-logs', ['--types' => 'http']); + // clean up rmdir($tmp_storage_path); } - + function test_it_splits_dump_files_to_fit_the_size() { $app = $this->createApplication(); $config = $app[Repository::class]; - $config->set('http_analyzer.dump_file_max_size', 1); // as little as possible + $config->set('apideveloperio_logs.httplog.dump_file_max_size', 1); // as little as possible $tmp_storage_path = $this->getTmpPath(__LINE__); - $config->set('http_analyzer.tmp_storage_path', $tmp_storage_path); - + $config->set('apideveloperio_logs.httplog.tmp_storage_path', $tmp_storage_path); + // Now imitate $n requests and make sure n files are produces $n = rand(0, 100); for ($i = 0; $i < $n; $i++) { - $app[Dispatcher::class]->fire(new RequestHandled(new Request(), new Response())); + if (Str::startsWith(app()->version(), ['5.2', '5.3'])) { + $app[Dispatcher::class]->fire('kernel.handled', [new Request(), new Response()]); + } else { + $app[Dispatcher::class]->fire(new RequestHandled(new Request(), new Response())); + } } - + $files_in_tmp_folder = array_filter(scandir($tmp_storage_path), function ($file) use ($tmp_storage_path) { return is_file($tmp_storage_path . "/" . $file); }); - + $this->assertEquals($n, count($files_in_tmp_folder)); - + // clean up array_walk($files_in_tmp_folder, function ($file) use ($tmp_storage_path) { unlink($tmp_storage_path . "/" . $file); }); rmdir($tmp_storage_path); } - + function test_it_skips_regex_urls() { $app = $this->createApplication(); - + /** @var Repository $config */ $config = app()[Repository::class]; $tmp_storage_path = $this->getTmpPath(__LINE__); - $config->set('http_analyzer.tmp_storage_path', $tmp_storage_path); - $config->set('http_analyzer.filtering.skip_url_matching_regexp', ['^/auth']); - + $config->set('apideveloperio_logs.httplog.tmp_storage_path', $tmp_storage_path); + $config->set('apideveloperio_logs.httplog.filtering.skip_url_matching_regexp', ['^/auth']); + $request = Request::create('/auth/signup'); $response = new Response(); - $app[Dispatcher::class]->fire(new RequestHandled($request, $response)); - + if (Str::startsWith(app()->version(), ['5.2', '5.3'])) { + $app[Dispatcher::class]->fire('kernel.handled', [$request, $response]); + } else { + $app[Dispatcher::class]->fire(new RequestHandled($request, $response)); + } + // Make sure file is there $this->assertFileNotExists($tmp_storage_path . "/recorded_requests"); - + // cleanup rmdir($tmp_storage_path); } - + function test_it_will_skip_certain_http_methods() { $app = $this->createApplication(); - + /** @var Repository $config */ $config = app()[Repository::class]; $tmp_storage_path = $this->getTmpPath(__LINE__); - $config->set('http_analyzer.tmp_storage_path', $tmp_storage_path); - $config->set('http_analyzer.filtering.skip_http_methods', ['OPtiONS']); - + $config->set('apideveloperio_logs.httplog.tmp_storage_path', $tmp_storage_path); + $config->set('apideveloperio_logs.httplog.filtering.skip_http_methods', ['OPtiONS']); + $request = Request::create('/auth/signup', 'OPTIONs'); $response = new Response(); - $app[Dispatcher::class]->fire(new RequestHandled($request, $response)); - + if (Str::startsWith(app()->version(), ['5.2', '5.3'])) { + $app[Dispatcher::class]->fire('kernel.handled', [$request, $response]); + } else { + $app[Dispatcher::class]->fire(new RequestHandled($request, $response)); + } + // Make sure file is there $this->assertFileNotExists($tmp_storage_path . "/recorded_requests"); - + // cleanup rmdir($tmp_storage_path); } - + function test_it_can_be_disabled() { $app = $this->createApplication(); - + /** @var Repository $config */ $config = app()[Repository::class]; $tmp_storage_path = $this->getTmpPath(__LINE__); - $config->set('http_analyzer.tmp_storage_path', $tmp_storage_path); - $config->set('http_analyzer.enabled', false); - + $config->set('apideveloperio_logs.httplog.tmp_storage_path', $tmp_storage_path); + $config->set('apideveloperio_logs.httplog.enabled', false); + $request = Request::create('/auth/signup'); $response = new Response(); - $app[Dispatcher::class]->fire(new RequestHandled($request, $response)); - + if (Str::startsWith(app()->version(), ['5.2', '5.3'])) { + $app[Dispatcher::class]->fire('kernel.handled', [$request, $response]); + } else { + $app[Dispatcher::class]->fire(new RequestHandled($request, $response)); + } + // Make sure file is there $this->assertFileNotExists($tmp_storage_path . "/recorded_requests"); - + // cleanup rmdir($tmp_storage_path); } - + function test_it_automatically_converts_query_bindings_to_strings() { $app = $this->createApplication(); - + // Enable logging while testing /** @var Repository $config */ $config = app()[Repository::class]; - $config->set('http_analyzer.filtering.ignore_environment', []); - $config->set('http_analyzer.enabled', true); - + $config->set('apideveloperio_logs.filtering.ignore_environment', []); + $config->set('apideveloperio_logs.httplog.httplog.enabled', true); + // Fake log writer // Log shoudl not be called because logging is only called on problem - $log_prophecy = static::prophesize(Log::class); + $log_prophecy = static::prophesize(LoggerInterface::class); $log_prophecy->alert(Argument::cetera())->shouldNotBeCalled(); - $app[Log::class] = $log_prophecy->reveal(); - + $app[LoggerInterface::class] = $log_prophecy->reveal(); + // Fake Connection $pdo_prophecy = static::prophesize(\PDO::class); $pdo_prophecy->getAttribute(\PDO::ATTR_DRIVER_NAME)->willReturn("A")->shouldBeCalled(); $pdo_prophecy->getAttribute(\PDO::ATTR_SERVER_VERSION)->willReturn("B")->shouldBeCalled(); - + $connection_prophecy = static::prophesize(Connection::class); $connection_prophecy->getName()->willReturn("default")->shouldBeCalled(); $connection_prophecy->getPdo()->willReturn($pdo_prophecy->reveal())->shouldBeCalled(); - + // Emit event $event = new QueryExecuted( 'select * from users where created_at>?', @@ -276,7 +308,7 @@ function test_it_automatically_converts_query_bindings_to_strings() ); // imitate db request $app[Dispatcher::class]->fire($event); - - + + } } \ No newline at end of file diff --git a/tests/LaravelApp.php b/tests/LaravelApp.php index 8dade52..7d15738 100644 --- a/tests/LaravelApp.php +++ b/tests/LaravelApp.php @@ -3,37 +3,61 @@ * @author Dmitriy Lezhnev */ -namespace HttpAnalyzerTest; +namespace Apideveloper\Laravel\Tests; -use HttpAnalyzer\Laravel\HttpAnalyzerServiceProvider; +use Apideveloper\Laravel\Laravel\ApideveloperioServiceProvider; use Illuminate\Config\Repository; -use Illuminate\Contracts\Logging\Log; use Orchestra\Testbench\TestCase; abstract class LaravelApp extends TestCase { protected function getPackageProviders($app) { - return [HttpAnalyzerServiceProvider::class]; + return [ApideveloperioServiceProvider::class]; } - + + protected function getEnvironmentSetUp($app) + { + /** @var Repository $config */ + $config = app()[Repository::class]; + + $config->set('apideveloperio_logs.httplog.tmp_storage_path', $this->getTmpPath(__LINE__)); + $config->set('apideveloperio_logs.textlog.tmp_storage_path', $this->getTmpPath(__LINE__)); + } + public function createApplication() { $app = parent::createApplication(); - + // Drop config value to allow testing $config = $app[Repository::class]; - $config->set('http_analyzer.filtering.ignore_environment', []); - + $config->set('apideveloperio_logs.httplog.filtering.ignore_environment', []); + $config->set('apideveloperio_logs.textlog.filtering.ignore_environment', []); + + // This is to speed up test execution + $app['hash']->setRounds(4); + return $app; } - + + public function tearDown() + { + `rm -rf {$this->getTmpDir()}*`; + parent::tearDown(); + } + + protected function getTmpPath($suffix = '') { - $path = __DIR__ . "/tmp/" . time() . "_" . $suffix; - mkdir($path); - + $path = $this->getTmpDir() . md5(microtime()) . "_" . $suffix; + mkdir($path, 0777, true); + return $path; } - + + protected function getTmpDir() + { + return __DIR__ . "/tmp/"; + } + } \ No newline at end of file diff --git a/tests/Text/ExceptionFormatterTest.php b/tests/Text/ExceptionFormatterTest.php new file mode 100644 index 0000000..88f87df --- /dev/null +++ b/tests/Text/ExceptionFormatterTest.php @@ -0,0 +1,28 @@ + + * Date: 25/01/2018 + */ + +namespace Apideveloper\Laravel\Laravel\Text; + +use PHPUnit\Framework\TestCase; + +class ExceptionFormatterTest extends TestCase +{ + function test_it_transforms_exception() + { + $p = new \DomainException("message_prev", 200); + $e = new \Exception("message", 100, $p); + $array = ExceptionFormatter::fromException($e)->toArray(); + + + $this->assertCount(2, $array); + $this->assertEquals($array[0]['message'], $e->getMessage()); + $this->assertEquals($array[0]['code'], $e->getCode()); + $this->assertEquals($array[0]['class'], get_class($e)); + $this->assertEquals($array[1]['message'], $p->getMessage()); + $this->assertEquals($array[1]['code'], $p->getCode()); + $this->assertEquals($array[1]['class'], get_class($p)); + } +} diff --git a/tests/Text/TextLogTest.php b/tests/Text/TextLogTest.php new file mode 100644 index 0000000..01b664b --- /dev/null +++ b/tests/Text/TextLogTest.php @@ -0,0 +1,162 @@ + + * Date: 22/01/2018 + */ + +namespace Apideveloper\Laravel\Tests\Text; + + +use Apideveloper\Laravel\Laravel\Text\EventListener; +use Apideveloper\Laravel\Laravel\Text\ExceptionFormatter; +use Apideveloper\Laravel\Tests\LaravelApp; +use Carbon\Carbon; +use Illuminate\Config\Repository; +use Illuminate\Foundation\Exceptions\Handler; +use Psr\Log\LoggerInterface; + +class TextLogTest extends LaravelApp +{ + function test_it_removes_duplicates() + { + Carbon::setTestNow(); + $app = $this->createApplication(); + $config = app()[Repository::class]; + $tmp_storage_path = $config->get('apideveloperio_logs.textlog.tmp_storage_path'); + $listener = $app[EventListener::class]; + + + // 0. No duplicates removed by default + $app[LoggerInterface::class]->alert("some message", ["some" => "value"]); + $app[LoggerInterface::class]->alert("some message", ["some" => "value"]); + $listener->flush(); // imitate end of life + + $this->assertFileExists($tmp_storage_path . "/buffered_text_logs"); + $file_contents = file_get_contents($tmp_storage_path . "/buffered_text_logs"); + $json_decoded = json_decode("[" . trim($file_contents, ",") . "]", true); + + $this->assertEquals([ + [ + "level" => "alert", + "message" => "some message", + "context" => ["some" => "value"], + "date" => Carbon::now()->toIso8601String(), + ], + [ + "level" => "alert", + "message" => "some message", + "context" => ["some" => "value"], + "date" => Carbon::now()->toIso8601String(), + ], + ], $json_decoded[0]['messages']); + unlink($tmp_storage_path . "/buffered_text_logs");//trash logged data + + // 1. Only sequential duplicates are removed + $config->set('apideveloperio_logs.textlog.filtering.remove_duplicates', true); + $app[LoggerInterface::class]->alert("some message", ["some" => "value"]); + $app[LoggerInterface::class]->alert("some message", ["some" => "value"]); + $app[LoggerInterface::class]->info("some message", ["some" => "value"]); + $app[LoggerInterface::class]->alert("some message", ["some" => "value"]); + $listener->flush(); // imitate end of life + + $this->assertFileExists($tmp_storage_path . "/buffered_text_logs"); + $file_contents = file_get_contents($tmp_storage_path . "/buffered_text_logs"); + $json_decoded = json_decode("[" . trim($file_contents, ",") . "]", true); + + $this->assertEquals([ + [ + "level" => "alert", + "message" => "some message", + "context" => ["some" => "value"], + "date" => Carbon::now()->toIso8601String(), + ], + [ + "level" => "info", + "message" => "some message", + "context" => ["some" => "value"], + "date" => Carbon::now()->toIso8601String(), + ], + [ + "level" => "alert", + "message" => "some message", + "context" => ["some" => "value"], + "date" => Carbon::now()->toIso8601String(), + ], + ], $json_decoded[0]['messages']); + } + + function test_it_catches_and_dumps_log_entries() + { + Carbon::setTestNow(); + $app = $this->createApplication(); + $tmp_storage_path = $app[Repository::class]->get('apideveloperio_logs.textlog.tmp_storage_path'); + $listener = $app[EventListener::class]; + + $messages = ["ddd fff", "ztt rre"]; + foreach ($messages as $i => $message) { + $app[LoggerInterface::class]->alert($message, ["message_index" => $i]); + } + + // Check written logs + $listener->flush(); // imitate end of life + $this->assertFileExists($tmp_storage_path . "/buffered_text_logs"); + $file_contents = file_get_contents($tmp_storage_path . "/buffered_text_logs"); + $json_decoded = json_decode("[" . trim($file_contents, ",") . "]", true); + + $this->assertEquals([ + [ + "level" => "alert", + "message" => $messages[0], + "context" => [ + "message_index" => 0, + ], + "date" => Carbon::now()->toIso8601String(), + ], + [ + "level" => "alert", + "message" => $messages[1], + "context" => [ + "message_index" => 1, + ], + "date" => Carbon::now()->toIso8601String(), + ], + ], $json_decoded[0]['messages']); + + } + + function test_it_logs_exceptions_in_separated_json_structure() + { + $app = $this->createApplication(); + $tmp_storage_path = $app[Repository::class]->get('apideveloperio_logs.textlog.tmp_storage_path'); + $listener = $app[EventListener::class]; + + $previous_exception = new \DomainException("Inner exception", 99); + $e = new \Exception("Outer exception", 100, $previous_exception); + $app[Handler::class]->report($e); + + // Check data + $listener->flush(); // imitate end of life + $file_contents = file_get_contents($tmp_storage_path . "/buffered_text_logs"); + $json_decoded = json_decode("[" . trim($file_contents, ",") . "]", true); + + $this->assertCount(1, $json_decoded); + $this->assertCount(1, $json_decoded[0]['messages']); + $this->assertCount(2, $json_decoded[0]['messages'][0]['exception']); + $this->assertEquals(ExceptionFormatter::fromException($e)->toArray(), + $json_decoded[0]['messages'][0]['exception']); + $this->assertArrayNotHasKey('message', $json_decoded[0]['messages'][0]); + + } + + function test_it_skip_logging_if_disabled() + { + $app = $this->createApplication(); + $tmp_storage_path = $app[Repository::class]->get('apideveloperio_logs.textlog.tmp_storage_path'); + $listener = $app[EventListener::class]; + $app[Repository::class]->set('apideveloperio_logs.textlog.enabled', false); + $app[LoggerInterface::class]->alert("message sent"); + + $listener->flush(); // imitate end of life + $this->assertFileNotExists($tmp_storage_path . "/buffered_text_logs"); + } +} \ No newline at end of file diff --git a/tests/tmp/.gitkeep b/tests/tmp/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/tests/tmp/1499687042_182/recorded_requests b/tests/tmp/1499687042_182/recorded_requests deleted file mode 100644 index 64e523d..0000000 --- a/tests/tmp/1499687042_182/recorded_requests +++ /dev/null @@ -1 +0,0 @@ -{"full_url":"http:\/\/localhost\/auth\/signup","http_method":"options","user_ip":"127.0.0.1","timestamp":1499687042,"user_agent":"Symfony\/3.X","http_request_headers":[{"name":"host","value":"localhost"},{"name":"user-agent","value":"Symfony\/3.X"},{"name":"accept","value":"text\/html,application\/xhtml+xml,application\/xml;q=0.9,*\/*;q=0.8"},{"name":"accept-language","value":"en-us,en;q=0.5"},{"name":"accept-charset","value":"ISO-8859-1,utf-8;q=0.7,*;q=0.7"}],"http_request_body":"","http_response_code":200,"http_response_body":"","http_response_headers":[{"name":"cache-control","value":"no-cache"}],"server":{"hostname":"lezhnev-3.local","ip":null},"ttr_ms":1}, \ No newline at end of file diff --git a/tests/tmp/1500018528_203/recorded_requests b/tests/tmp/1500018528_203/recorded_requests deleted file mode 100644 index 250f9f9..0000000 --- a/tests/tmp/1500018528_203/recorded_requests +++ /dev/null @@ -1 +0,0 @@ -{"full_url":"http:\/\/localhost\/auth\/signup","http_method":"get","user_ip":"127.0.0.1","timestamp":1500018528,"user_agent":"Symfony\/3.X","http_request_headers":[{"name":"host","value":"localhost"},{"name":"user-agent","value":"Symfony\/3.X"},{"name":"accept","value":"text\/html,application\/xhtml+xml,application\/xml;q=0.9,*\/*;q=0.8"},{"name":"accept-language","value":"en-us,en;q=0.5"},{"name":"accept-charset","value":"ISO-8859-1,utf-8;q=0.7,*;q=0.7"}],"http_request_body":"","http_response_code":200,"http_response_body":"","http_response_headers":[{"name":"cache-control","value":"no-cache"}],"server":{"hostname":"lezhnev-3.local","ip":null},"ttr_ms":1,"external_queries":[]}, \ No newline at end of file diff --git a/tests/tmp/1500290545_99/recorded_requests b/tests/tmp/1500290545_99/recorded_requests deleted file mode 100644 index 2ad323c..0000000 --- a/tests/tmp/1500290545_99/recorded_requests +++ /dev/null @@ -1 +0,0 @@ -{"full_url":"http:\/\/localhost\/api\/signup?b=12","http_method":"post","user_ip":"127.0.0.1","timestamp":1500290545,"user_agent":"Symfony\/3.X","http_request_headers":[{"name":"host","value":"localhost"},{"name":"user-agent","value":"Symfony\/3.X"},{"name":"accept","value":"text\/html,application\/xhtml+xml,application\/xml;q=0.9,*\/*;q=0.8"},{"name":"accept-language","value":"en-us,en;q=0.5"},{"name":"accept-charset","value":"ISO-8859-1,utf-8;q=0.7,*;q=0.7"},{"name":"content-type","value":"application\/x-www-form-urlencoded"}],"http_request_body":"","http_response_code":200,"http_response_body":"","http_response_headers":[{"name":"cache-control","value":"no-cache, private"},{"name":"date","value":"Mon, 17 Jul 2017 11:22:25 GMT"}],"server":{"hostname":"lezhnev-3.local","ip":null},"ttr_ms":1,"external_queries":[]}, \ No newline at end of file