├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Gemfile ├── Gruntfile.js ├── Guardfile ├── LICENSE ├── README.md ├── composer.json ├── composer.lock ├── package.json ├── php_5.3.ini ├── php_5.5.ini ├── php_7.ini ├── phpmd.xml ├── phpunit.xml.dist ├── src └── Aptoma │ ├── Cache │ └── SerializingPredisCache.php │ ├── Ftp │ ├── Exception │ │ ├── FtpException.php │ │ └── VerifySizeException.php │ └── Ftp.php │ ├── Guzzle │ └── Plugin │ │ ├── HttpCallInterceptor │ │ ├── Exception │ │ │ └── HttpCallToBackendException.php │ │ └── HttpCallInterceptorPlugin.php │ │ ├── RequestLogger │ │ └── RequestLoggerPlugin.php │ │ ├── RequestPreSendLogger │ │ └── RequestBeforeSendLoggerPlugin.php │ │ └── RequestToken │ │ └── RequestTokenPlugin.php │ ├── JsonErrorHandler.php │ ├── Log │ ├── ExtraContextProcessor.php │ └── RequestProcessor.php │ ├── Security │ ├── Authentication │ │ └── Token │ │ │ └── ApiKeyToken.php │ ├── Encoder │ │ └── SaltLessPasswordEncoderInterface.php │ ├── Http │ │ └── Firewall │ │ │ └── ApiKeyAuthenticationListener.php │ ├── Provider │ │ └── ApiKeyAuthenticationProvider.php │ └── User │ │ └── ApiKeyUserProviderInterface.php │ ├── Silex │ ├── Application.php │ └── Provider │ │ ├── ApiKeyServiceProvider.php │ │ ├── CacheServiceProvider.php │ │ ├── ConsoleLoggerServiceProvider.php │ │ ├── ExtendedLoggerServiceProvider.php │ │ ├── GuzzleServiceProvider.php │ │ ├── MemcachedServiceProvider.php │ │ ├── PredisClientServiceProvider.php │ │ ├── StorageServiceProvider.php │ │ └── UrlGeneratorServiceProvider.php │ ├── Storage │ ├── Exception │ │ ├── FileNotFoundException.php │ │ └── StorageException.php │ ├── FileStorage.php │ └── StorageInterface.php │ └── TestToolkit │ ├── BaseWebTestCase.php │ └── TestClient.php └── tests └── src └── Aptoma ├── Cache └── SerializingPredisCacheTest.php ├── JsonErrorHandlerTest.php ├── Log ├── ExtraContextProcessorTest.php └── RequestProcessorTest.php ├── Security └── Authentication │ ├── Http │ └── Firewall │ │ └── ApiKeyAuthenticationListenerTest.php │ ├── Provider │ └── ApiKeyAuthenticationProviderTest.php │ └── Token │ └── ApiKeyTokenTest.php ├── Silex ├── ApplicationTest.php ├── Mocks │ ├── AppExtension.php │ └── Application.php └── Provider │ └── ExtendedLoggerServiceProviderTest.php ├── Storage └── FileStorageTest.php ├── TestToolkit ├── BaseWebTestCaseTest.php ├── TestClientTest.php └── mocks │ ├── Application.php │ └── app.php └── fixtures └── topgun.jpg /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | node_modules 3 | vendor 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # Lint: http://lint.travis-ci.org 2 | 3 | notifications: 4 | email: 5 | - gunnar@aptoma.com 6 | 7 | language: php 8 | 9 | php: 10 | - 7.0 11 | 12 | services: 13 | - redis-server 14 | 15 | cache: 16 | directories: 17 | - vendor 18 | - node_modules 19 | env: 20 | global: 21 | # travis encrypt CODECLIMATE_REPO_TOKEN= 22 | - secure: "R5FU+atovj/XTxz890KBiDkF1IwpOCgkcjpAsyL/apijPjH4Jn0m/aqF/9JBSKgBPr0lF/dA/p5gROotYERDooKZxUkRa25oFGLTYki1XlFvampk5/ibCI51w/WQLwSCGFqqy7CFyG10YhwQ64wUcazOCDOKteiyxg/+y9/jwow=" 23 | 24 | git: 25 | submodules: false 26 | 27 | before_install: 28 | 29 | install: 30 | - npm install 31 | before_script: 32 | - if [ -f php_$TRAVIS_PHP_VERSION.ini ]; then phpenv config-add php_$TRAVIS_PHP_VERSION.ini; fi 33 | - mkdir -p build/logs 34 | 35 | script: 36 | - ./node_modules/grunt-cli/bin/grunt travis 37 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | CHANGELOG 2 | ========= 3 | 4 | 3.0.0 5 | ----- 6 | 7 | * Changed: Require ~2.8 version of Symfony components 8 | * Added: `ApiKeyAuthenticationListener` now also looks for apikey in Authorization header (`Authorization: apikey supersecretkey`) 9 | 10 | 2.0.7 11 | ----- 12 | 13 | * Bugfix: Catch error when trying to retrieve client IP for logging purposes 14 | 15 | 2.0.6 16 | ----- 17 | 18 | * Improved: Add options for persistent connections and timeout to PredisClientServiceProvider. 19 | 20 | 2.0.3 21 | ----- 22 | 23 | * Improved: Caching is updated to expose both serializing and nonserializing Predis storage, to accomodate Guzzle. 24 | 25 | 2.0.2 26 | ----- 27 | 28 | * Improved: `PredisCache` now handles both serialized and unserialized data on fetch 29 | 30 | 2.0.1 31 | ----- 32 | 33 | * Added: `MonologGuzzleLogAdapter` now add a context entry for responseTime 34 | * Improved: `PredisCache` now handles serialization 35 | 36 | 2.0.0 37 | ----- 38 | 39 | * Added: `CacheServiceProvider` for exposing `Doctrine\Cache` compatible cache implemenations 40 | * Added: `PredisClientServiceProvider` for exposing a `Predis\Client` service. 41 | * Improved: Guzzle cache plugin now reads cache implementation from `$app['cache']`, so you can configure it to use something other than Memcached. 42 | * BC: The `cache` service is now moved to `CacheServiceProvider`. It still returns Memcached by default, but you need to register the CacheServiceProvider to make it available. 43 | 44 | 1.6.0 45 | ----- 46 | 47 | * Added: `JsonErrorHandler` now supports an extra argumenent to enable handling of errors regardless of accept header 48 | 49 | 1.5.1 50 | ----- 51 | 52 | * Enhancement: Add passive mode support to `Ftp` 53 | 54 | 1.5.0 55 | ----- 56 | 57 | * New: Add `RequestBeforeSendLoggerPlugin` for logging dispatch of Guzzle requests. 58 | * Enhancement: Add event context entry `MonologGuzzleLogAdapter`. 59 | 60 | 1.4.0 61 | ----- 62 | 63 | * BC: `SingleLineFormatter` for Monolog is removed. Use Monolog's `LineFormatter@~1.8.0` instead. 64 | * Enhancement: Default `ExtendLoggerServiceProvider` monolog formatter now includes microseconds. 65 | 66 | 1.3.2 67 | ----- 68 | 69 | * Bugfix: RemoteRequestToken is cached in the processor, so it's available for all later log entries 70 | 71 | 1.3.1 72 | ----- 73 | 74 | * Enhancement: RequestProcessor now adds remoteRequestToken if available. 75 | 76 | 1.3.0 77 | ----- 78 | 79 | * New: Add `RequestTokenPlugin` for adding and forwarding request token headers. 80 | * BC: `RequestTokenPlugin` needs `RequestStack` introduced in Symfony 2.4 81 | 82 | 1.2.3 83 | ----- 84 | 85 | * Bugfix: Make 1.2.2 compatible with stable silex branch. 86 | 87 | 1.2.2 88 | ----- 89 | 90 | * New: Add `SingleLineFormatter` for ensuring log entries don't span multiple lines 91 | * Enhancement: `ExtendedLoggerServiceProvider` now uses SingleLineFormatter, unless you override `monolog.formatter` 92 | 93 | 1.2.1 94 | ----- 95 | 96 | Add license to prepare for Packagist release. 97 | 98 | 1.2.0 99 | ----- 100 | 101 | * New: Add `MemcachedServiceProvider` 102 | * New: Add `GuzzleServiceProvider` and related helper classes for logging and tests 103 | 104 | 1.1.3 105 | ----- 106 | 107 | * Enhancement: Add `PsrLogMessageProcessor` when using `ExtendedLoggerServiceProvider` 108 | 109 | 1.1.2 110 | ----- 111 | 112 | * Enhancement: JsonErrorHandler now handles all exception types, not just HttpExpcetions 113 | 114 | 1.1.1 115 | ----- 116 | 117 | * New: Allow overriding logfile used by console logger 118 | 119 | 1.1.0 120 | ----- 121 | 122 | * New: ConsoleLoggerServiceProvider - Allows piping Monolog logging to stdout when running console commands 123 | 124 | 1.0.0 125 | ----- 126 | 127 | This release encompasses all other stuff since the first commit. 128 | 129 | * New: Storage lib is introduced 130 | * New: Base test classes 131 | * New: Base Application 132 | * New: JsonErrorHandler 133 | * New: ApiKey component 134 | 135 | 0.0.1 136 | ----- 137 | * Initial release. 138 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributing 2 | ============ 3 | 4 | A key feature of this repo is to not have to repeat boilerplate code across projects. 5 | That means that it is a bit intrusive in setting up logging, error handlers and services, 6 | which may or may not fit everyone’s needs. We are open for modifications that allow 7 | overriding and extending behavior, but we are not likely to accept changes that will 8 | require changing existing projects depending on this package. However, feel free to 9 | voice any suggestions, and we might very well end up with a more flexible design. 10 | 11 | Any contributions should follow the principles defined in [SOFT MANDEL](https://github.com/aptoma/softmandel). 12 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # Install Bundler using: 2 | # `gem install bundler` 3 | # 4 | # Then use `bundle install` to install gem dependencies. 5 | # 6 | 7 | source "https://rubygems.org" 8 | 9 | group :guard do 10 | gem "guard", "~>1.5" 11 | gem "guard-phpunit", "~>0.1.4" 12 | gem "guard-phpmd", "~>0.0.2" 13 | gem "guard-phpcs" 14 | gem "guard-shell", "~>0.5.1" 15 | gem "rb-fsevent", "~>0.9.2" 16 | gem "growl", "~>1.0" 17 | end 18 | 19 | 20 | group :development do 21 | gem 'rb-readline' 22 | end 23 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Grunt configuration - http://gruntjs.com 3 | */ 4 | 5 | 6 | module.exports = function (grunt) { 7 | 'use strict'; 8 | 9 | require('time-grunt')(grunt); 10 | 11 | // Project configuration. 12 | grunt.initConfig({ 13 | pkg: grunt.file.readJSON('package.json'), 14 | 15 | // files to be used (minimatch syntax) - https://github.com/isaacs/minimatch 16 | files: { 17 | phplint: [ 18 | 'src/*.php', 19 | 'tests/*.php' 20 | ] 21 | }, 22 | 23 | dirs: { 24 | phpcs: [ 25 | 'src', 26 | 'tests' 27 | ], 28 | phpmd: [ 29 | 'src', 30 | 'tests' 31 | ] 32 | }, 33 | 34 | phplint: { 35 | files: { 36 | src: '<%= files.phplint %>' 37 | } 38 | }, 39 | 40 | // https://github.com/jharding/grunt-exec 41 | exec: { 42 | 43 | // https://github.com/sebastianbergmann/phpunit/ 44 | 'phpunit': { 45 | cmd: 'vendor/bin/phpunit -c phpunit.xml.dist' 46 | }, 47 | 48 | 'phpunit-ci': { 49 | cmd: 'vendor/bin/phpunit -c phpunit.xml.dist ' + 50 | '--coverage-html build/coverage ' + 51 | '--coverage-clover build/logs/clover.xml ' + 52 | '--log-junit build/logs/junit.xml' 53 | }, 54 | 55 | 'phpunit-travis': { 56 | cmd: 'vendor/bin/phpunit --coverage-clover build/logs/clover.xml' 57 | }, 58 | 59 | // http://www.squizlabs.com/php-codesniffer 60 | 'phpcs': { 61 | cmd: function () { 62 | return 'mkdir -p build/reports && vendor/bin/phpcs --report=full --report=checkstyle --tab-width=4 --report-checkstyle=build/reports/checkstyle.xml ' + 63 | '--standard=PSR2 ' + grunt.config.data.dirs.phpcs.join(' '); 64 | } 65 | }, 66 | 67 | 'phpcs-travis': { 68 | cmd: function () { 69 | return 'vendor/bin/phpcs --standard=PSR2 --extensions=php ' + grunt.config.data.dirs.phpcs.join(' '); 70 | } 71 | }, 72 | 73 | 'phpmd': { 74 | cmd: function () { 75 | return 'vendor/bin/phpmd ' + grunt.config.data.dirs.phpmd.join(',') + ' text phpmd.xml --suffixes=php'; 76 | } 77 | }, 78 | 79 | 'phpmd-ci': { 80 | cmd: function () { 81 | return 'mkdir -p build/reports && vendor/bin/phpmd ' + grunt.config.data.dirs.phpmd.join(',') + ' xml phpmd.xml --suffixes=php --reportfile build/reports/phpmd.xml'; 82 | } 83 | }, 84 | 85 | 'composer-install': { 86 | cmd: 'composer install' 87 | }, 88 | 89 | 'npm-install': { 90 | cmd: 'npm install' 91 | }, 92 | 93 | 'bundle-install': { 94 | cmd: 'bundle install' 95 | } 96 | } 97 | } 98 | ) 99 | ; 100 | 101 | // Tasks from NPM 102 | grunt.loadNpmTasks('grunt-exec'); 103 | 104 | // Task aliases 105 | grunt.registerTask('phpunit', 'PHP Unittests', 'exec:phpunit'); 106 | grunt.registerTask('phpunit-ci', 'PHP Unittests for CI', 'exec:phpunit-ci'); 107 | grunt.registerTask('phpcs', 'PHP Codesniffer', 'exec:phpcs'); 108 | grunt.registerTask('phpmd', 'PHP Mess Detector', 'exec:phpmd'); 109 | grunt.registerTask('install', 'Install all project dependencies', ['exec:npm-install', 'exec:composer-install', 'exec:bundle-install']); 110 | grunt.registerTask('default', ['qa']); 111 | grunt.registerTask('qa', ['exec:composer-install', 'phpunit', 'phpcs', 'phpmd']); 112 | grunt.registerTask('travis', ['exec:composer-install', 'exec:phpunit-travis', 'exec:phpcs-travis', 'phpmd']); 113 | } 114 | ; 115 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | # Guard - https://github.com/guard/guard 2 | # 3 | # Install: 4 | # sudo gem install guard guard-phpunit guard-phpmd guard-shell rb-fsevent growl 5 | # 6 | # Usage: 7 | # guard -G tools/Guardfile 8 | # --------------------------------------------------------------- 9 | 10 | 11 | ## PHP ## 12 | guard 'phpunit', :tests_path => 'tests', :all_on_start => false, :all_after_pass => false, :cli => '--colors --verbose' do 13 | watch(%r{^(app/.+)\.php}) { |m| "tests/#{m[1]}Test.php" } 14 | watch(%r{^(src/.+)\.php}) { |m| "tests/#{m[1]}Test.php" } 15 | watch(%r{^tests/.*\.php$}) 16 | end 17 | 18 | guard 'phpmd', :rules => 'phpmd.xml' do 19 | watch(%r{^app/\w*\.php$}) 20 | watch(%r{^src/.*\.php$}) 21 | watch(%r{^tests/.*\.php$}) 22 | end 23 | 24 | guard 'phpcs', :standard => 'PSR2' do 25 | watch(%r{^app/\w*\.php$}) 26 | watch(%r{^src/.*\.php$}) 27 | watch(%r{^tests/.*\.php$}) 28 | end -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013-2014 Aptoma AS 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Silex Extras 2 | ============ 3 | 4 | [![Build Status](https://travis-ci.org/aptoma/silex-extras.svg)](https://travis-ci.org/aptoma/silex-extras) 5 | [![Coverage Status](https://img.shields.io/coveralls/aptoma/silex-extras.svg)](https://coveralls.io/r/aptoma/silex-extras) 6 | 7 | Package consisting of reusable stuff for Silex powered applications. 8 | 9 | It is collection of various services that will ease bootsrapping new Silex 10 | applications, keep best practices in sync across projects, and ensure we do 11 | stuff in a similar manner whenever we do something. 12 | 13 | ## What's In It? 14 | 15 | - Log processor for adding extra context to logs 16 | - ServiceProvider for rich logging in Logstash format 17 | - Base Application class for doing common stuff we always do in Silex applications 18 | - Error handler that outputs stuff as json if `Accept: application/json` 19 | - TestToolkit to bootstrap functional testing of Silex applications 20 | - API key user authentication 21 | - Storage interface, with local file and Level3 implementations 22 | - Ftp upload abstraction, basically a wrapper around native FTP functionality 23 | - Level3 upload service, for uploading stuff to Level3 24 | - ConsoleLoggerServiceProvider, for integrating the logger with the console 25 | - CacheServiceProvider, for Memcached and Redis implementations of Doctrin Cache (can also be used standalone) 26 | - GuzzleServiceProvider for extra Guzzle features 27 | - Guzzle HttpCallInterceptorPlugin for testing with Guzzle services 28 | 29 | ### Aptoma\Ftp 30 | 31 | Wraps native PHP FTP functions in an object oriented manner. It's designed for 32 | working with Level3 CDN, and thus has a concept of multiple paths for upload. 33 | 34 | Usage: 35 | 36 | ````PHP 37 | $ftp = new Ftp($hostname, $username, $password, $logger); 38 | $file = new File($pathToFile); 39 | 40 | $destination = 'path/on/server/with/filename.txt'; 41 | // All directories needs to be created before upload 42 | $ftp->preparePaths(array($destination)); 43 | $ftp->put($destination, $file->getRealPath()); 44 | ```` 45 | 46 | The class has a few more features for validating upload integrity and moving 47 | the file to publish location after upload. Read the source :) 48 | 49 | ### Aptoma\Service\Level3Service 50 | 51 | Provides an abstraction for uploading files to Level3. You need to provide a 52 | an `Ftp` instance and various paths, and can then simply do: 53 | `$service->upload($fileContents, $targetFileName, $relativeLocalTempDir);`. 54 | 55 | After upload, the file will be renamed and put in a folder matching it's checksum, 56 | in order to avoid duplicate uploads, and to deal with Level3's (sensible) limitation 57 | of max number of files in a directory. The full public url is returned. Strictly 58 | speaking, this isn't actually Level3 specific, but more a two-step strategy for 59 | validated uploads. 60 | 61 | There's also a bundled Level3ServiceProvider for simpler integration with Silex. 62 | 63 | ### Aptoma\Log 64 | 65 | This folder provides two classes: 66 | 67 | - `ExtraContextProcessor` for always adding a predefined set of extra fields to log entries 68 | - `RequestProcessor` for adding client ip, unique request token and username to all entries 69 | - `MonologGuzzleLogAdapater` for integrating Monolog with Guzzle, to log request times and errors 70 | 71 | Usage is simple: 72 | 73 | ````PHP 74 | use Monolog\Logger; 75 | use Aptoma\Log\RequestProcessor; 76 | 77 | $app = new \Silex\Application(...); 78 | 79 | $app['logger']->pushProcessor(new RequestProcessor($app)); 80 | $app['logger']->pushProcessor(new ExtraContextProcessor(array('service' => 'my-service', 'environment' => 'staging'))); 81 | ```` 82 | 83 | ### Aptoma\Silex\Provider\ExtendedLoggerServiceProvider 84 | 85 | This is a service provider that automatically adds the above mentioned `RequestProcessor`, 86 | as well as a LogstashFormatter if you have specified `monolog.logstashfile`. 87 | 88 | The LogstashFormatter can also add some extra context to each record: 89 | 90 | ````PHP 91 | $app['meta.service'] = 'drvideo-metadata-admin-gui'; // The name of the service, consult with AMP 92 | $app['meta.customer'] = 'Aptoma'; // The name of customer for this record 93 | $app['meta.environment'] = 'production'; // The environment of the current installation 94 | ```` 95 | 96 | These extra fields will help us classify records in our consolidated logging 97 | infrastructure (Loggly, Logstash and friends), and lead to great success. 98 | 99 | ### Aptoma\Silex\Provider\ConsoleLoggerServiceProvider 100 | 101 | This service provider makes it easy to show log messages from services in the console, 102 | without having to inject an instance of `OutputInterface` into the services. This 103 | requires version ~2.4 of Symfony Components. More info about the change is at the 104 | [Symfony Blog](http://symfony.com/blog/new-in-symfony-2-4-show-logs-in-console). 105 | 106 | In your console application, you can now do something like this: 107 | 108 | ````PHP 109 | use Symfony\Component\Console\Application; 110 | 111 | $app = require 'app.php'; 112 | $console = new Application('My Console Application', '1.0'); 113 | // You should only register this service provider when running commands 114 | $app->register(new \Aptoma\Silex\Provider\ConsoleLoggerServiceProvider()); 115 | 116 | $console->addCommands( 117 | array( 118 | //... 119 | ) 120 | ); 121 | 122 | $console->run($app['console.input'], $app['console.output']); 123 | ```` 124 | 125 | You will still use the normal `OutputInterface` instance for command feedback 126 | in your commands, but you will now also get output from anything your services 127 | are logging. 128 | 129 | The console logger overrides the default `monolog.handler` in order to allow setting 130 | a custom log file. If defined, it will use `monolog.console_logfile`, and if not, it 131 | will fall back to `monolog.logfile`. 132 | 133 | This Provider is also available as standalone package: https://github.com/glensc/ConsoleLoggerServiceProvider 134 | 135 | ### Aptoma\Silex\Application 136 | 137 | This is a base application you can extend. It will add a json formatter for errors, 138 | register `ServiceControllerServiceProvider` and `UrlGeneratorServiceProvider`, 139 | automatically log execution time for scripts (up until the response has been sent), 140 | and also exposes `registerTwig` and `registerLogger`, which you can use to set up 141 | those with one line of code. 142 | 143 | This class should include functionality that we _always_ use, meaning it's not 144 | a collection of "nice to haves". 145 | 146 | ### Aptoma\JsonErrorHandler 147 | 148 | This class simply formats exceptions as `JsonResponse`s, provided the client 149 | has sent an `Accept: application/json` header. It will be loaded automatically 150 | by the base `Application` class mentioned above, or it can be registered manually: 151 | 152 | ````PHP 153 | $jsonErrorHandler = new Aptoma\JsonErrorHandler($app); 154 | $app->error(array($jsonErrorHandler, 'handle')); 155 | ```` 156 | 157 | ### Aptoma\TestToolkit 158 | 159 | This includes a BaseWebTestCase you can use to bootstrap your test, and an 160 | associated `TestClient` with shortcuts for `postJson($url, $data)` and 161 | `putJson($url, $data)`. 162 | 163 | To use it, you need to have your tests extend it, and probably also add the 164 | path to your bootstrap file: 165 | 166 | ````PHP 167 | class MyObjectTest extends TestToolkit\BaseWebTestCase 168 | { 169 | public function setUp() 170 | { 171 | $this->pathToAppBootstrap = __DIR__.'/../../app/app.php'; 172 | parent::setUp(); 173 | } 174 | } 175 | ```` 176 | 177 | ### Aptoma\Security 178 | 179 | Component for API key user authentication. 180 | 181 | All it requires is a UserProvider and an encoder to encode the API key. 182 | It'll typically be used in your app like this: 183 | 184 | ````PHP 185 | $app->register( 186 | new Aptoma\Silex\Provider\ApiKeyServiceProvider(), 187 | array( 188 | 'api_key.user_provider' => new App\Specific\UserProvider(), 189 | 'api_key.encoder' => new App\Specific\Encoder() 190 | ) 191 | ); 192 | ```` 193 | 194 | It can then be attached to any firewall of your choice: 195 | 196 | ````PHP 197 | $app->register( 198 | new Silex\Provider\SecurityServiceProvider(), 199 | array( 200 | 'security.firewalls' => array( 201 | // ... 202 | 'secured' => array( 203 | 'pattern' => '^.*$', 204 | 'api_key' => true 205 | // more settings... 206 | ) 207 | ) 208 | ) 209 | ); 210 | ```` 211 | 212 | ### CacheServiceProvider 213 | 214 | Registers services for `cache.memcached` and `cache.predis`, as well as a generic 215 | `cache`, which can be configured to return either of these (Memcached by default). 216 | 217 | ````PHP 218 | 219 | $app->register(new Aptoma\Silex\Provider\MemcachedServicerProvider()); 220 | $app->register(new Aptoma\Silex\Provider\CacheServicerProvider()); 221 | 222 | $app['cache']->save('mykey', 'myvalue'); 223 | 224 | ```` 225 | 226 | See below for config options for Memcached. 227 | 228 | ### MemcachedServiceProvider 229 | 230 | Registers Memcached as a service, and takes care of prefixes and persistent connections. 231 | It returns an instance of \Memcached. 232 | 233 | ````PHP 234 | 235 | $app['memcached.identifier'] = 'my_app'; 236 | $app['memcached.prefix'] = 'ma_'; 237 | $app['memcached.servers'] = array( 238 | array('host' => '127.0.0.1', 'port' => 11211), 239 | ); 240 | 241 | $app->register(new Aptoma\Silex\Provider\MemcachedServicerProvider()); 242 | 243 | $app['memcached']->set('mykey', 'myvalue'); 244 | 245 | ```` 246 | 247 | ### PredisServiceProvider 248 | 249 | Registers Predis as a service. It returns an instance of \Predis\Client. 250 | 251 | ````PHP 252 | 253 | $app['redis.host'] = '127.0.0.1'; 254 | $app['redis.port'] = 6379; 255 | $app['redis.prefix'] = 'prefix::'; 256 | $app['redis.database'] = 0; 257 | 258 | $app->register(new Aptoma\Silex\Provider\PredisClientServicerProvider()); 259 | 260 | $app['predis.client']->set('mykey', 'myvalue'); 261 | 262 | ```` 263 | 264 | ### GuzzleServiceProvider 265 | 266 | Extends the base GuzzleServiceProvider to allow registering global plugins, and also 267 | adds a few plugins: 268 | 269 | - generic logging of each request 270 | - logging of total requests 271 | - adding of request token header to outgoing requests 272 | - cache plugin for HTTP based caching 273 | 274 | To configure which cache storage to use, define `$app['guzzle.default_cache']`, 275 | which should be a string referencing the cache service to use, ie. 'cache.memcached'. 276 | 277 | ````PHP 278 | $app->register(new GuzzleServiceProvider(), array('guzzle.services' => array())); 279 | $app->finish(array($app['guzzle.request_logger_plugin'], 'writeLog')); 280 | 281 | $app['guzzle.plugins'] = $app->share( 282 | function () use ($app) { 283 | return array( 284 | $app['guzzle.log_plugin'], 285 | $app['guzzle.request_logger_plugin'], 286 | $app['guzzle.request_token_plugin'], 287 | $app['guzzle.cache_plugin'], 288 | ); 289 | } 290 | ); 291 | ```` 292 | 293 | ### Guzzle HttpCallInterceptorPlugin 294 | 295 | Guzzle plugin for use in unit testing to ensure there are no calls made to any 296 | external services. In your test setup, do something like this: 297 | 298 | ````PHP 299 | 300 | $this->app['guzzle.plugins'] = $this->app->share( 301 | $this->app->extend( 302 | 'guzzle.plugins', 303 | function (array $plugins, $app) { 304 | $plugins[] = new HttpCallInterceptorPlugin($app['logger']); 305 | 306 | return $plugins; 307 | } 308 | ) 309 | ); 310 | 311 | // Intercept errors and fail tests, if you don't do this, you'll most often get 312 | // a rather cryptic error message 313 | $this->app->error( 314 | function (HttpCallToBackendException $e) use ($testCase) { 315 | $testCase->fail($e->getMessage()); 316 | }, 317 | 10 318 | ); 319 | 320 | $this->app->error( 321 | function (BatchTransferException $e) use ($testCase) { 322 | $testCase->fail($e->getMessage()); 323 | }, 324 | 10 325 | ); 326 | ```` 327 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aptoma/silex-extras", 3 | "type": "library", 4 | "description": "Collection of common stuff for Silex powered applications", 5 | "keywords": ["silex"], 6 | "homepage": "https://github.com/aptoma/silex-extras", 7 | "license": "MIT", 8 | "autoload": { 9 | "psr-0": { 10 | "Aptoma": "src/" 11 | } 12 | }, 13 | "require": { 14 | "monolog/monolog": "^1.23", 15 | "symfony/http-foundation": "^3.3", 16 | "symfony/http-kernel": "~3.3", 17 | "symfony/browser-kit": "^3.3", 18 | "symfony/css-selector": "^3.3", 19 | "symfony/monolog-bridge": "^3.3", 20 | "symfony/security": "^3.3", 21 | "symfony/finder": "^3.3", 22 | "symfony/filesystem": "^3.3", 23 | "silex/silex": "^2.2", 24 | "pimple/pimple": "^3.2", 25 | "doctrine/cache": "^1.6" 26 | }, 27 | "suggest": { 28 | "ext-ftp": "Allows using Ftp services.", 29 | "guzzlehttp/guzzle": "In order to use Guzzle service provider", 30 | "doctrine/cache": "In order to use Doctrine\\Cache compatible Memcached", 31 | "predis/predis": "In order to use Redis cache with Doctrine" 32 | }, 33 | "require-dev": { 34 | "phpmd/phpmd": "^2.6", 35 | "twig/twig": "^2.4", 36 | "squizlabs/php_codesniffer": "^3.1", 37 | "guzzlehttp/guzzle": "^6.3", 38 | "pdepend/pdepend": "^2.5", 39 | "codeclimate/php-test-reporter": "^0.4.4", 40 | "predis/predis": "^1.1", 41 | "phpunit/phpunit": "^6.3" 42 | }, 43 | "minimum-stability": "stable", 44 | "authors": [ 45 | { 46 | "name": "Gunnar Lium", 47 | "email": "gunnar@aptoma.com", 48 | "homepage": "https://github.com/gunnarlium", 49 | "role": "Developer" 50 | } 51 | ] 52 | } 53 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "silex-extras", 3 | "repository": "https://github.com/aptoma/silex-extras.git", 4 | "version": "2.1.0", 5 | "engines": { 6 | "node": ">=0.8.0" 7 | }, 8 | "devDependencies": { 9 | "grunt": "~0.4.0", 10 | "grunt-cli": "~0.1", 11 | "grunt-exec": "~0.4", 12 | "time-grunt": "^0.4.0" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /php_5.3.ini: -------------------------------------------------------------------------------- 1 | extension="memcached.so" 2 | extension="apc.so" 3 | extension="redis.so" 4 | -------------------------------------------------------------------------------- /php_5.5.ini: -------------------------------------------------------------------------------- 1 | extension="memcached.so" 2 | extension="redis.so" 3 | -------------------------------------------------------------------------------- /php_7.ini: -------------------------------------------------------------------------------- 1 | extension="memcached.so" 2 | extension="redis.so" 3 | -------------------------------------------------------------------------------- /phpmd.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | Aptoma PHP Mess detector rules 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 15 | ./tests/ 16 | 17 | 18 | 19 | 20 | vendor 21 | tests 22 | 23 | 24 | src 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/Aptoma/Cache/SerializingPredisCache.php: -------------------------------------------------------------------------------- 1 | client = $client; 19 | } 20 | 21 | protected function doFetch($id) 22 | { 23 | $value = $this->client->get($id); 24 | 25 | if (null === $value) { 26 | return false; 27 | } 28 | 29 | if (!is_string($value)) { 30 | return $value; 31 | } 32 | 33 | try { 34 | return unserialize($value); 35 | } catch (\Exception $e) { 36 | return $value; 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Aptoma/Ftp/Exception/FtpException.php: -------------------------------------------------------------------------------- 1 | hostname = $hostname; 29 | $this->username = $username; 30 | $this->password = $password; 31 | $this->logger = $logger; 32 | } 33 | 34 | /** 35 | * @param bool $state 36 | * @return bool 37 | */ 38 | public function setPassiveMode($state) 39 | { 40 | return ftp_pasv($this->getConnection(), $state); 41 | } 42 | 43 | /** 44 | * @param $path 45 | */ 46 | public function mkdir($path) 47 | { 48 | $this->logger->info('mkdir ' . $path); 49 | $parts = explode('/', $path); 50 | 51 | $mkTemp = ''; 52 | foreach ($parts as $dir) { 53 | if ($dir == '') { 54 | continue; 55 | } 56 | $mkTemp .= '/' . $dir; 57 | $this->logger->debug('mkdir: ' . $mkTemp); 58 | if (@ftp_mkdir($this->getConnection(), $mkTemp)) { 59 | $this->logger->debug('mkdir success'); 60 | } 61 | } 62 | } 63 | 64 | /** 65 | * @param $target 66 | * @param $origin 67 | * @param int $mode 68 | * @return bool 69 | */ 70 | public function put($target, $origin, $mode = FTP_BINARY) 71 | { 72 | $this->logger->debug(sprintf('FTP: put from %s to %s', $origin, $target)); 73 | return ftp_put($this->getConnection(), $target, $origin, $mode); 74 | } 75 | 76 | /** 77 | * @param $source 78 | * @param $target 79 | * @return bool 80 | */ 81 | public function move($source, $target) 82 | { 83 | $this->logger->debug(sprintf('FTP: move from %s to %s', $source, $target)); 84 | return ftp_rename($this->getConnection(), $source, $target); 85 | } 86 | 87 | /** 88 | * @param $path 89 | * @return bool 90 | */ 91 | public function delete($path) 92 | { 93 | $this->logger->debug('FTP: delete ' . $path); 94 | return ftp_delete($this->getConnection(), $path); 95 | } 96 | 97 | /** 98 | * @param array $paths 99 | */ 100 | public function preparePaths(array $paths) 101 | { 102 | foreach ($paths as $path) { 103 | $this->logger->debug('FTP: preparePath ' . $path); 104 | $this->mkdir(dirname($path)); 105 | } 106 | } 107 | 108 | /** 109 | * @param $file 110 | * @return int 111 | */ 112 | public function getSize($file) 113 | { 114 | return ftp_size($this->getConnection(), $file); 115 | } 116 | 117 | /** 118 | * @param $originSize 119 | * @param $target 120 | * @param null $tempTarget 121 | * @return bool 122 | */ 123 | public function checkIfAlreadyUploaded($originSize, $target, $tempTarget = null) 124 | { 125 | $this->logger->debug('FTP: check if already uploaded', array($originSize, $target, $tempTarget)); 126 | $targetSize = $this->getSize($target); 127 | $this->logger->debug('Public size: ' . $targetSize); 128 | $this->logger->debug('Origin size: ' . $originSize); 129 | if ($targetSize > 0 && $targetSize === $originSize) { 130 | $this->logger->info('Remote file exists, and has same size. Return.'); 131 | 132 | return true; 133 | } 134 | if ($targetSize > 0 && $targetSize !== $originSize) { 135 | $this->logger->info('Remote file exists, but has different size. Remove and replace.'); 136 | $this->delete($target); 137 | } 138 | 139 | if ($tempTarget) { 140 | $this->logger->debug('Temp target provided, check this one.'); 141 | if ($this->checkIfAlreadyUploaded($originSize, $tempTarget)) { 142 | return $this->move($tempTarget, $target); 143 | } else { 144 | return false; 145 | } 146 | } else { 147 | $this->logger->debug('No temp target provided, upload needed.'); 148 | return false; 149 | } 150 | } 151 | 152 | /** 153 | * Verify that uploaded file is the same as origin file, and if so, move to public folder 154 | * 155 | * @param $originSize 156 | * @param $tmpDestination 157 | * @param $publicDestination 158 | * @return bool 159 | * @throws FtpException 160 | * @throws VerifySizeException 161 | */ 162 | public function verifyAndMoveUploadedFile( 163 | $originSize, 164 | $tmpDestination, 165 | $publicDestination 166 | ) { 167 | $remoteTempSize = $this->getSize($tmpDestination); 168 | $this->logger->debug('Temp size: ' . $remoteTempSize); 169 | $this->logger->debug('Origin size: ' . $originSize); 170 | if ($remoteTempSize <= 0) { 171 | throw new VerifySizeException('Uploaded file has size ' . $remoteTempSize); 172 | } 173 | 174 | if ($remoteTempSize !== $originSize) { 175 | throw new VerifySizeException( 176 | sprintf( 177 | 'Uploaded file has wrong size. Expected %s, got %s.', 178 | $originSize, 179 | $remoteTempSize 180 | ) 181 | ); 182 | } 183 | 184 | $this->logger->info('OK: Uploaded temp file has right size.'); 185 | if (!$this->move($tmpDestination, $publicDestination)) { 186 | throw new FtpException('Error renaming uploaded file from temp to public.'); 187 | } 188 | 189 | $remotePublicSize = $this->getSize($publicDestination); 190 | $this->logger->debug('Renamed size: ' . $remotePublicSize); 191 | if ($remotePublicSize <= 0) { 192 | throw new VerifySizeException('Renamed file has size ' . $remotePublicSize); 193 | } 194 | 195 | if ($remotePublicSize !== $originSize) { 196 | throw new VerifySizeException( 197 | sprintf( 198 | 'Renamed file has wrong size. Expected %s, got %s.', 199 | $originSize, 200 | $remotePublicSize 201 | ) 202 | ); 203 | } 204 | $this->logger->info('OK: Renamed file has right size.'); 205 | 206 | return true; 207 | } 208 | 209 | /** 210 | * @return resource 211 | * @throws Exception\FtpException 212 | */ 213 | private function getConnection() 214 | { 215 | if (!$this->connection) { 216 | $this->connection = ftp_connect($this->hostname); 217 | if (!$this->connection) { 218 | throw new FtpException(sprintf('Error connecting to FTP server at %s.', $this->hostname)); 219 | } 220 | 221 | ftp_login($this->connection, $this->username, $this->password); 222 | } 223 | 224 | return $this->connection; 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /src/Aptoma/Guzzle/Plugin/HttpCallInterceptor/Exception/HttpCallToBackendException.php: -------------------------------------------------------------------------------- 1 | logger = $logger; 25 | } 26 | 27 | public static function getSubscribedEvents() 28 | { 29 | return array( 30 | // Use a number lower than the MockPlugin 31 | 'request.before_send' => array('onRequestBeforeSend', -1000), 32 | ); 33 | } 34 | 35 | public function onRequestBeforeSend(Event $event) 36 | { 37 | /** @var \Guzzle\Http\Message\Request $request */ 38 | $request = $event['request']; 39 | if ($request->getResponse()) { 40 | return; 41 | } 42 | 43 | $message = sprintf('Call to %s was not intercepted by MockPlugin.', $request->getUrl()); 44 | if ($this->logger) { 45 | $this->logger->critical($message); 46 | } 47 | 48 | throw new HttpCallToBackendException($message); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Aptoma/Guzzle/Plugin/RequestLogger/RequestLoggerPlugin.php: -------------------------------------------------------------------------------- 1 | finish(array($app['guzzle.request_logger_plugin'], 'writeLog'), Application::LATE_EVENT); 17 | * 18 | * If you do any requests in other finish listeners, you should ensure this is the last one to be called. 19 | */ 20 | class RequestLoggerPlugin implements EventSubscriberInterface 21 | { 22 | 23 | /** @var LoggerInterface */ 24 | private $logger; 25 | private $requestCount = 0; 26 | private $totalRequestTime = 0.0; 27 | 28 | public function __construct($logger) 29 | { 30 | $this->logger = $logger; 31 | } 32 | 33 | public static function getSubscribedEvents() 34 | { 35 | return array( 36 | 'request.sent' => array('onRequestSent', 999), 37 | ); 38 | } 39 | 40 | public function onRequestSent(Event $event) 41 | { 42 | $this->requestCount++; 43 | $this->totalRequestTime += $event['response']->getInfo('total_time'); 44 | } 45 | 46 | /** 47 | * @return int 48 | */ 49 | public function getRequestCount() 50 | { 51 | return $this->requestCount; 52 | } 53 | 54 | /** 55 | * @return double 56 | */ 57 | public function getTotalRequestTime() 58 | { 59 | return $this->totalRequestTime; 60 | } 61 | 62 | public function writeLog(Request $request) 63 | { 64 | if ($this->getRequestCount() == 0) { 65 | return; 66 | } 67 | 68 | $message = sprintf( 69 | 'Executed %s API calls in %sms', 70 | $this->getRequestCount(), 71 | round($this->getTotalRequestTime() * 1000, 1) 72 | ); 73 | $context = array( 74 | 'requestCount' => $this->getRequestCount(), 75 | 'totalRequestTime' => $this->getTotalRequestTime(), 76 | 'method' => $request->getMethod(), 77 | 'path' => $request->getPathInfo(), 78 | ); 79 | if ($request->getQueryString()) { 80 | $context['query'] = $request->getQueryString(); 81 | } 82 | $this->logger->info($message, $context); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/Aptoma/Guzzle/Plugin/RequestPreSendLogger/RequestBeforeSendLoggerPlugin.php: -------------------------------------------------------------------------------- 1 | logger = $logger; 17 | } 18 | 19 | public static function getSubscribedEvents() 20 | { 21 | return array( 22 | 'request.before_send' => array('onBeforeRequestSend', 999), 23 | ); 24 | } 25 | 26 | public function onBeforeRequestSend(Event $event) 27 | { 28 | /** @var \Guzzle\Http\Message\Request $guzzleRequest */ 29 | $guzzleRequest = $event['request']; 30 | $this->logger->info( 31 | sprintf( 32 | 'Sending %s %s', 33 | $guzzleRequest->getMethod(), 34 | $guzzleRequest->getUrl() 35 | ), 36 | array('event' => 'request.send') 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Aptoma/Guzzle/Plugin/RequestToken/RequestTokenPlugin.php: -------------------------------------------------------------------------------- 1 | finish(array($app['guzzle.request_logger_plugin'], 'writeLog'), Application::LATE_EVENT); 16 | * 17 | * If you do any requests in other finish listeners, you should ensure this is the last one to be called. 18 | */ 19 | class RequestTokenPlugin implements EventSubscriberInterface 20 | { 21 | 22 | private $token; 23 | /** @var RequestStack */ 24 | private $requestStack; 25 | 26 | public function __construct($token, RequestStack $requestStack) 27 | { 28 | $this->token = $token; 29 | $this->requestStack = $requestStack; 30 | } 31 | 32 | public static function getSubscribedEvents() 33 | { 34 | return array( 35 | 'request.before_send' => array('onBeforeRequestSend', 999), 36 | ); 37 | } 38 | 39 | public function onBeforeRequestSend(Event $event) 40 | { 41 | /** @var \Guzzle\Http\Message\Request $guzzleRequest */ 42 | $guzzleRequest = $event['request']; 43 | if (null !== $remoteToken = $this->getRemoteRequestToken()) { 44 | $guzzleRequest->addHeader('X-Remote-Request-Token', $remoteToken . ' ' . $this->token); 45 | } else { 46 | $guzzleRequest->addHeader('X-Remote-Request-Token', $this->token); 47 | } 48 | } 49 | 50 | private function getRemoteRequestToken() 51 | { 52 | if (!$this->requestStack->getCurrentRequest()) { 53 | return null; 54 | } 55 | 56 | return $this->requestStack->getCurrentRequest()->headers->get('X-Remote-Request-Token'); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Aptoma/JsonErrorHandler.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | class JsonErrorHandler 18 | { 19 | /** @var Application */ 20 | private $app; 21 | 22 | /** @var Request */ 23 | private $request; 24 | 25 | /** @var bool */ 26 | private $handleNonJsonRequests; 27 | 28 | public function __construct(Container $app, $handleNonJsonRequests = false) 29 | { 30 | $this->app = $app; 31 | $this->handleNonJsonRequests = $handleNonJsonRequests; 32 | } 33 | 34 | public function setRequest(Request $request) 35 | { 36 | $this->request = $request; 37 | 38 | return $this; 39 | } 40 | 41 | public function handle(\Exception $e, $code) 42 | { 43 | if (!$this->request) { 44 | try { 45 | $this->request = $this->app['request_stack']->getCurrentRequest(); 46 | } catch (\RuntimeException $e) { 47 | return null; 48 | } 49 | } 50 | 51 | if (!$this->shouldHandleRequest()) { 52 | return null; 53 | } 54 | 55 | if ($e instanceof HttpException) { 56 | return $this->handleHttpException($e, $code); 57 | } 58 | 59 | return $this->handleGenericException($e, $code); 60 | } 61 | 62 | private function handleHttpException(HttpException $e, $code) 63 | { 64 | $message = array( 65 | 'status' => $e->getStatusCode(), 66 | 'code' => $code, 67 | 'message' => $e->getMessage() 68 | ); 69 | 70 | return $this->app->json( 71 | $message, 72 | $e->getStatusCode(), 73 | $e->getHeaders() 74 | ); 75 | } 76 | 77 | private function handleGenericException(\Exception $e, $code) 78 | { 79 | $message = array( 80 | 'status' => 500, 81 | 'code' => $code, 82 | 'message' => $e->getMessage() 83 | ); 84 | 85 | return $this->app->json( 86 | $message, 87 | 500, 88 | array('Content-Type' => 'application/json') 89 | ); 90 | } 91 | 92 | /** 93 | * @return bool 94 | */ 95 | private function shouldHandleRequest() 96 | { 97 | if ($this->handleNonJsonRequests) { 98 | return true; 99 | } 100 | 101 | return in_array('application/json', $this->request->getAcceptableContentTypes()); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/Aptoma/Log/ExtraContextProcessor.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | class ExtraContextProcessor 9 | { 10 | 11 | private $extras; 12 | 13 | public function __construct(array $extras) 14 | { 15 | $this->extras = $extras; 16 | } 17 | 18 | public function __invoke(array $record) 19 | { 20 | foreach ($this->extras as $key => $value) { 21 | $record['extra'][$key] = $value; 22 | } 23 | 24 | return $record; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Aptoma/Log/RequestProcessor.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | class RequestProcessor 18 | { 19 | /** 20 | * @var Application 21 | */ 22 | private $app; 23 | private $token; 24 | private $clientIp; 25 | private $remoteRequestToken; 26 | 27 | public function __construct(Container $app, $token = null) 28 | { 29 | $this->app = $app; 30 | $this->token = $token ?: uniqid(); 31 | } 32 | 33 | public function __invoke(array $record) 34 | { 35 | try { 36 | $record['extra']['clientIp'] = $this->getClientIp($this->app['request_stack']); 37 | } catch (\Exception $e) { 38 | $record['extra']['clientIp'] = ''; 39 | } 40 | if ($this->app->offsetExists('security.token_storage')) { 41 | $record['extra']['user'] = $this->getUsername($this->app['security.token_storage']); 42 | } 43 | $record['extra']['token'] = $this->token; 44 | 45 | $record = $this->addRemoteRequestToken($record, $this->app['request_stack']); 46 | 47 | return $record; 48 | } 49 | 50 | private function getClientIp(RequestStack $requestStack) 51 | { 52 | if (!$this->clientIp) { 53 | if ($requestStack->getCurrentRequest()) { 54 | $this->clientIp = $requestStack->getCurrentRequest()->getClientIp(); 55 | } 56 | } 57 | 58 | return $this->clientIp; 59 | } 60 | 61 | private function getUsername(TokenStorageInterface $tokenStorage) 62 | { 63 | try { 64 | $token = $tokenStorage->getToken(); 65 | 66 | if ($token) { 67 | return $token->getUsername(); 68 | } 69 | } catch (\InvalidArgumentException $e) { 70 | } 71 | 72 | return ''; 73 | } 74 | 75 | private function addRemoteRequestToken($record, RequestStack $requestStack) 76 | { 77 | if (!$this->remoteRequestToken) { 78 | $request = $requestStack->getCurrentRequest(); 79 | if ($request && null !== $remoteRequestToken = $request->headers->get('X-Remote-Request-Token')) { 80 | $this->remoteRequestToken = $remoteRequestToken; 81 | } 82 | } 83 | 84 | if ($this->remoteRequestToken) { 85 | $record['extra']['remoteRequestToken'] = $this->remoteRequestToken; 86 | return $record; 87 | } 88 | 89 | return $record; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/Aptoma/Security/Authentication/Token/ApiKeyToken.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class ApiKeyToken extends AbstractToken 13 | { 14 | private $apiKey; 15 | 16 | /** 17 | * Constructor 18 | * 19 | * @param string $apikey the users API key 20 | * @param array $roles an array of optional user roles 21 | */ 22 | public function __construct($apiKey, array $roles = array()) 23 | { 24 | parent::__construct($roles); 25 | $this->apiKey = $apiKey; 26 | parent::setAuthenticated(count($roles) > 0); 27 | } 28 | 29 | public function setAuthenticated($isAuthenticated) 30 | { 31 | if ($isAuthenticated) { 32 | throw new \LogicException('Cannot set this token to trusted after instantiation.'); 33 | } 34 | 35 | parent::setAuthenticated(false); 36 | } 37 | 38 | public function getCredentials() 39 | { 40 | return $this->apiKey; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Aptoma/Security/Encoder/SaltLessPasswordEncoderInterface.php: -------------------------------------------------------------------------------- 1 | tokenStorage = $tokenStorage; 24 | $this->authenticationManager = $authenticationManager; 25 | } 26 | 27 | /** 28 | * Handles API key authentication. 29 | * 30 | * @param GetResponseEvent $event A GetResponseEvent instance 31 | */ 32 | public function handle(GetResponseEvent $event) 33 | { 34 | $apiKey = $this->getApiKeyFromQueryOrHeader($event->getRequest()); 35 | 36 | if (false === $apiKey) { 37 | return; 38 | } 39 | 40 | try { 41 | $token = $this->authenticationManager->authenticate(new ApiKeyToken($apiKey)); 42 | $this->tokenStorage->setToken($token); 43 | } catch (AuthenticationException $failed) { 44 | $this->tokenStorage->setToken(null); 45 | $this->doFailureResponse($event); 46 | } 47 | } 48 | 49 | /** 50 | * Failure response 51 | * 52 | * Can be overridden if a different response is needed 53 | * 54 | * @param GetResponseEvent $event 55 | */ 56 | protected function doFailureResponse(GetResponseEvent $event) 57 | { 58 | $headers = array(); 59 | $content = 'Forbidden'; 60 | if (in_array('application/json', $event->getRequest()->getAcceptableContentTypes())) { 61 | $headers['Content-Type'] = 'application/json'; 62 | $content = json_encode(array('message' => $content)); 63 | } 64 | 65 | $event->setResponse(new Response($content, 403, $headers)); 66 | } 67 | 68 | /** 69 | * @param Request $request 70 | * @return string|boolean 71 | */ 72 | private function getApiKeyFromQueryOrHeader(Request $request) 73 | { 74 | $apiKey = $request->get('apikey', false); 75 | if ($apiKey) { 76 | return $apiKey; 77 | } 78 | 79 | $apiKeyHeader = $request->headers->get('authorization'); 80 | if (!($apiKeyHeader && mb_strpos($apiKeyHeader, 'apikey') === 0)) { 81 | return false; 82 | } 83 | $apiKeyHeadersParts = explode(' ', $apiKeyHeader); 84 | 85 | if (!isset($apiKeyHeadersParts[1])) { 86 | return false; 87 | } 88 | 89 | return $apiKeyHeadersParts[1]; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/Aptoma/Security/Provider/ApiKeyAuthenticationProvider.php: -------------------------------------------------------------------------------- 1 | userProvider = $userProvider; 35 | $this->encoder = $encoder; 36 | } 37 | 38 | /** 39 | * Authenticate the user based on an API key 40 | * 41 | * @param TokenInterface $token 42 | */ 43 | public function authenticate(TokenInterface $token) 44 | { 45 | $user = $this->userProvider->loadUserByApiKey($this->encoder->encodePassword($token->getCredentials())); 46 | 47 | if (!$user || !($user instanceof UserInterface)) { 48 | throw new AuthenticationException('Bad credentials'); 49 | } 50 | 51 | $token = new ApiKeyToken($token->getCredentials(), $user->getRoles()); 52 | $token->setUser($user); 53 | 54 | return $token; 55 | } 56 | 57 | public function supports(TokenInterface $token) 58 | { 59 | return $token instanceof ApiKeyToken; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Aptoma/Security/User/ApiKeyUserProviderInterface.php: -------------------------------------------------------------------------------- 1 | 19 | * 20 | * This represents the service container, so it will by definition know about 21 | * a lot of classes. This is not really an issue for this class. 22 | * @SuppressWarnings(PHPMD.CouplingBetweenObjects) 23 | */ 24 | class Application extends BaseApplication 25 | { 26 | protected $defaultValues = array( 27 | 'timer.threshold_info' => 1000, 28 | 'timer.threshold_warning' => 5000, 29 | ); 30 | 31 | public function __construct(array $values = array()) 32 | { 33 | $values['request_token'] = $this->generateRequestToken(); 34 | $values = array_merge($this->defaultValues, $values); 35 | parent::__construct($values); 36 | 37 | $app = $this; 38 | 39 | $errorHandler = new JsonErrorHandler($app); 40 | $app->error(array($errorHandler, 'handle')); 41 | 42 | $app->register(new ServiceControllerServiceProvider()); 43 | $app->register(new UrlGeneratorServiceProvider()); 44 | 45 | // Register timer function 46 | $app->finish(array($app, 'logExecTime')); 47 | } 48 | 49 | public function logExecTime(Request $request) 50 | { 51 | $execTime = round(microtime(true) - $this['timer.start'], 6) * 1000; 52 | $message = sprintf('Script executed in %sms.', $execTime); 53 | $context = array( 54 | 'msExecTime' => $execTime, 55 | 'method' => $request->getMethod(), 56 | 'path' => $request->getPathInfo(), 57 | ); 58 | if ($request->getQueryString()) { 59 | $context['query'] = $request->getQueryString(); 60 | } 61 | if ($execTime < $this['timer.threshold_info']) { 62 | $this['logger']->debug($message, $context); 63 | } elseif ($execTime < $this['timer.threshold_warning']) { 64 | $this['logger']->info($message, $context); 65 | } else { 66 | $this['logger']->warn($message, $context); 67 | } 68 | } 69 | 70 | /** 71 | * @param Application $app 72 | * @return void 73 | */ 74 | protected function registerTwig(Application $app) 75 | { 76 | if (!$app->offsetExists('twig.path')) { 77 | return; 78 | } 79 | $app->register( 80 | new TwigServiceProvider(), 81 | array( 82 | 'twig.path' => $app['twig.path'], 83 | 'twig.options' => $app['twig.options'] 84 | ) 85 | ); 86 | 87 | if (class_exists('\App\Twig\Extension\AppExtension')) { 88 | $app['twig'] = $app->extend( 89 | 'twig', 90 | function (\Twig_Environment $twig) use ($app) { 91 | $twig->addExtension(new \App\Twig\Extension\AppExtension($app)); 92 | 93 | return $twig; 94 | } 95 | ); 96 | } 97 | } 98 | 99 | /** 100 | * @param Application $app 101 | * @return void 102 | */ 103 | protected function registerLogger(Application $app) 104 | { 105 | if (!$app->offsetExists('monolog.name')) { 106 | return; 107 | } 108 | $app->register( 109 | new MonologServiceProvider(), 110 | array( 111 | 'monolog.name' => $app['monolog.name'], 112 | 'monolog.level' => $app['monolog.level'], 113 | 'monolog.logfile' => $app['monolog.logfile'], 114 | ) 115 | ); 116 | $this->register(new ExtendedLoggerServiceProvider()); 117 | } 118 | 119 | /** 120 | * Generate unique identifier for this request 121 | * 122 | * The token should be a string without any spaces. 123 | * 124 | * @return string 125 | */ 126 | protected function generateRequestToken() 127 | { 128 | return uniqid(); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/Aptoma/Silex/Provider/ApiKeyServiceProvider.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | class ApiKeyServiceProvider implements ServiceProviderInterface 18 | { 19 | public function register(Container $app) 20 | { 21 | $app['security.authentication_listener.factory.api_key'] = $app->protect( 22 | function ($name, $options) use ($app) { 23 | unset($options); // not in use 24 | $app['security.authentication_provider.'.$name.'.api_key'] = function () use ($app) { 25 | return new ApiKeyAuthenticationProvider( 26 | $app['api_key.user_provider'], 27 | $app['api_key.encoder'] 28 | ); 29 | }; 30 | 31 | $app['security.authentication_listener.'.$name.'.api_key'] = function () use ($app) { 32 | return new ApiKeyAuthenticationListener( 33 | $app['security'], 34 | $app['security.authentication_manager'] 35 | ); 36 | }; 37 | 38 | return array( 39 | 'security.authentication_provider.' . $name . '.api_key', 40 | 'security.authentication_listener.' . $name . '.api_key', 41 | null, // the entry point id 42 | 'pre_auth' // // the position of the listener in the stack 43 | ); 44 | } 45 | ); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Aptoma/Silex/Provider/CacheServiceProvider.php: -------------------------------------------------------------------------------- 1 | setMemcached($app['memcached']); 31 | 32 | return $cache; 33 | }; 34 | 35 | $app['cache.predis'] = function () use ($app) { 36 | if (!class_exists('\Doctrine\Common\Cache\PredisCache')) { 37 | throw new \Exception('You need to include doctrine/common in order to use the cache service'); 38 | } 39 | return new PredisCache($app['predis.client']); 40 | }; 41 | 42 | $app['cache.predis_serializer'] = function () use ($app) { 43 | if (!class_exists('\Doctrine\Common\Cache\PredisCache')) { 44 | throw new \Exception('You need to include doctrine/common in order to use the cache service'); 45 | } 46 | return new SerializingPredisCache($app['predis.client']); 47 | }; 48 | 49 | $app['cache'] = function () use ($app) { 50 | if ($app->offsetExists('cache.default')) { 51 | return $app[$app['cache.default']]; 52 | } 53 | 54 | return $app['memcached']; 55 | }; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Aptoma/Silex/Provider/ConsoleLoggerServiceProvider.php: -------------------------------------------------------------------------------- 1 | offsetExists('monolog.console_logfile') 30 | ? $app['monolog.console_logfile'] 31 | : $app['monolog.logfile']; 32 | return new StreamHandler($logfile, $app['monolog.level']); 33 | }; 34 | 35 | $app['logger'] = $app->extend( 36 | 'logger', 37 | function ( 38 | Logger $logger, 39 | \Pimple $app 40 | ) { 41 | $consoleHandler = new ConsoleHandler($app['console.output']); 42 | if (!class_exists('Symfony\Bridge\Monolog\Handler\ConsoleHandler')) { 43 | throw new \Exception('ConsoleLoggerServiceProvider requires symfony/monolog-bridge ~2.4.'); 44 | } 45 | $consoleHandler->setFormatter(new ConsoleFormatter($app['logger.console_format'])); 46 | $logger->pushHandler($consoleHandler); 47 | 48 | return $logger; 49 | } 50 | ); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Aptoma/Silex/Provider/ExtendedLoggerServiceProvider.php: -------------------------------------------------------------------------------- 1 | 24 | */ 25 | class ExtendedLoggerServiceProvider implements ServiceProviderInterface 26 | { 27 | public function register(Container $app) 28 | { 29 | $app['monolog.formatter'] = function () { 30 | return new LineFormatter(null, 'Y-m-d H:i:s.u'); 31 | }; 32 | 33 | $app['monolog.handler'] = $app->factory(function () use ($app) { 34 | if (!$app['monolog.logfile']) { 35 | return new NullHandler(); 36 | } 37 | if (method_exists('Silex\Provider\MonologServiceProvider', 'translateLevel')) { 38 | $level = MonologServiceProvider::translateLevel($app['monolog.level']); 39 | } else { 40 | $level = $app['monolog.level']; 41 | } 42 | $streamHandler = new StreamHandler($app['monolog.logfile'], $level); 43 | $streamHandler->setFormatter($app['monolog.formatter']); 44 | 45 | return $streamHandler; 46 | }); 47 | 48 | $app['logger'] = $app->extend( 49 | 'logger', 50 | function ( 51 | Logger $logger, 52 | Container $app 53 | ) { 54 | $logger->pushProcessor($app['logger.request_processor']); 55 | $logger->pushProcessor(new PsrLogMessageProcessor()); 56 | 57 | if (!($app->offsetExists('monolog.logstashfile') && $app['monolog.logstashfile'])) { 58 | return $logger; 59 | } 60 | 61 | $logstashHandler = new StreamHandler( 62 | $app['monolog.logstashfile'], 63 | $app['monolog.level'] 64 | ); 65 | $logstashHandler->setFormatter(new LogstashFormatter($app['monolog.name'])); 66 | 67 | $extras = array(); 68 | if ($app->offsetExists('meta.service')) { 69 | $extras['service'] = $app['meta.service']; 70 | } 71 | if ($app->offsetExists('meta.customer')) { 72 | $extras['customer'] = $app['meta.customer']; 73 | } 74 | if ($app->offsetExists('meta.environment')) { 75 | $extras['environment'] = $app['meta.environment']; 76 | } 77 | $logstashHandler->pushProcessor(new ExtraContextProcessor($extras)); 78 | 79 | $logger->pushHandler($logstashHandler); 80 | 81 | return $logger; 82 | } 83 | ); 84 | 85 | $app['logger.request_processor'] = function () use ($app) { 86 | return new RequestProcessor($app); 87 | }; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/Aptoma/Silex/Provider/GuzzleServiceProvider.php: -------------------------------------------------------------------------------- 1 | $app['guzzle.handler_stack'] 31 | ]); 32 | 33 | return $client; 34 | }; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Aptoma/Silex/Provider/MemcachedServiceProvider.php: -------------------------------------------------------------------------------- 1 | setOption(\Memcached::OPT_COMPRESSION, false); 17 | $memcached->setOption(\Memcached::OPT_PREFIX_KEY, $app['memcached.prefix']); 18 | 19 | $serversToAdd = array_udiff( 20 | $app['memcached.servers'], 21 | $memcached->getServerList(), 22 | function ($a, $b) { 23 | return ($a['host'] == $b['host'] && $a['port'] == $b['port']) ? 0 : 1; 24 | } 25 | ); 26 | if (count($serversToAdd)) { 27 | $memcached->addServers($serversToAdd); 28 | } 29 | 30 | return $memcached; 31 | }; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Aptoma/Silex/Provider/PredisClientServiceProvider.php: -------------------------------------------------------------------------------- 1 | $app['redis.host'], 17 | 'port' => $app['redis.port'], 18 | 'database' => $app['redis.database'], 19 | 'persistent' => $app['redis.persistent'], 20 | 'timeout' => $app['redis.timeout'], 21 | ), 22 | array( 23 | 'prefix' => $app['redis.prefix'] 24 | ) 25 | ); 26 | 27 | return $redisClient; 28 | }; 29 | 30 | $app['redis.host'] = '127.0.0.1'; 31 | $app['redis.port'] = 6379; 32 | $app['redis.prefix'] = 'prefix::'; 33 | $app['redis.database'] = 0; 34 | $app['redis.persistent'] = false; 35 | $app['redis.timeout'] = 5.0; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Aptoma/Silex/Provider/StorageServiceProvider.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | class UrlGeneratorServiceProvider implements ServiceProviderInterface 15 | { 16 | public function register(Container $app) 17 | { 18 | $app['url_generator'] = function ($app) { 19 | $app->flush(); 20 | 21 | return new UrlGenerator($app['routes'], $app['request_context']); 22 | }; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Aptoma/Storage/Exception/FileNotFoundException.php: -------------------------------------------------------------------------------- 1 | storageDirectory = $storageDirectory; 23 | $this->publicUrlTemplate = $publicUrlTemplate; 24 | $this->logger = $logger; 25 | } 26 | 27 | /** 28 | * @param resource|string|UploadedFile $resource 29 | * @return string Asset identifier 30 | */ 31 | public function put($resource) 32 | { 33 | $tmpFileName = tempnam('/tmp', 'test_'); 34 | if ($resource instanceof UploadedFile) { 35 | $pathinfo = pathinfo($tmpFileName); 36 | $resource->move($pathinfo['dirname'], $pathinfo['basename']); 37 | } else { 38 | file_put_contents($tmpFileName, $resource); 39 | } 40 | 41 | $checksum = sha1_file($tmpFileName); 42 | $file = new File($tmpFileName); 43 | $targetFileName = $checksum . '.' . $file->guessExtension(); 44 | $target = $file->move($this->storageDirectory, $targetFileName); 45 | $filesystem = new Filesystem(); 46 | 47 | $filesystem->chmod($target, 0777); 48 | 49 | return $targetFileName; 50 | } 51 | 52 | /** 53 | * @param $identifier 54 | * @return string Url where resource can be read 55 | */ 56 | public function getUrl($identifier) 57 | { 58 | return str_replace('{assetId}', $identifier, $this->publicUrlTemplate); 59 | } 60 | 61 | /** 62 | * @param $identifier 63 | * @param bool $asResource 64 | * @throws Exception\StorageException 65 | * @return string|resource The raw content or a resource to read the content stream. 66 | */ 67 | public function getRaw($identifier, $asResource = false) 68 | { 69 | $source = $this->getSourceFileName($identifier); 70 | if (!file_exists($source)) { 71 | throw new FileNotFoundException(sprintf('File `%s` does not exist.', $source)); 72 | } 73 | 74 | if ($asResource) { 75 | return fopen($source, 'rb'); 76 | } else { 77 | return file_get_contents($source); 78 | } 79 | } 80 | 81 | public function getMimeType($identifier) 82 | { 83 | $file = new File($this->getSourceFileName($identifier)); 84 | 85 | return $file->getMimeType(); 86 | } 87 | 88 | /** 89 | * @param $identifier 90 | * @return string 91 | */ 92 | private function getSourceFileName($identifier) 93 | { 94 | return $this->storageDirectory . '/' . $identifier; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/Aptoma/Storage/StorageInterface.php: -------------------------------------------------------------------------------- 1 | pathToAppBootstrap && is_readable($this->pathToAppBootstrap))) { 20 | $app = new Application(); 21 | } else { 22 | $app = require $this->pathToAppBootstrap; 23 | } 24 | 25 | $app['debug'] = false; 26 | unset($app['exception_handler']); 27 | 28 | return $app; 29 | } 30 | 31 | /** 32 | * Creates a TestClient. 33 | * 34 | * @param array $server An array of server parameters 35 | * 36 | * @return TestClient A Client instance 37 | */ 38 | public function createClient(array $server = array()) 39 | { 40 | return new TestClient($this->app, $server); 41 | } 42 | 43 | /** 44 | * Create a client with basic auth credentials. 45 | * 46 | * @param array $server 47 | * @return TestClient 48 | */ 49 | protected function createAuthorizedClient(array $server = array()) 50 | { 51 | return $this->createClient( 52 | array_merge( 53 | array( 54 | 'PHP_AUTH_USER' => 'username', 55 | 'PHP_AUTH_PW' => 'password', 56 | ), 57 | $server 58 | ) 59 | ); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Aptoma/TestToolkit/TestClient.php: -------------------------------------------------------------------------------- 1 | request( 19 | 'POST', 20 | $url, 21 | array(), 22 | array(), 23 | array('CONTENT_TYPE' => 'application/json', 'HTTP_ACCEPT' => 'application/json'), 24 | json_encode($data) 25 | ); 26 | } 27 | 28 | /** 29 | * Shortcut method for simple PUTing of JSON-encoded data 30 | * 31 | * @param $url 32 | * @param $data 33 | * @return \Symfony\Component\DomCrawler\Crawler 34 | */ 35 | public function putJson($url, $data) 36 | { 37 | return $this->request( 38 | 'PUT', 39 | $url, 40 | array(), 41 | array(), 42 | array('CONTENT_TYPE' => 'application/json', 'HTTP_ACCEPT' => 'application/json'), 43 | json_encode($data) 44 | ); 45 | } 46 | 47 | /** 48 | * Override getResponse to provide actual return value hinting. 49 | * 50 | * @return \Symfony\Component\HttpFoundation\Response 51 | */ 52 | public function getResponse() 53 | { 54 | return parent::getResponse(); 55 | } 56 | 57 | public function getJsonDecodedResponseBody() 58 | { 59 | return json_decode($this->getResponse()->getContent(), true); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /tests/src/Aptoma/Cache/SerializingPredisCacheTest.php: -------------------------------------------------------------------------------- 1 | redisClient = $this->createClient(); 19 | $this->cache = new SerializingPredisCache($this->redisClient); 20 | $this->cache->setNamespace('test'); 21 | $this->cache->flushAll(); 22 | } 23 | 24 | public function testFetchShouldHandleUnserializedData() 25 | { 26 | $this->redisClient->set('test[foo][1]', 'bar'); 27 | $value = $this->cache->fetch('foo'); 28 | 29 | $this->assertEquals('bar', $value); 30 | } 31 | 32 | public function testFetchShouldHandleSerializedData() 33 | { 34 | $redisClient = $this->createClient(); 35 | $cache = new SerializingPredisCache($redisClient); 36 | $cache->save('foo', 'bar'); 37 | $value = $cache->fetch('foo'); 38 | 39 | $this->assertEquals('bar', $value); 40 | } 41 | 42 | private function createClient() 43 | { 44 | return new Client( 45 | array( 46 | 'host' => 'localhost', 47 | 'port' => 6379, 48 | 'database' => 15, 49 | ), 50 | array( 51 | 'prefix' => 'test::' 52 | ) 53 | ); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /tests/src/Aptoma/JsonErrorHandlerTest.php: -------------------------------------------------------------------------------- 1 | headers->set('Accept', 'application/xml'); 17 | 18 | $errorHandler = new JsonErrorHandler($this->app); 19 | $errorHandler->setRequest($request); 20 | 21 | $this->assertNull($errorHandler->handle(new HttpException(404), 404)); 22 | } 23 | 24 | public function testHandleShouldReturnHandleNonJsonAcceptHeaderWhenForceHandleIsEnabled() 25 | { 26 | $request = new Request(); 27 | $request->headers->set('Accept', 'application/xml'); 28 | 29 | $errorHandler = new JsonErrorHandler($this->app, true); 30 | $errorHandler->setRequest($request); 31 | 32 | $this->assertInstanceOf( 33 | '\Symfony\Component\HttpFoundation\JsonResponse', 34 | $errorHandler->handle(new HttpException(404), 404) 35 | ); 36 | } 37 | 38 | public function testHandleShouldReturnNullIfNoValidRequestIsAvailable() 39 | { 40 | $request = new Request(); 41 | 42 | $errorHandler = new JsonErrorHandler($this->app); 43 | $errorHandler->setRequest($request); 44 | 45 | $this->assertNull($errorHandler->handle(new HttpException(404), 404)); 46 | } 47 | 48 | public function testHandleShouldNotReturnNullIfValidRequestIsAvailable() 49 | { 50 | $errorHandler = new JsonErrorHandler($this->app); 51 | $request = new Request(); 52 | $request->headers->set('Accept', 'application/json'); 53 | 54 | $this->app['request_stack']->push($request); 55 | 56 | $this->assertNotNull($errorHandler->handle(new HttpException(404), 404)); 57 | } 58 | 59 | public function testHandleShouldReturnJsonResponse() 60 | { 61 | $request = new Request(); 62 | $request->headers->set('Accept', 'application/json'); 63 | 64 | $errorHandler = new JsonErrorHandler($this->app); 65 | $errorHandler->setRequest($request); 66 | 67 | $response = $errorHandler->handle(new HttpException(404, 'Foo Bar'), 400); 68 | $body = json_decode($response->getContent(), true); 69 | 70 | $this->assertInstanceOf('\Symfony\Component\HttpFoundation\JsonResponse', $response); 71 | $this->assertEquals(404, $response->getStatusCode()); 72 | $this->assertEquals( 73 | array( 74 | 'status' => 404, 75 | 'code' => 400, 76 | 'message' => 'Foo Bar', 77 | ), 78 | $body 79 | ); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /tests/src/Aptoma/Log/ExtraContextProcessorTest.php: -------------------------------------------------------------------------------- 1 | assertEquals($expected, $processor(array())); 21 | } 22 | 23 | public function invokeDataProvider() 24 | { 25 | return array( 26 | 'no values' => array ( 27 | array(), array() 28 | ), 29 | 'single value' => array ( 30 | array('service' => 'admin'), array('extra' => array('service' => 'admin')) 31 | ), 32 | 'multiple values' => array ( 33 | array('service' => 'admin', 'customer' => 'Aptoma', 'environment' => 'test'), 34 | array('extra' => array('service' => 'admin', 'customer' => 'Aptoma', 'environment' => 'test')) 35 | ), 36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tests/src/Aptoma/Log/RequestProcessorTest.php: -------------------------------------------------------------------------------- 1 | server->set('REMOTE_ADDR', '127.0.0.1'); 20 | $requestStack = new RequestStack(); 21 | $requestStack->push($request); 22 | $app['request_stack'] = $requestStack; 23 | 24 | $this->setupSecurityMocks($app); 25 | 26 | $token = new AnonymousToken('key', 'testuser'); 27 | $app['security.token_storage']->setToken($token); 28 | 29 | $processor = new RequestProcessor($app); 30 | $record = $processor(array()); 31 | 32 | $this->assertEquals('127.0.0.1', $record['extra']['clientIp']); 33 | $this->assertEquals('testuser', $record['extra']['user']); 34 | } 35 | 36 | public function testInvokeShouldSetEmptyUsernameWhenNoContextIsFound() 37 | { 38 | $app = new Application(); 39 | $processor = new RequestProcessor($app); 40 | 41 | $this->setupSecurityMocks($app); 42 | 43 | $record = $processor(array()); 44 | 45 | $this->assertEquals('', $record['extra']['user']); 46 | } 47 | 48 | /** 49 | * @return Application 50 | */ 51 | private function setupSecurityMocks(Application $app) 52 | { 53 | $authenticationManager = $this->createMock( 54 | 'Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface' 55 | ); 56 | $accessDecisionManager = $this->createMock( 57 | 'Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface' 58 | ); 59 | 60 | $app['security.token_storage'] = new TokenStorage(); 61 | $app['security.authorization_checker'] = new AuthorizationChecker( 62 | $app['security.token_storage'], 63 | $authenticationManager, 64 | $accessDecisionManager 65 | ); 66 | 67 | return $app; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /tests/src/Aptoma/Security/Authentication/Http/Firewall/ApiKeyAuthenticationListenerTest.php: -------------------------------------------------------------------------------- 1 | createMock($authenticationManager); 22 | $authenticationManager->expects($this->once()) 23 | ->method('authenticate') 24 | ->will($this->returnValue($token)); 25 | 26 | $tokenStorage = $this->createMock( 27 | 'Symfony\\Component\\Security\\Core\\Authentication\\Token\\Storage\\TokenStorageInterface' 28 | ); 29 | $tokenStorage->expects($this->once()) 30 | ->method('setToken') 31 | ->with($token); 32 | 33 | $listener = new ApiKeyAuthenticationListener($tokenStorage, $authenticationManager); 34 | $listener->handle($this->getGetResponseEventWithApiKeyQueryParameter()); 35 | } 36 | 37 | public function testHandleShouldAuthenticateTokenFromAuthorizationHeader() 38 | { 39 | $token = new ApiKeyToken('key'); 40 | 41 | $authenticationManager = 'Symfony\\Component\\Security\\Core\\Authentication\\AuthenticationManagerInterface'; 42 | $authenticationManager = $this->createMock($authenticationManager); 43 | $authenticationManager->expects($this->once()) 44 | ->method('authenticate') 45 | ->will($this->returnValue($token)); 46 | 47 | $tokenStorage = $this->createMock( 48 | 'Symfony\\Component\\Security\\Core\\Authentication\\Token\\Storage\\TokenStorageInterface' 49 | ); 50 | $tokenStorage->expects($this->once()) 51 | ->method('setToken') 52 | ->with($token); 53 | 54 | $listener = new ApiKeyAuthenticationListener($tokenStorage, $authenticationManager); 55 | $listener->handle($this->getGetResponseEventWithApiKeyAuthorizationHeader()); 56 | } 57 | 58 | public function testHandleShouldNullifyTokenOnFailure() 59 | { 60 | $authenticationManager = 'Symfony\\Component\\Security\\Core\\Authentication\\AuthenticationManagerInterface'; 61 | $authenticationManager = $this->createMock($authenticationManager); 62 | $authenticationManager->expects($this->once()) 63 | ->method('authenticate') 64 | ->will($this->throwException(new AuthenticationException('Authentication failed'))); 65 | 66 | $tokenStorage = $this->createMock( 67 | 'Symfony\\Component\\Security\\Core\\Authentication\\Token\\Storage\\TokenStorageInterface' 68 | ); 69 | $tokenStorage->expects($this->once()) 70 | ->method('setToken') 71 | ->with(null); 72 | 73 | $listener = new ApiKeyAuthenticationListener($tokenStorage, $authenticationManager); 74 | $listener->handle($this->getGetResponseEventWithApiKeyQueryParameter()); 75 | } 76 | 77 | private function getGetResponseEventWithApiKeyQueryParameter() 78 | { 79 | $request = new Request(array('apikey' => 'key')); 80 | 81 | $event = $this->getMockBuilder('Symfony\\Component\\HttpKernel\\Event\\GetResponseEvent') 82 | ->disableOriginalConstructor() 83 | ->getMock(); 84 | $event->expects($this->any()) 85 | ->method('getRequest') 86 | ->will($this->returnValue($request)); 87 | 88 | return $event; 89 | } 90 | 91 | private function getGetResponseEventWithApiKeyAuthorizationHeader() 92 | { 93 | $request = new Request(); 94 | $request->headers->set('Authorization', 'apikey key'); 95 | 96 | $event = $this->getMockBuilder('Symfony\\Component\\HttpKernel\\Event\\GetResponseEvent') 97 | ->disableOriginalConstructor() 98 | ->getMock(); 99 | $event->expects($this->any()) 100 | ->method('getRequest') 101 | ->will($this->returnValue($request)); 102 | 103 | return $event; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /tests/src/Aptoma/Security/Authentication/Provider/ApiKeyAuthenticationProviderTest.php: -------------------------------------------------------------------------------- 1 | createMock('Aptoma\\Security\\User\\ApiKeyUserProviderInterface'); 20 | $userProvider->expects($this->once()) 21 | ->method('loadUserByApiKey') 22 | ->will($this->returnValue(false)); 23 | 24 | $encoder = $this->createMock('Aptoma\\Security\\Encoder\\SaltLessPasswordEncoderInterface'); 25 | $encoder->expects($this->once()) 26 | ->method('encodePassword') 27 | ->will($this->returnValue('anything')); 28 | 29 | $provider = new ApiKeyAuthenticationProvider($userProvider, $encoder); 30 | $provider->authenticate(new ApiKeyToken('key')); 31 | } 32 | 33 | public function testAuthenticateShouldReturnTokenWithUser() 34 | { 35 | $user = $this->createMock('Symfony\\Component\\Security\\Core\\User\\UserInterface'); 36 | $user->expects($this->once()) 37 | ->method('getRoles') 38 | ->will($this->returnValue(array())); 39 | 40 | $userProvider = $this->createMock('Aptoma\\Security\\User\\ApiKeyUserProviderInterface'); 41 | $userProvider->expects($this->once()) 42 | ->method('loadUserByApiKey') 43 | ->will($this->returnValue($user)); 44 | 45 | $encoder = $this->createMock('Aptoma\\Security\\Encoder\\SaltLessPasswordEncoderInterface'); 46 | 47 | $provider = new ApiKeyAuthenticationProvider($userProvider, $encoder); 48 | $token = $provider->authenticate(new ApiKeyToken('key')); 49 | 50 | $this->assertInstanceOf('Aptoma\\Security\\Authentication\\Token\\ApiKeyToken', $token); 51 | $this->assertSame($user, $token->getUser()); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /tests/src/Aptoma/Security/Authentication/Token/ApiKeyTokenTest.php: -------------------------------------------------------------------------------- 1 | assertFalse($token->isAuthenticated()); 15 | } 16 | 17 | public function testConstructWithRolesShouldAuthenticateToken() 18 | { 19 | $token = new ApiKeyToken('key', array('ROLE_USER')); 20 | $this->assertTrue($token->isAuthenticated()); 21 | } 22 | 23 | public function testConstructWithApiKShouldSetCredentials() 24 | { 25 | $token = new ApiKeyToken('key'); 26 | $this->assertSame('key', $token->getCredentials()); 27 | } 28 | 29 | /** 30 | * @expectedException \LogicException 31 | */ 32 | public function testAuthenticateAfterInstantiationShouldThrowException() 33 | { 34 | $token = new ApiKeyToken('key'); 35 | $token->setAuthenticated(true); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/src/Aptoma/Silex/ApplicationTest.php: -------------------------------------------------------------------------------- 1 | false, 'timer.start' => microtime(true)), 21 | $thresholds 22 | ) 23 | ); 24 | 25 | $app->register( 26 | new MonologServiceProvider(), 27 | array( 28 | 'monolog.handler' => new TestHandler() 29 | ) 30 | ); 31 | usleep(1001); 32 | $request = new Request(array(), array(), array(), array(), array(), array('QUERY_STRING' => 'foo=bar')); 33 | $app->logExecTime($request); 34 | $records = $app['monolog.handler']->getRecords(); 35 | 36 | $this->assertTrue($app['monolog.handler']->{$methodToCheck}()); 37 | $this->assertEquals('foo=bar', $records[0]['context']['query']); 38 | } 39 | 40 | public function logExecTimeDataProvider() 41 | { 42 | return array( 43 | 'debug' => array( 44 | array( 45 | 'timer.threshold_info' => 1000, 46 | 'timer.threshold_warning' => 5000, 47 | ), 48 | 'hasDebugRecords', 49 | ), 50 | 'info' => array( 51 | array( 52 | 'timer.threshold_info' => 1, 53 | 'timer.threshold_warning' => 5000, 54 | ), 55 | 'hasInfoRecords', 56 | ), 57 | 'warning' => array( 58 | array( 59 | 'timer.threshold_info' => 0, 60 | 'timer.threshold_warning' => 1, 61 | ), 62 | 'hasWarningRecords', 63 | ), 64 | ); 65 | } 66 | 67 | public function testRegisterLoggerShouldNotDoAnythingIfNameIsNotSet() 68 | { 69 | require_once 'Mocks/Application.php'; 70 | $app = new MockApplication(array()); 71 | 72 | $this->assertFalse($app->offsetExists('monolog')); 73 | } 74 | 75 | public function testRegisterLoggerShouldRegisterLogger() 76 | { 77 | require_once 'Mocks/Application.php'; 78 | $app = new MockApplication( 79 | array( 80 | 'monolog.name' => 'test', 81 | 'monolog.level' => 100, 82 | 'monolog.logfile' => false, 83 | ) 84 | ); 85 | 86 | $this->assertTrue($app->offsetExists('monolog')); 87 | } 88 | 89 | public function testRegisterTwigShouldDoNothingIfNoTemplatePathIsSet() 90 | { 91 | require_once 'Mocks/Application.php'; 92 | $app = new MockApplication(array()); 93 | 94 | $this->assertFalse($app->offsetExists('twig')); 95 | } 96 | 97 | public function testRegisterTwigShouldRegisterTwig() 98 | { 99 | require_once 'Mocks/Application.php'; 100 | $app = new MockApplication(array('twig.path' => __DIR__, 'twig.options' => array())); 101 | 102 | $this->assertTrue($app->offsetExists('twig')); 103 | } 104 | 105 | public function testRegisterTwigShouldAddAppExtensionIfAvailable() 106 | { 107 | require_once 'Mocks/Application.php'; 108 | 109 | $app = new MockApplication(array('twig.path' => __DIR__, 'twig.options' => array())); 110 | $this->assertFalse($app['twig']->hasExtension('App\Twig\Extension\AppExtension')); 111 | 112 | require_once 'Mocks/AppExtension.php'; 113 | $app = new MockApplication(array('twig.path' => __DIR__, 'twig.options' => array())); 114 | $this->assertTrue($app['twig']->hasExtension('App\Twig\Extension\AppExtension')); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /tests/src/Aptoma/Silex/Mocks/AppExtension.php: -------------------------------------------------------------------------------- 1 | registerLogger($this); 14 | $this->registerTwig($this); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tests/src/Aptoma/Silex/Provider/ExtendedLoggerServiceProviderTest.php: -------------------------------------------------------------------------------- 1 | register(new MonologServiceProvider()); 18 | $app->register(new ExtendedLoggerServiceProvider()); 19 | 20 | /** @var Logger $logger */ 21 | $logger = $app['logger']; 22 | $handler = $logger->popHandler(); 23 | 24 | $this->assertInstanceOf('Monolog\Formatter\LineFormatter', $handler->getFormatter()); 25 | } 26 | 27 | public function testLoggerShouldAddLogstashHandlerIfLogstashFileIsSet() 28 | { 29 | $app = new Application(); 30 | $app['monolog.logfile'] = false; 31 | $app['monolog.logstashfile'] = 'logstash.log'; 32 | $app->register(new MonologServiceProvider()); 33 | $app->register(new ExtendedLoggerServiceProvider()); 34 | 35 | /** @var Logger $logger */ 36 | $logger = $app['logger']; 37 | $handler = $logger->popHandler(); 38 | 39 | $this->assertInstanceOf('Monolog\Formatter\LogstashFormatter', $handler->getFormatter()); 40 | } 41 | 42 | public function testMetaFieldsAreSetIfAvailable() 43 | { 44 | $app = new Application(); 45 | $app['monolog.logfile'] = false; 46 | $app['monolog.logstashfile'] = 'logstash.log'; 47 | $app['meta.service'] = 'foo'; 48 | $app['meta.customer'] = 'bar'; 49 | $app['meta.environment'] = 'test'; 50 | $app->register(new MonologServiceProvider()); 51 | $app->register(new ExtendedLoggerServiceProvider()); 52 | 53 | /** @var Logger $logger */ 54 | $logger = $app['logger']; 55 | $handler = $logger->popHandler(); 56 | 57 | /** @var ExtraContextProcessor $processor */ 58 | $processor = $handler->popProcessor(); 59 | 60 | $this->assertInstanceOf('Aptoma\Log\ExtraContextProcessor', $processor); 61 | 62 | $record = $processor(array()); 63 | 64 | $this->assertEquals(array('service' => 'foo', 'customer' => 'bar', 'environment' => 'test'), $record['extra']); 65 | } 66 | 67 | public function testCoverBoot() 68 | { 69 | $app = new Application(); 70 | $app['monolog.logfile'] = false; 71 | $app->register(new MonologServiceProvider()); 72 | $app->register(new ExtendedLoggerServiceProvider()); 73 | 74 | $app->boot(); 75 | $this->assertTrue(true); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /tests/src/Aptoma/Storage/FileStorageTest.php: -------------------------------------------------------------------------------- 1 | storageDir = '/tmp/' . uniqid('silex-extras-' . time() . '-'); 18 | } 19 | 20 | public function testFileStorageShouldImplementStorageInterface() 21 | { 22 | $service = $this->createService(); 23 | $this->assertInstanceOf('\Aptoma\Storage\StorageInterface', $service); 24 | } 25 | 26 | public function testPutShouldStoreFileInSpecifiedDirectory() 27 | { 28 | $service = $this->createService(); 29 | $fileToRead = __DIR__ . '/../fixtures/topgun.jpg'; 30 | $resource = fopen($fileToRead, 'rb'); 31 | $identifier = $service->put($resource); 32 | 33 | $this->assertFileExists($this->storageDir . '/' . $identifier); 34 | $this->assertEquals(filesize($fileToRead), filesize($this->storageDir . '/' . $identifier)); 35 | } 36 | 37 | public function testPutShouldHandleUploadFileObject() 38 | { 39 | $fileToRead = __DIR__ . '/../fixtures/topgun.jpg'; 40 | $file = new UploadedFile($fileToRead, 'topgun.jpg'); 41 | $service = $this->createService(); 42 | 43 | try { 44 | $service->put($file); 45 | $this->fail('Put should fail for uploaded file.'); 46 | } catch (FileException $e) { 47 | $this->assertTrue(true); 48 | return; 49 | } 50 | 51 | $this->fail('Put should fail throw FileException.'); 52 | } 53 | 54 | public function testGetMimeTypeShouldReturnMimeTypeOfAsset() 55 | { 56 | $service = $this->createService(); 57 | $resource = fopen(__DIR__ . '/../fixtures/topgun.jpg', 'rb'); 58 | $identifier = $service->put($resource); 59 | 60 | $this->assertEquals('image/jpeg', $service->getMimeType($identifier)); 61 | } 62 | 63 | public function testGetUrlShouldReturnUrlToFile() 64 | { 65 | $service = $this->createService(); 66 | 67 | $this->assertEquals('http://www.example.com/files/filename.jpg/raw', $service->getUrl('filename.jpg')); 68 | } 69 | 70 | /** 71 | * @expectedException \Aptoma\Storage\Exception\FileNotFoundException 72 | */ 73 | public function testGetRawShouldThrowExceptionForNonExistingFile() 74 | { 75 | $service = $this->createService(); 76 | $service->getRaw('unkown'); 77 | } 78 | 79 | public function testGetRawTypeShouldReturnRawDataEqualToStoredData() 80 | { 81 | $service = $this->createService(); 82 | $resource = file_get_contents(__DIR__ . '/../fixtures/topgun.jpg'); 83 | $identifier = $service->put($resource); 84 | 85 | $this->assertEquals($resource, $service->getRaw($identifier)); 86 | } 87 | 88 | public function testGetRawTypeShouldReturnResourceWhenAsResourceArgumentIsProvided() 89 | { 90 | $service = $this->createService(); 91 | $resource = file_get_contents(__DIR__ . '/../fixtures/topgun.jpg'); 92 | $identifier = $service->put($resource); 93 | 94 | $this->assertInternalType('resource', $service->getRaw($identifier, true)); 95 | } 96 | 97 | /** 98 | * @return FileStorage 99 | */ 100 | private function createService() 101 | { 102 | $logger = new Logger('test'); 103 | 104 | $service = new FileStorage($this->storageDir, $this->publicUrlTemplate, $logger); 105 | 106 | return $service; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /tests/src/Aptoma/TestToolkit/BaseWebTestCaseTest.php: -------------------------------------------------------------------------------- 1 | pathToAppBootstrap = __DIR__ . '/mocks/app.php'; 14 | parent::setUp(); 15 | } 16 | 17 | public function testCreateApplication() 18 | { 19 | $app = $this->createApplication(); 20 | $this->assertInstanceOf('Mock\Application', $app); 21 | } 22 | 23 | public function testCreateClient() 24 | { 25 | $this->assertInstanceOf('Aptoma\TestToolkit\TestClient', $this->createClient()); 26 | } 27 | 28 | public function testCreateAuthorizedClient() 29 | { 30 | $client = $this->createAuthorizedClient(array('test_key' => 'test_value')); 31 | 32 | $this->assertEquals('test_value', $client->getServerParameter('test_key')); 33 | $this->assertEquals('username', $client->getServerParameter('PHP_AUTH_USER')); 34 | $this->assertEquals('password', $client->getServerParameter('PHP_AUTH_PW')); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tests/src/Aptoma/TestToolkit/TestClientTest.php: -------------------------------------------------------------------------------- 1 | createMockTestClient(); 14 | $client 15 | ->expects($this->once()) 16 | ->method('request') 17 | ->with( 18 | $this->equalTo('POST'), 19 | $this->equalTo('/url'), 20 | $this->equalTo(array()), 21 | $this->equalTo(array()), 22 | $this->equalTo(array('CONTENT_TYPE' => 'application/json', 'HTTP_ACCEPT' => 'application/json')), 23 | $this->equalTo(json_encode(array('foo' => 'bar'))) 24 | ); 25 | 26 | $client->postJson('/url', array('foo' => 'bar')); 27 | } 28 | 29 | public function testPutJson() 30 | { 31 | $client = $this->createMockTestClient(); 32 | $client 33 | ->expects($this->once()) 34 | ->method('request') 35 | ->with( 36 | $this->equalTo('PUT'), 37 | $this->equalTo('/url'), 38 | $this->equalTo(array()), 39 | $this->equalTo(array()), 40 | $this->equalTo(array('CONTENT_TYPE' => 'application/json', 'HTTP_ACCEPT' => 'application/json')), 41 | $this->equalTo(json_encode(array('foo' => 'bar'))) 42 | ); 43 | 44 | $client->putJson('/url', array('foo' => 'bar')); 45 | } 46 | 47 | public function testGetResponse() 48 | { 49 | $client = $this->createClient(); 50 | $this->app->get( 51 | '/', 52 | function () { 53 | return 'index'; 54 | } 55 | ); 56 | 57 | $client->request('GET', '/'); 58 | $this->assertInstanceOf('\Symfony\Component\HttpFoundation\Response', $client->getResponse()); 59 | } 60 | 61 | public function testGetJsonDecodedResponseBody() 62 | { 63 | $client = $this->getMockbuilder('\Aptoma\TestToolkit\TestClient') 64 | ->setMethods(array('getResponse')) 65 | ->setConstructorArgs(array($this->app)) 66 | ->getMock(); 67 | 68 | $data = array('foo' => 'bar'); 69 | $response = new Response(json_encode($data), 200); 70 | $client 71 | ->expects($this->any()) 72 | ->method('getResponse') 73 | ->will($this->returnValue($response)); 74 | 75 | $this->assertEquals($data, $client->getJsonDecodedResponseBody()); 76 | } 77 | 78 | private function createMockTestClient() 79 | { 80 | return $this->getMockBuilder('\Aptoma\TestToolkit\TestClient') 81 | ->setMethods(array('request')) 82 | ->setConstructorArgs(array($this->app)) 83 | ->getMock(); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /tests/src/Aptoma/TestToolkit/mocks/Application.php: -------------------------------------------------------------------------------- 1 |